Implemented confi snapshots
This commit is contained in:
@@ -75,6 +75,88 @@ function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Reads a single parameter's value from a config instance, coercing it to a
|
||||
// number. Returns null if the instance/set/param is missing or non-numeric.
|
||||
async function readConfigParam(instanceId: string, key: string): Promise<number | null> {
|
||||
try {
|
||||
const ir = await fetch(`/api/v1/config/instances/${encodeURIComponent(instanceId)}`);
|
||||
if (!ir.ok) return null;
|
||||
const inst: { setId: string; setVersion?: number; values?: Record<string, any> } = await ir.json();
|
||||
const sv = inst.setVersion && inst.setVersion > 0
|
||||
? `/api/v1/config/sets/${encodeURIComponent(inst.setId)}/versions/${inst.setVersion}`
|
||||
: `/api/v1/config/sets/${encodeURIComponent(inst.setId)}`;
|
||||
const sr = await fetch(sv);
|
||||
if (!sr.ok) return null;
|
||||
const set: { parameters?: Array<{ key: string; default?: any }> } = await sr.json();
|
||||
const param = (set.parameters ?? []).find(p => p.key === key);
|
||||
if (!param) return null;
|
||||
const raw = inst.values?.[key] ?? param.default;
|
||||
const n = toNum(raw);
|
||||
return isNaN(n) ? null : n;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Write a single numeric value into a config instance's parameter, creating a
|
||||
// new revision server-side. Reads the current instance, overwrites values[key],
|
||||
// and PUTs it back. Silently no-ops on any transport/parse error.
|
||||
async function writeConfigParam(instanceId: string, key: string, val: number): Promise<void> {
|
||||
try {
|
||||
const ir = await fetch(`/api/v1/config/instances/${encodeURIComponent(instanceId)}`);
|
||||
if (!ir.ok) return;
|
||||
const inst: any = await ir.json();
|
||||
inst.values = { ...(inst.values ?? {}), [key]: val };
|
||||
await fetch(`/api/v1/config/instances/${encodeURIComponent(instanceId)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(inst),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Create a new config instance for `setId`, optionally seeding its values from
|
||||
// an existing instance `fromId`. Returns the new instance id, or '' on failure.
|
||||
async function createConfigInstance(setId: string, name: string, fromId: string): Promise<string> {
|
||||
try {
|
||||
let values: Record<string, any> = {};
|
||||
if (fromId) {
|
||||
const fr = await fetch(`/api/v1/config/instances/${encodeURIComponent(fromId)}`);
|
||||
if (fr.ok) {
|
||||
const src: any = await fr.json();
|
||||
values = { ...(src.values ?? {}) };
|
||||
}
|
||||
}
|
||||
const r = await fetch('/api/v1/config/instances', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, setId, values }),
|
||||
});
|
||||
if (!r.ok) return '';
|
||||
const out: any = await r.json();
|
||||
return String(out?.id ?? '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot the current live values of every target signal of `setId` into a new
|
||||
// config instance (optional label). Returns the new instance id, or '' on failure.
|
||||
async function snapshotConfigSet(setId: string, name: string): Promise<string> {
|
||||
try {
|
||||
const r = await fetch(`/api/v1/config/sets/${encodeURIComponent(setId)}/snapshot`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!r.ok) return '';
|
||||
const out: any = await r.json();
|
||||
return String(out?.instance?.id ?? '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function toNum(v: any): number {
|
||||
if (typeof v === 'number') return v;
|
||||
if (typeof v === 'boolean') return v ? 1 : 0;
|
||||
@@ -158,6 +240,20 @@ class LogicEngine {
|
||||
return toNum(this.live.get(refKey(ds, name)));
|
||||
}
|
||||
|
||||
// Resolve the config-instance id a node operates on. With instanceSource
|
||||
// 'var' the id is read (as a raw string) from a panel-local variable / signal
|
||||
// — this is how a config-selector widget feeds the apply/read/write nodes.
|
||||
// Otherwise the fixed `instance` param (an id chosen at design time) is used.
|
||||
private resolveInstanceId(node: LogicNode): string {
|
||||
if ((node.params.instanceSource ?? 'fixed') === 'var') {
|
||||
const r = parseRef(node.params.instanceVar ?? '');
|
||||
if (!r) return '';
|
||||
const v = this.live.get(refKey(r.ds, r.name));
|
||||
return v == null ? '' : String(v).trim();
|
||||
}
|
||||
return (node.params.instance ?? '').trim();
|
||||
}
|
||||
|
||||
/** Activate a panel's logic graph. Replaces any previously loaded graph. */
|
||||
load(graph: LogicGraph | undefined): void {
|
||||
this.clear();
|
||||
@@ -266,6 +362,18 @@ class LogicEngine {
|
||||
case 'action.write':
|
||||
case 'action.accumulate':
|
||||
case 'action.log': collectRefs(node.params.expr ?? '').forEach(want); break;
|
||||
case 'action.config.apply':
|
||||
case 'action.config.read':
|
||||
case 'action.config.write': {
|
||||
// Dynamic-instance nodes read their target instance id from a panel
|
||||
// var; subscribe it so the live cache holds the selected id.
|
||||
if ((node.params.instanceSource ?? 'fixed') === 'var') {
|
||||
const r = parseRef(node.params.instanceVar ?? '');
|
||||
if (r) want(r);
|
||||
}
|
||||
if (node.kind === 'action.config.write') collectRefs(node.params.expr ?? '').forEach(want);
|
||||
break;
|
||||
}
|
||||
case 'action.dialog.info':
|
||||
case 'action.dialog.error':
|
||||
case 'action.dialog.setpoint': {
|
||||
@@ -390,6 +498,65 @@ class LogicEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
case 'action.config.apply': {
|
||||
const id = this.resolveInstanceId(node);
|
||||
if (id) {
|
||||
try {
|
||||
await fetch(`/api/v1/config/instances/${encodeURIComponent(id)}/apply`, { method: 'POST' });
|
||||
} catch {}
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'action.config.read': {
|
||||
const id = this.resolveInstanceId(node);
|
||||
const key = (node.params.key ?? '').trim();
|
||||
const ref = parseRef(node.params.target ?? '');
|
||||
if (id && key && ref) {
|
||||
const val = await readConfigParam(id, key);
|
||||
if (val !== null && !isNaN(val)) wsClient.write(ref, val);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'action.config.write': {
|
||||
const id = this.resolveInstanceId(node);
|
||||
const key = (node.params.key ?? '').trim();
|
||||
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
|
||||
if (id && key && !isNaN(val)) {
|
||||
await writeConfigParam(id, key, val);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'action.config.create': {
|
||||
const setId = (node.params.set ?? '').trim();
|
||||
const name = (node.params.name ?? '').trim() || 'auto';
|
||||
const fromId = (node.params.from ?? '').trim();
|
||||
const target = parseRef(node.params.target ?? '');
|
||||
if (setId) {
|
||||
const newId = await createConfigInstance(setId, name, fromId);
|
||||
if (newId && target) wsClient.write(target, newId);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'action.config.snapshot': {
|
||||
const setId = (node.params.set ?? '').trim();
|
||||
const name = (node.params.name ?? '').trim();
|
||||
const target = parseRef(node.params.target ?? '');
|
||||
if (setId) {
|
||||
const newId = await snapshotConfigSet(setId, name);
|
||||
if (newId && target) wsClient.write(target, newId);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'action.delay':
|
||||
await sleep(Math.max(0, parseInt(node.params.ms ?? '0', 10) || 0));
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
|
||||
Reference in New Issue
Block a user