@camstack/system 1.2.308 → 1.2.309
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composed-device.d.ts +26 -0
- package/dist/builtins/composer/composer.addon.d.ts +12 -0
- package/dist/builtins/composer/composer.addon.js +1682 -0
- package/dist/builtins/composer/composer.addon.mjs +1677 -0
- package/dist/builtins/composer/composer.d.ts +103 -0
- package/dist/builtins/composer/composition-declarations.d.ts +53 -0
- package/dist/builtins/composer/composition-runtime.d.ts +88 -0
- package/dist/builtins/composer/deadline-timer.d.ts +18 -0
- package/dist/builtins/composer/index.d.ts +1 -0
- package/dist/builtins/composer/index.js +8 -0
- package/dist/builtins/composer/index.mjs +2 -0
- package/dist/builtins/composer/source-tracker.d.ts +78 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-supervisor.d.ts +32 -1
- package/dist/builtins/core-blocks/blocks-integration.d.ts +5 -0
- package/dist/builtins/core-blocks/composition-api.d.ts +25 -0
- package/dist/builtins/core-blocks/composition-sources.d.ts +10 -0
- package/dist/builtins/core-blocks/core-block-store.d.ts +34 -2
- package/dist/builtins/core-blocks/core-blocks.addon.d.ts +6 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +504 -11
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +504 -11
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +10 -31
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +9 -30
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/shared/device-state-changed.d.ts +17 -0
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/device-state-changed-Bk7cvTnz.js +31 -0
- package/dist/device-state-changed-DoBbkyEI.mjs +20 -0
- package/dist/{dist-DygWCI8k.mjs → dist-BEuQPDk_.mjs} +1286 -79
- package/dist/{dist-DGhk7Tay.js → dist-BhO9eYKO.js} +1375 -78
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.mjs +4 -2
- package/dist/{retired-settings-keys-z02aqTy7.mjs → retired-settings-keys-CQvG6B-Q.mjs} +1 -1
- package/dist/{retired-settings-keys-IZ9MH0GO.js → retired-settings-keys-DHgnJqS3.js} +1 -1
- package/package.json +20 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-BhO9eYKO.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -156,6 +156,27 @@ async function removeBlockPackage(root, blockId) {
|
|
|
156
156
|
}
|
|
157
157
|
//#endregion
|
|
158
158
|
//#region src/builtins/core-blocks/block-supervisor.ts
|
|
159
|
+
/**
|
|
160
|
+
* Converging the running block processes onto the stored blocks.
|
|
161
|
+
*
|
|
162
|
+
* Reconcile against the registry, not one-shot reactions to events: a spawn
|
|
163
|
+
* that was missed because an event was dropped (D8 — events are telemetry) has
|
|
164
|
+
* to be fixed by the next pass, and an operator restarting the hub must end up
|
|
165
|
+
* with exactly the blocks the store says are enabled. So `converge()` is
|
|
166
|
+
* idempotent and safe to call from a timer, from a write, and from boot.
|
|
167
|
+
*
|
|
168
|
+
* **The supervision itself is not in this file, and that is deliberate.** A
|
|
169
|
+
* block that never returns is bounded by `process-service`'s `CrashSupervisor`
|
|
170
|
+
* (D6) — the same circuit-breaker that bounds every addon. This class only
|
|
171
|
+
* decides which runners should exist and translates their state back into
|
|
172
|
+
* something the author can read. Re-implementing backoff here would be a second
|
|
173
|
+
* supervisor to keep in step with the first.
|
|
174
|
+
*
|
|
175
|
+
* The invariant it exists to hold: **a block that is not running says why.**
|
|
176
|
+
* Every path out of `converge()` that does not end in a live process writes a
|
|
177
|
+
* status the operator can see, with the reason attached. A block that is
|
|
178
|
+
* silently absent is the failure mode this whole feature has to avoid.
|
|
179
|
+
*/
|
|
159
180
|
/** Runner state → what the author should read. `crashed` is the window between
|
|
160
181
|
* an exit and the scheduled respawn, so it reads as `starting` with the reason
|
|
161
182
|
* attached rather than as a terminal failure it is not. */
|
|
@@ -180,6 +201,16 @@ var BlockSupervisor = class {
|
|
|
180
201
|
/** Serialises passes so a timer tick cannot interleave with a write-driven
|
|
181
202
|
* converge and spawn the same runner twice. */
|
|
182
203
|
inFlight = Promise.resolve();
|
|
204
|
+
/** Composition restarts not yet delivered to the composer. Stores nothing durable (D62). */
|
|
205
|
+
pendingCompositionRestarts = /* @__PURE__ */ new Set();
|
|
206
|
+
/**
|
|
207
|
+
* Empty applies still owed: the composer withdraws only after TWO agree
|
|
208
|
+
* (D49), and a boot sends one. Only an apply the composer says it ACTED on
|
|
209
|
+
* (`result.acted`) pays one off — it can refuse with the store loaded.
|
|
210
|
+
*/
|
|
211
|
+
emptyAppliesOwed = 2;
|
|
212
|
+
/** Consecutive applies that could not reach the composer: warned once per run. */
|
|
213
|
+
failedApplies = 0;
|
|
183
214
|
constructor(deps) {
|
|
184
215
|
this.deps = deps;
|
|
185
216
|
}
|
|
@@ -216,6 +247,11 @@ var BlockSupervisor = class {
|
|
|
216
247
|
* then never re-launch it.
|
|
217
248
|
*/
|
|
218
249
|
restart(blockId) {
|
|
250
|
+
const block = this.deps.store.get(blockId);
|
|
251
|
+
if (block !== null && require_dist.coreBlockSourceKind(block) === "composition") {
|
|
252
|
+
this.pendingCompositionRestarts = new Set([...this.pendingCompositionRestarts, blockId]);
|
|
253
|
+
return this.converge();
|
|
254
|
+
}
|
|
219
255
|
this.inFlight = this.inFlight.then(() => this.runRestart(blockId), () => this.runRestart(blockId));
|
|
220
256
|
return this.inFlight;
|
|
221
257
|
}
|
|
@@ -240,9 +276,11 @@ var BlockSupervisor = class {
|
|
|
240
276
|
}
|
|
241
277
|
}
|
|
242
278
|
async reconcile() {
|
|
279
|
+
if (!this.deps.storeLoaded()) await this.deps.loadStore();
|
|
243
280
|
const blocks = this.deps.store.list();
|
|
244
|
-
const mine = blocks.filter((b) => b.enabled && this.isPlacedHere(b));
|
|
245
|
-
const
|
|
281
|
+
const mine = blocks.filter((b) => b.enabled && this.isPlacedHere(b) && require_dist.coreBlockSourceKind(b) === "code");
|
|
282
|
+
const snapshots = await this.deps.host.list();
|
|
283
|
+
const runners = new Map(snapshots.map((r) => [blockIdFromAddonId(r.name), r]).filter((pair) => pair[0] !== null));
|
|
246
284
|
for (const block of blocks) {
|
|
247
285
|
if (!block.enabled || this.isPlacedHere(block)) continue;
|
|
248
286
|
await this.report(block.id, "failed", `no core-block supervisor runs on "${block.placement}" — this block is not running anywhere. Move it to "${this.deps.nodeId}" to run it.`);
|
|
@@ -260,6 +298,7 @@ var BlockSupervisor = class {
|
|
|
260
298
|
if (this.deps.store.get(blockId) !== null) await this.report(blockId, "stopped");
|
|
261
299
|
else await removeBlockPackage(this.deps.root, blockId);
|
|
262
300
|
}
|
|
301
|
+
for (const block of blocks) if (block.enabled && require_dist.coreBlockSourceKind(block) === "recipe") await this.report(block.id, "failed", "a recipe block cannot run yet — recipes are stored, not executed, in this release");
|
|
263
302
|
for (const block of mine) {
|
|
264
303
|
const runner = runners.get(block.id);
|
|
265
304
|
const stale = runner !== void 0 && this.materialized.get(block.id) !== block.updatedAt;
|
|
@@ -277,6 +316,71 @@ var BlockSupervisor = class {
|
|
|
277
316
|
}
|
|
278
317
|
await this.launch(block);
|
|
279
318
|
}
|
|
319
|
+
await this.convergeCompositions(blocks, snapshots);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Send every composition placed here to the composer and mirror its verdicts
|
|
323
|
+
* into the block status, reason verbatim. A `starting` verdict (the composer
|
|
324
|
+
* does not trust its store or seed yet) is mirrored as `starting`; the next
|
|
325
|
+
* pass of this same converge is the retry — no timer of its own (D3).
|
|
326
|
+
*/
|
|
327
|
+
async convergeCompositions(blocks, runners) {
|
|
328
|
+
const compositions = blocks.flatMap((block) => {
|
|
329
|
+
const composition = require_dist.coreBlockComposition(block);
|
|
330
|
+
return composition !== null && this.isPlacedHere(block) ? [{
|
|
331
|
+
block,
|
|
332
|
+
composition
|
|
333
|
+
}] : [];
|
|
334
|
+
});
|
|
335
|
+
const unreadable = this.deps.store.unreadableCompositions();
|
|
336
|
+
const nothingToSay = compositions.length === 0 && unreadable.length === 0;
|
|
337
|
+
if (nothingToSay && this.emptyAppliesOwed === 0) return;
|
|
338
|
+
const composer = this.deps.composer;
|
|
339
|
+
if (composer === null) {
|
|
340
|
+
for (const { block } of compositions) await this.report(block.id, "failed", "no composer is available on this node — a composition cannot run here");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (runners.find((r) => r.name === "composer")?.state === "failed") {
|
|
344
|
+
for (const { block } of compositions) await this.report(block.id, "failed", "the composer runner's crash circuit-breaker tripped — no composition runs until it is restarted");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const restart = this.pendingCompositionRestarts;
|
|
348
|
+
const integrationId = this.deps.integrationId();
|
|
349
|
+
const storeLoaded = this.deps.storeLoaded();
|
|
350
|
+
try {
|
|
351
|
+
const result = await composer.apply({
|
|
352
|
+
integrationId,
|
|
353
|
+
storeLoaded,
|
|
354
|
+
blocks: compositions.map(({ block, composition }) => ({
|
|
355
|
+
id: block.id,
|
|
356
|
+
name: block.name,
|
|
357
|
+
enabled: block.enabled,
|
|
358
|
+
updatedAt: block.updatedAt,
|
|
359
|
+
composition
|
|
360
|
+
})),
|
|
361
|
+
restartBlockIds: [...restart],
|
|
362
|
+
unreadableBlocks: [...unreadable]
|
|
363
|
+
});
|
|
364
|
+
if (this.failedApplies > 0) {
|
|
365
|
+
this.deps.logger.info("the composer answered again", { meta: { failedApplies: this.failedApplies } });
|
|
366
|
+
this.failedApplies = 0;
|
|
367
|
+
}
|
|
368
|
+
this.pendingCompositionRestarts = new Set([...this.pendingCompositionRestarts].filter((id) => !restart.has(id)));
|
|
369
|
+
this.emptyAppliesOwed = !nothingToSay || !result.acted ? 2 : Math.max(0, this.emptyAppliesOwed - 1);
|
|
370
|
+
const verdicts = new Map(result.blocks.map((v) => [v.blockId, v]));
|
|
371
|
+
for (const { block } of compositions) {
|
|
372
|
+
const verdict = verdicts.get(block.id);
|
|
373
|
+
if (verdict === void 0) await this.report(block.id, "starting", "the composer did not answer for this block");
|
|
374
|
+
else await this.report(block.id, verdict.status, verdict.reason);
|
|
375
|
+
}
|
|
376
|
+
} catch (err) {
|
|
377
|
+
this.failedApplies += 1;
|
|
378
|
+
if (this.failedApplies === 1) this.deps.logger.warn("the composer could not be reached — compositions keep their last status", { meta: {
|
|
379
|
+
error: require_dist.errMsg(err),
|
|
380
|
+
compositions: compositions.length
|
|
381
|
+
} });
|
|
382
|
+
for (const { block } of compositions) if (block.status === "stopped" && block.enabled) await this.report(block.id, "starting", `the composer is not reachable yet: ${require_dist.errMsg(err)}`);
|
|
383
|
+
}
|
|
280
384
|
}
|
|
281
385
|
/** Compile, write the package, spawn. Each failure is reported ON the block
|
|
282
386
|
* — the author asked for this to run, so they are owed the reason it does
|
|
@@ -547,12 +651,42 @@ var CORE_BLOCKS_INDEXES = [{
|
|
|
547
651
|
name: "idx_core_blocks_enabled",
|
|
548
652
|
columns: ["enabled"]
|
|
549
653
|
}];
|
|
654
|
+
/**
|
|
655
|
+
* Rows one read asks for. A read that ANSWERS this many is not a loaded table:
|
|
656
|
+
* rows past the limit would be absent, and an absent composition reads as a
|
|
657
|
+
* deleted one (D49).
|
|
658
|
+
*/
|
|
659
|
+
var CORE_BLOCKS_LOAD_LIMIT = 1e3;
|
|
660
|
+
/**
|
|
661
|
+
* What an unparseable row's `source.kind` says, or null when it cannot be told.
|
|
662
|
+
* Absent means `code`, as it does for a readable row.
|
|
663
|
+
*/
|
|
664
|
+
function rawSourceKind(raw) {
|
|
665
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
666
|
+
if (!("source" in raw) || raw.source === void 0) return "code";
|
|
667
|
+
const source = raw.source;
|
|
668
|
+
if (typeof source !== "object" || source === null || !("kind" in source)) return null;
|
|
669
|
+
return typeof source.kind === "string" ? source.kind : null;
|
|
670
|
+
}
|
|
671
|
+
/** An unreadable row that may be a composition: one whose kind says so, or cannot be told. */
|
|
672
|
+
function mayBeComposition(raw) {
|
|
673
|
+
const kind = rawSourceKind(raw);
|
|
674
|
+
return kind !== "code" && kind !== "recipe";
|
|
675
|
+
}
|
|
676
|
+
function parseErrorText(issues) {
|
|
677
|
+
return issues.map((i) => `${i.path.map(String).join(".")}: ${i.message}`).join("; ");
|
|
678
|
+
}
|
|
550
679
|
var CoreBlockStore = class {
|
|
551
680
|
byId = /* @__PURE__ */ new Map();
|
|
681
|
+
/** Rows that exist but do not parse and may be compositions — held for the composer, never dropped (D49). */
|
|
682
|
+
unreadable = [];
|
|
552
683
|
store;
|
|
553
684
|
logger;
|
|
554
685
|
now;
|
|
555
686
|
newId;
|
|
687
|
+
loaded = false;
|
|
688
|
+
/** Consecutive failed reads — the warn is once per run of them, not once per retry. */
|
|
689
|
+
failedLoads = 0;
|
|
556
690
|
constructor(deps) {
|
|
557
691
|
this.store = deps.store;
|
|
558
692
|
this.logger = deps.logger;
|
|
@@ -566,32 +700,70 @@ var CoreBlockStore = class {
|
|
|
566
700
|
indexes: [...CORE_BLOCKS_INDEXES]
|
|
567
701
|
});
|
|
568
702
|
}
|
|
703
|
+
/** True only after a `load()` that READ the table. The composer withdraws nothing otherwise (D49). */
|
|
704
|
+
isLoaded() {
|
|
705
|
+
return this.loaded;
|
|
706
|
+
}
|
|
707
|
+
/** Stored composition rows that do not parse, by id — the composer holds their devices. */
|
|
708
|
+
unreadableCompositions() {
|
|
709
|
+
return this.unreadable;
|
|
710
|
+
}
|
|
569
711
|
/**
|
|
570
712
|
* Hydrate every block.
|
|
571
713
|
*
|
|
572
714
|
* A row whose JSON no longer validates is SKIPPED with a warning rather than
|
|
573
715
|
* dropped or repaired: it is somebody's code, and the honest answer to "I
|
|
574
|
-
* cannot read this" is to leave it on disk and say so.
|
|
716
|
+
* cannot read this" is to leave it on disk and say so. A skipped row that may
|
|
717
|
+
* be a COMPOSITION is also kept in `unreadableCompositions()`: absent from the
|
|
718
|
+
* composer's input it would read as deleted, and its device would go (D49).
|
|
719
|
+
*
|
|
720
|
+
* `source` is parsed here, and a build older than D659 strips `source` and
|
|
721
|
+
* rewrites the row — rolling back below D659 erases compositions.
|
|
575
722
|
*/
|
|
576
723
|
async load() {
|
|
577
724
|
try {
|
|
578
725
|
const rows = await this.store.query.query({
|
|
579
726
|
collection: CORE_BLOCKS_COLLECTION,
|
|
580
|
-
filter: { limit:
|
|
727
|
+
filter: { limit: CORE_BLOCKS_LOAD_LIMIT }
|
|
581
728
|
});
|
|
729
|
+
if (rows.length >= 1e3) throw new Error(`core_blocks answered ${rows.length} rows, the read limit — rows past it would read as deleted`);
|
|
582
730
|
this.byId.clear();
|
|
731
|
+
this.loaded = true;
|
|
732
|
+
if (this.failedLoads > 0) {
|
|
733
|
+
this.logger.info("core blocks loaded after failed reads", { meta: { failedAttempts: this.failedLoads } });
|
|
734
|
+
this.failedLoads = 0;
|
|
735
|
+
}
|
|
583
736
|
let skipped = 0;
|
|
737
|
+
const unreadable = [];
|
|
584
738
|
for (const row of rows) {
|
|
585
|
-
const
|
|
739
|
+
const raw = row.data["block"];
|
|
740
|
+
const parsed = require_dist.CoreBlockSchema.safeParse(raw);
|
|
586
741
|
if (!parsed.success) {
|
|
587
742
|
skipped += 1;
|
|
743
|
+
if (mayBeComposition(raw)) {
|
|
744
|
+
const name = row.data["name"];
|
|
745
|
+
unreadable.push({
|
|
746
|
+
id: row.id,
|
|
747
|
+
name: typeof name === "string" ? name : null,
|
|
748
|
+
error: parseErrorText(parsed.error.issues)
|
|
749
|
+
});
|
|
750
|
+
}
|
|
588
751
|
continue;
|
|
589
752
|
}
|
|
590
753
|
this.byId.set(parsed.data.id, parsed.data);
|
|
591
754
|
}
|
|
592
|
-
|
|
755
|
+
this.unreadable = unreadable;
|
|
756
|
+
if (skipped > 0) this.logger.warn("core blocks skipped as unreadable — left on disk, not repaired", { meta: {
|
|
757
|
+
skipped,
|
|
758
|
+
heldForComposer: unreadable.map((u) => u.id)
|
|
759
|
+
} });
|
|
593
760
|
} catch (err) {
|
|
594
|
-
this.
|
|
761
|
+
this.failedLoads += 1;
|
|
762
|
+
if (this.failedLoads === 1) this.logger.warn("core block load failed — retried on every converge until it reads", { meta: { error: String(err) } });
|
|
763
|
+
else this.logger.debug("core block load failed again", { meta: {
|
|
764
|
+
error: String(err),
|
|
765
|
+
failedAttempts: this.failedLoads
|
|
766
|
+
} });
|
|
595
767
|
}
|
|
596
768
|
}
|
|
597
769
|
list() {
|
|
@@ -605,6 +777,7 @@ var CoreBlockStore = class {
|
|
|
605
777
|
return this.list().filter((b) => b.enabled && b.placement === nodeId);
|
|
606
778
|
}
|
|
607
779
|
async create(input, createdBy) {
|
|
780
|
+
this.requireLoaded();
|
|
608
781
|
const now = this.now();
|
|
609
782
|
const block = require_dist.CoreBlockSchema.parse({
|
|
610
783
|
...input,
|
|
@@ -623,13 +796,61 @@ var CoreBlockStore = class {
|
|
|
623
796
|
async update(blockId, patch) {
|
|
624
797
|
const existing = this.byId.get(blockId);
|
|
625
798
|
if (!existing) throw new Error(`core block not found: ${blockId}`);
|
|
799
|
+
if ((patch.code !== void 0 && patch.code !== existing.code || patch.placement !== void 0 && patch.placement !== existing.placement || patch.integrationId !== void 0 && patch.integrationId !== existing.integrationId) && require_dist.coreBlockSourceKind(existing) !== "code") throw new Error(`core block "${existing.name}" is a composition — its code, placement and integration are fixed; edit it with updateComposition`);
|
|
800
|
+
if (patch.name !== void 0 && patch.name !== existing.name && require_dist.coreBlockSourceKind(existing) !== "code") throw new Error(`core block "${existing.name}" is a composition — rename it with updateComposition, which checks expectedUpdatedAt`);
|
|
626
801
|
const updated = require_dist.CoreBlockSchema.parse({
|
|
627
802
|
...existing,
|
|
628
803
|
...patch,
|
|
629
804
|
id: existing.id,
|
|
630
805
|
createdBy: existing.createdBy,
|
|
631
806
|
createdAt: existing.createdAt,
|
|
632
|
-
updatedAt: this.now()
|
|
807
|
+
updatedAt: Math.max(this.now(), existing.updatedAt + 1)
|
|
808
|
+
});
|
|
809
|
+
await this.persist(updated);
|
|
810
|
+
this.byId.set(updated.id, updated);
|
|
811
|
+
return updated;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Store a composition block (D659): hub-placed, no code, never `running` on
|
|
815
|
+
* creation — the composer's verdict, mirrored by the supervisor, says that.
|
|
816
|
+
*/
|
|
817
|
+
async createComposition(input, createdBy) {
|
|
818
|
+
this.requireLoaded();
|
|
819
|
+
const now = this.now();
|
|
820
|
+
const block = require_dist.CoreBlockSchema.parse({
|
|
821
|
+
name: input.name,
|
|
822
|
+
enabled: input.enabled,
|
|
823
|
+
placement: "hub",
|
|
824
|
+
code: "",
|
|
825
|
+
source: {
|
|
826
|
+
kind: "composition",
|
|
827
|
+
composition: input.composition
|
|
828
|
+
},
|
|
829
|
+
id: this.newId(),
|
|
830
|
+
createdBy,
|
|
831
|
+
createdAt: now,
|
|
832
|
+
updatedAt: now,
|
|
833
|
+
status: "stopped",
|
|
834
|
+
lastChangedAt: now
|
|
835
|
+
});
|
|
836
|
+
await this.persist(block);
|
|
837
|
+
this.byId.set(block.id, block);
|
|
838
|
+
return block;
|
|
839
|
+
}
|
|
840
|
+
/** Patch a composition block. Immutable: returns the NEW block. Refuses a code block by name. */
|
|
841
|
+
async updateComposition(blockId, patch) {
|
|
842
|
+
const existing = this.byId.get(blockId);
|
|
843
|
+
if (!existing) throw new Error(`core block not found: ${blockId}`);
|
|
844
|
+
if (require_dist.coreBlockSourceKind(existing) !== "composition") throw new Error(`core block "${existing.name}" is a code block — edit it with update`);
|
|
845
|
+
const updated = require_dist.CoreBlockSchema.parse({
|
|
846
|
+
...existing,
|
|
847
|
+
...patch.name === void 0 ? {} : { name: patch.name },
|
|
848
|
+
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
849
|
+
...patch.composition === void 0 ? {} : { source: {
|
|
850
|
+
kind: "composition",
|
|
851
|
+
composition: patch.composition
|
|
852
|
+
} },
|
|
853
|
+
updatedAt: Math.max(this.now(), existing.updatedAt + 1)
|
|
633
854
|
});
|
|
634
855
|
await this.persist(updated);
|
|
635
856
|
this.byId.set(updated.id, updated);
|
|
@@ -655,9 +876,17 @@ var CoreBlockStore = class {
|
|
|
655
876
|
await this.persist(next);
|
|
656
877
|
this.byId.set(blockId, next);
|
|
657
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* A write before the table has been read would be wiped from memory by the
|
|
881
|
+
* load that follows (it clears and refills), so it is refused by name.
|
|
882
|
+
*/
|
|
883
|
+
requireLoaded() {
|
|
884
|
+
if (!this.loaded) throw new Error("core_blocks has not loaded yet — nothing can be created until it has; retry shortly");
|
|
885
|
+
}
|
|
658
886
|
/** Idempotent delete — an unknown id is a no-op. */
|
|
659
887
|
async delete(blockId) {
|
|
660
888
|
this.byId.delete(blockId);
|
|
889
|
+
this.unreadable = this.unreadable.filter((u) => u.id !== blockId);
|
|
661
890
|
await this.store.delete.mutate({
|
|
662
891
|
collection: CORE_BLOCKS_COLLECTION,
|
|
663
892
|
key: blockId
|
|
@@ -722,6 +951,11 @@ var BLOCKS_INTEGRATION_NAME = "Blocks";
|
|
|
722
951
|
* with every device on the cluster, and the sweep would delete the blocks'), or
|
|
723
952
|
* make `core-blocks` declare a device of its own. Neither is worth the
|
|
724
953
|
* convenience; the spec test pins both.
|
|
954
|
+
*
|
|
955
|
+
* A SECOND owner hangs devices here since D659: `composer` (every composed
|
|
956
|
+
* device). It is equally safe for the same reason — its rows are owned by
|
|
957
|
+
* `composer`, never by `core-blocks`, and its own withdrawal sweep is scoped to
|
|
958
|
+
* `composer`.
|
|
725
959
|
*/
|
|
726
960
|
async function reconcileBlocksIntegration(ports) {
|
|
727
961
|
return (await new require_dist.DeclaredDevices({
|
|
@@ -779,6 +1013,229 @@ function armBlocksIntegration(options) {
|
|
|
779
1013
|
})() };
|
|
780
1014
|
}
|
|
781
1015
|
//#endregion
|
|
1016
|
+
//#region src/builtins/core-blocks/composition-api.ts
|
|
1017
|
+
/**
|
|
1018
|
+
* The composition half of the `core-blocks` provider (D659). Flat methods,
|
|
1019
|
+
* spread into the provider: no nested sub-object.
|
|
1020
|
+
*
|
|
1021
|
+
* Every save is VALIDATED against the live sources and every other stored
|
|
1022
|
+
* composition first; a refusal names each problem by path and reason (D391)
|
|
1023
|
+
* and stores nothing. The store's own refusals (not loaded, unknown block, a
|
|
1024
|
+
* code block) propagate unchanged — this layer never swallows them.
|
|
1025
|
+
*/
|
|
1026
|
+
var NOT_LOADED_REASON = "core_blocks has not loaded yet";
|
|
1027
|
+
function writeQueue() {
|
|
1028
|
+
let tail = Promise.resolve();
|
|
1029
|
+
return (write) => {
|
|
1030
|
+
const run = tail.then(write, write);
|
|
1031
|
+
tail = run.then(() => void 0, () => void 0);
|
|
1032
|
+
return run;
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Optimistic concurrency: an update names the `updatedAt` it read. A block
|
|
1037
|
+
* changed since is refused by name — last-write-wins would drop the other
|
|
1038
|
+
* admin's edit without a word. An unknown block falls through to the store,
|
|
1039
|
+
* whose own refusal names it.
|
|
1040
|
+
*/
|
|
1041
|
+
function refuseIfChanged(deps, blockId, expectedUpdatedAt) {
|
|
1042
|
+
const stored = deps.store.get(blockId);
|
|
1043
|
+
if (stored === null || stored.updatedAt === expectedUpdatedAt) return;
|
|
1044
|
+
deps.logger.warn("composition update refused: changed since read", { meta: {
|
|
1045
|
+
blockId,
|
|
1046
|
+
storedUpdatedAt: stored.updatedAt,
|
|
1047
|
+
expectedUpdatedAt
|
|
1048
|
+
} });
|
|
1049
|
+
throw new Error(`core-blocks: the composition "${stored.name}" changed since you read it: stored updatedAt ${stored.updatedAt}, yours ${expectedUpdatedAt} — reload and re-apply your edit`);
|
|
1050
|
+
}
|
|
1051
|
+
async function validateAgainstStore(deps, composition, blockId) {
|
|
1052
|
+
if (!deps.store.isLoaded()) throw new Error(`${NOT_LOADED_REASON} — a composition cannot be judged against the stored ones until it has; retry shortly`);
|
|
1053
|
+
const self = blockId === null ? null : require_dist.composedDeviceRef(blockId);
|
|
1054
|
+
const peers = deps.store.list().flatMap((block) => {
|
|
1055
|
+
const peer = require_dist.coreBlockComposition(block);
|
|
1056
|
+
return peer === null || block.id === blockId ? [] : [{
|
|
1057
|
+
target: require_dist.composedDeviceRef(block.id),
|
|
1058
|
+
composition: peer
|
|
1059
|
+
}];
|
|
1060
|
+
});
|
|
1061
|
+
const selfKey = self === null ? null : require_dist.compositionSourceKey(self);
|
|
1062
|
+
const refs = require_dist.compositionSourceRefs(composition).filter((r) => require_dist.compositionSourceKey(r) !== selfKey);
|
|
1063
|
+
const sources = await deps.describeSources(refs);
|
|
1064
|
+
return require_dist.validateComposition(composition, {
|
|
1065
|
+
lookupCap: deps.lookupCap,
|
|
1066
|
+
sources,
|
|
1067
|
+
self,
|
|
1068
|
+
peers
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
function describeProblems(validation) {
|
|
1072
|
+
return validation.problems.map((p) => `${p.path} [${p.code}]: ${p.message}`);
|
|
1073
|
+
}
|
|
1074
|
+
function refuse(deps, subject, validation) {
|
|
1075
|
+
const problems = describeProblems(validation);
|
|
1076
|
+
deps.logger.warn("composition save refused", { meta: {
|
|
1077
|
+
name: subject.name,
|
|
1078
|
+
blockId: subject.blockId,
|
|
1079
|
+
problems
|
|
1080
|
+
} });
|
|
1081
|
+
throw new Error(`core-blocks: the composition "${subject.name}" was refused — ${problems.join("; ")}`);
|
|
1082
|
+
}
|
|
1083
|
+
async function readStates(deps, blockIds) {
|
|
1084
|
+
try {
|
|
1085
|
+
const states = await deps.composerStates(blockIds);
|
|
1086
|
+
const byId = new Map(states.map((s) => [s.blockId, s]));
|
|
1087
|
+
return new Map(blockIds.map((id) => [id, {
|
|
1088
|
+
kind: "answered",
|
|
1089
|
+
state: byId.get(id) ?? null
|
|
1090
|
+
}]));
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
const error = require_dist.errMsg(err);
|
|
1093
|
+
deps.logger.warn("composer state unreachable", { meta: {
|
|
1094
|
+
blocks: blockIds.length,
|
|
1095
|
+
error
|
|
1096
|
+
} });
|
|
1097
|
+
return new Map(blockIds.map((id) => [id, {
|
|
1098
|
+
kind: "unreachable",
|
|
1099
|
+
error
|
|
1100
|
+
}]));
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function toViews(blocks, states) {
|
|
1104
|
+
return blocks.flatMap((block) => {
|
|
1105
|
+
const state = states.get(block.id);
|
|
1106
|
+
return state === void 0 ? [] : [{
|
|
1107
|
+
block,
|
|
1108
|
+
state
|
|
1109
|
+
}];
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Every write runs validate-then-persist on ONE chain. Validation awaits a
|
|
1114
|
+
* source read before the save lands; two unserialized saves (A reads B, B
|
|
1115
|
+
* reads A) would each be judged against a store without the other and BOTH
|
|
1116
|
+
* pass — a stored cycle nothing downstream catches.
|
|
1117
|
+
*/
|
|
1118
|
+
function buildCompositionApi(deps) {
|
|
1119
|
+
const serialized = writeQueue();
|
|
1120
|
+
return {
|
|
1121
|
+
createComposition: ({ block, caller }) => serialized(async () => {
|
|
1122
|
+
const validation = await validateAgainstStore(deps, block.composition, null);
|
|
1123
|
+
if (!validation.ok) refuse(deps, {
|
|
1124
|
+
name: block.name,
|
|
1125
|
+
blockId: null
|
|
1126
|
+
}, validation);
|
|
1127
|
+
const created = await deps.store.createComposition(block, caller.userId);
|
|
1128
|
+
deps.logger.info("composition created", { meta: {
|
|
1129
|
+
blockId: created.id,
|
|
1130
|
+
name: created.name,
|
|
1131
|
+
by: caller.userId
|
|
1132
|
+
} });
|
|
1133
|
+
deps.convergeSoon();
|
|
1134
|
+
return { block: created };
|
|
1135
|
+
}),
|
|
1136
|
+
updateComposition: ({ blockId, expectedUpdatedAt, block, caller }) => serialized(async () => {
|
|
1137
|
+
refuseIfChanged(deps, blockId, expectedUpdatedAt);
|
|
1138
|
+
if (block.composition !== void 0) {
|
|
1139
|
+
const validation = await validateAgainstStore(deps, block.composition, blockId);
|
|
1140
|
+
if (!validation.ok) refuse(deps, {
|
|
1141
|
+
name: block.name ?? blockId,
|
|
1142
|
+
blockId
|
|
1143
|
+
}, validation);
|
|
1144
|
+
}
|
|
1145
|
+
const updated = await deps.store.updateComposition(blockId, block);
|
|
1146
|
+
deps.logger.info("composition updated", { meta: {
|
|
1147
|
+
blockId,
|
|
1148
|
+
by: caller.userId
|
|
1149
|
+
} });
|
|
1150
|
+
deps.convergeSoon();
|
|
1151
|
+
return { block: updated };
|
|
1152
|
+
}),
|
|
1153
|
+
getComposition: async ({ blockId }) => {
|
|
1154
|
+
if (!deps.store.isLoaded()) return {
|
|
1155
|
+
state: "not-loaded",
|
|
1156
|
+
reason: NOT_LOADED_REASON
|
|
1157
|
+
};
|
|
1158
|
+
const block = deps.store.get(blockId);
|
|
1159
|
+
if (block === null || require_dist.coreBlockComposition(block) === null) return {
|
|
1160
|
+
state: "loaded",
|
|
1161
|
+
view: null
|
|
1162
|
+
};
|
|
1163
|
+
const [view] = toViews([block], await readStates(deps, [blockId]));
|
|
1164
|
+
return {
|
|
1165
|
+
state: "loaded",
|
|
1166
|
+
view: view ?? null
|
|
1167
|
+
};
|
|
1168
|
+
},
|
|
1169
|
+
listCompositions: async () => {
|
|
1170
|
+
if (!deps.store.isLoaded()) return {
|
|
1171
|
+
state: "not-loaded",
|
|
1172
|
+
reason: NOT_LOADED_REASON
|
|
1173
|
+
};
|
|
1174
|
+
const blocks = deps.store.list().filter((b) => require_dist.coreBlockComposition(b) !== null);
|
|
1175
|
+
if (blocks.length === 0) return {
|
|
1176
|
+
state: "loaded",
|
|
1177
|
+
views: []
|
|
1178
|
+
};
|
|
1179
|
+
return {
|
|
1180
|
+
state: "loaded",
|
|
1181
|
+
views: toViews(blocks, await readStates(deps, blocks.map((b) => b.id)))
|
|
1182
|
+
};
|
|
1183
|
+
},
|
|
1184
|
+
validateComposition: async ({ composition, blockId }) => validateAgainstStore(deps, composition, blockId ?? null)
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region src/builtins/core-blocks/composition-sources.ts
|
|
1189
|
+
/**
|
|
1190
|
+
* Describe a composition's sources for SAVE-time validation (D659): one
|
|
1191
|
+
* `listAll` per owning addon, one `getBindings` per found device. A read that
|
|
1192
|
+
* fails makes the source `unreadable`, and the save is REFUSED rather than
|
|
1193
|
+
* stored unchecked. Refusing a save destroys nothing (D49).
|
|
1194
|
+
*
|
|
1195
|
+
* Sources are keyed by `{addonId, stableId}` — never a numeric device id,
|
|
1196
|
+
* which is this node's allocation and not the composition's identity.
|
|
1197
|
+
*/
|
|
1198
|
+
async function listOwners(ports, refs) {
|
|
1199
|
+
const addons = [...new Set(refs.map((r) => r.addonId))];
|
|
1200
|
+
const listed = await Promise.all(addons.map(async (addonId) => {
|
|
1201
|
+
try {
|
|
1202
|
+
return [addonId, await ports.listByAddon(addonId)];
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
return [addonId, { error: require_dist.errMsg(err) }];
|
|
1205
|
+
}
|
|
1206
|
+
}));
|
|
1207
|
+
return new Map(listed);
|
|
1208
|
+
}
|
|
1209
|
+
async function describeOne(ports, ref, listing) {
|
|
1210
|
+
if (listing === void 0) return {
|
|
1211
|
+
kind: "unreadable",
|
|
1212
|
+
error: `\`${ref.addonId}\` was not listed`
|
|
1213
|
+
};
|
|
1214
|
+
if ("error" in listing) return {
|
|
1215
|
+
kind: "unreadable",
|
|
1216
|
+
error: listing.error
|
|
1217
|
+
};
|
|
1218
|
+
const row = listing.find((r) => r.stableId === ref.stableId);
|
|
1219
|
+
if (row === void 0) return { kind: "absent" };
|
|
1220
|
+
try {
|
|
1221
|
+
return {
|
|
1222
|
+
kind: "found",
|
|
1223
|
+
deviceId: row.id,
|
|
1224
|
+
caps: new Set(await ports.boundCaps(row.id))
|
|
1225
|
+
};
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
return {
|
|
1228
|
+
kind: "unreadable",
|
|
1229
|
+
error: require_dist.errMsg(err)
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
async function describeCompositionSources(ports, refs) {
|
|
1234
|
+
const owners = await listOwners(ports, refs);
|
|
1235
|
+
const described = await Promise.all(refs.map(async (ref) => [require_dist.compositionSourceKey(ref), await describeOne(ports, ref, owners.get(ref.addonId))]));
|
|
1236
|
+
return new Map(described);
|
|
1237
|
+
}
|
|
1238
|
+
//#endregion
|
|
782
1239
|
//#region src/builtins/core-blocks/core-blocks.addon.ts
|
|
783
1240
|
/**
|
|
784
1241
|
* The `core-blocks` provider — CRUD over user-authored TypeScript, with the
|
|
@@ -890,6 +1347,7 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
890
1347
|
store: {
|
|
891
1348
|
list: () => store.list(),
|
|
892
1349
|
get: (blockId) => store.get(blockId),
|
|
1350
|
+
unreadableCompositions: () => store.unreadableCompositions(),
|
|
893
1351
|
setStatus: (blockId, status, error) => store.setStatus(blockId, status, error)
|
|
894
1352
|
},
|
|
895
1353
|
host: createBrokerProcessHost(broker),
|
|
@@ -900,7 +1358,15 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
900
1358
|
};
|
|
901
1359
|
return compileBlock(this.transpiler, code);
|
|
902
1360
|
},
|
|
903
|
-
logger: this.ctx.logger.child("supervisor")
|
|
1361
|
+
logger: this.ctx.logger.child("supervisor"),
|
|
1362
|
+
composer: { apply: async (input) => {
|
|
1363
|
+
const api = this.ctx.api;
|
|
1364
|
+
if (!api) throw new Error("no ctx.api on this node — the composer cannot be reached");
|
|
1365
|
+
return api.composer.applyBlocks.mutate(input);
|
|
1366
|
+
} },
|
|
1367
|
+
integrationId: () => this.blocksIntegrationId,
|
|
1368
|
+
storeLoaded: () => store.isLoaded(),
|
|
1369
|
+
loadStore: () => store.load()
|
|
904
1370
|
});
|
|
905
1371
|
}
|
|
906
1372
|
/**
|
|
@@ -1036,8 +1502,35 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
1036
1502
|
filePath: l.filePath,
|
|
1037
1503
|
content: l.content
|
|
1038
1504
|
})) };
|
|
1039
|
-
}
|
|
1505
|
+
},
|
|
1506
|
+
...this.buildCompositionApi()
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* The composition methods (D659), flat. Sources are read over `ctx.api` at
|
|
1511
|
+
* SAVE time — one listing per owning addon, one binding read per found
|
|
1512
|
+
* device — and a read that fails refuses the save rather than skipping it.
|
|
1513
|
+
*/
|
|
1514
|
+
buildCompositionApi() {
|
|
1515
|
+
const requireApi = () => {
|
|
1516
|
+
const api = this.ctx.api;
|
|
1517
|
+
if (!api) throw new Error("core-blocks: no ctx.api on this node — compositions cannot be read");
|
|
1518
|
+
return api;
|
|
1040
1519
|
};
|
|
1520
|
+
return buildCompositionApi({
|
|
1521
|
+
store: this.requireStore(),
|
|
1522
|
+
describeSources: (refs) => describeCompositionSources({
|
|
1523
|
+
listByAddon: async (addonId) => requireApi().deviceManager.listAll.query({
|
|
1524
|
+
addonId,
|
|
1525
|
+
projection: "slim"
|
|
1526
|
+
}),
|
|
1527
|
+
boundCaps: async (deviceId) => (await requireApi().deviceManager.getBindings.query({ deviceId })).entries.map((e) => e.capName)
|
|
1528
|
+
}, refs),
|
|
1529
|
+
lookupCap: require_dist.buildCapabilityLookup(require_dist.ALL_CAPABILITY_DEFINITIONS),
|
|
1530
|
+
composerStates: async (blockIds) => (await requireApi().composer.getStates.query({ blockIds: [...blockIds] })).states,
|
|
1531
|
+
convergeSoon: () => this.convergeSoon(),
|
|
1532
|
+
logger: this.ctx.logger.child("compositions")
|
|
1533
|
+
});
|
|
1041
1534
|
}
|
|
1042
1535
|
};
|
|
1043
1536
|
/**
|