@camstack/system 1.2.313 → 1.2.315
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/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- 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/claim-gate.d.ts +32 -0
- package/dist/builtins/composer/composed-device-set.d.ts +38 -0
- package/dist/builtins/composer/composed-device.d.ts +4 -3
- package/dist/builtins/composer/composer-grafts.d.ts +54 -0
- package/dist/builtins/composer/composer-plan.d.ts +96 -0
- package/dist/builtins/composer/composer-verdict.d.ts +19 -0
- package/dist/builtins/composer/composer.addon.js +2717 -477
- package/dist/builtins/composer/composer.addon.mjs +2722 -482
- package/dist/builtins/composer/composer.d.ts +71 -52
- package/dist/builtins/composer/composition-runtime.d.ts +93 -9
- package/dist/builtins/composer/confirmed-seed.d.ts +32 -0
- package/dist/builtins/composer/existing-target.d.ts +96 -0
- package/dist/builtins/composer/field-record.d.ts +50 -0
- package/dist/builtins/composer/graft-host.d.ts +47 -0
- package/dist/builtins/composer/held-claims.d.ts +12 -0
- package/dist/builtins/composer/owned-fields-target.d.ts +55 -0
- package/dist/builtins/composer/slice-assembler.d.ts +10 -0
- package/dist/builtins/composer/source-readings.d.ts +122 -0
- package/dist/builtins/composer/source-tracker.d.ts +35 -42
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/composition-api.d.ts +7 -4
- package/dist/builtins/core-blocks/composition-peers.d.ts +9 -0
- package/dist/builtins/core-blocks/composition-sources.d.ts +16 -3
- package/dist/builtins/core-blocks/core-block-store.d.ts +8 -1
- package/dist/builtins/core-blocks/core-blocks.addon.d.ts +4 -3
- package/dist/builtins/core-blocks/core-blocks.addon.js +255 -79
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +255 -79
- package/dist/builtins/device-manager/claimed-status-overlay.d.ts +12 -0
- package/dist/builtins/device-manager/device-manager.addon.d.ts +16 -0
- package/dist/builtins/device-manager/device-manager.addon.js +1825 -317
- package/dist/builtins/device-manager/device-manager.addon.mjs +1825 -317
- package/dist/builtins/device-manager/device-provider-context.d.ts +24 -2
- package/dist/builtins/device-manager/device-row-store.d.ts +2 -0
- package/dist/builtins/device-manager/device-state-claim-views.d.ts +77 -0
- package/dist/builtins/device-manager/device-state-claims.d.ts +43 -0
- package/dist/builtins/device-manager/device-state-mirror.d.ts +145 -61
- package/dist/builtins/device-manager/field-claims-index.d.ts +69 -0
- package/dist/builtins/device-manager/field-ownership.d.ts +53 -0
- package/dist/builtins/device-manager/migrate-device.d.ts +27 -0
- package/dist/builtins/device-manager/migrate-guard.d.ts +7 -0
- package/dist/builtins/device-manager/migrate-hardware-state.d.ts +55 -0
- package/dist/builtins/device-manager/migration-refused.d.ts +23 -0
- package/dist/builtins/device-manager/mirror-row-writer.d.ts +138 -0
- package/dist/builtins/device-manager/runtime-state-persist-gate.d.ts +4 -0
- package/dist/builtins/doorbell/binding-mirror.d.ts +1 -0
- package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +59 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +3 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +315 -10
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +315 -10
- 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/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/{child-cap-dispatch-CHTiHbsv.mjs → child-cap-dispatch-AAltfBUH.mjs} +1484 -1417
- package/dist/{child-cap-dispatch-Dyn_7ezH.js → child-cap-dispatch-DzcUd_ct.js} +1503 -1424
- package/dist/composition-sources-DYqQLRzF.js +122 -0
- package/dist/composition-sources-yonsRneP.mjs +111 -0
- package/dist/{dist-CkRwnhbo.js → dist-DVKv5i-e.js} +5396 -4331
- package/dist/{dist-Cj7pJmXq.mjs → dist-MzYJVCLE.mjs} +5330 -4331
- package/dist/index.js +137 -8
- package/dist/index.mjs +133 -8
- package/dist/kernel/capability-registry.d.ts +43 -1
- package/dist/kernel/index.d.ts +3 -1
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +0 -10
- package/dist/kernel/status-overlay.d.ts +18 -0
- package/dist/kernel/transport/claimed-get-status.d.ts +15 -0
- package/dist/kernel/transport/index.d.ts +2 -0
- package/dist/kernel/transport/local-child-registry.d.ts +23 -0
- package/dist/kernel/transport/parent-unowned-call.d.ts +25 -0
- package/dist/{retired-settings-keys-C1duPikd.mjs → retired-settings-keys-8yY2sTc-.mjs} +1 -1
- package/dist/{retired-settings-keys-CGSuXhP8.js → retired-settings-keys-iY5I0nKM.js} +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { K as buildCapabilityLookup,
|
|
1
|
+
import { $ as coreBlockAddonId, Gn as BaseAddon, In as compositionSourceKey, K as buildCapabilityLookup, Q as compositionSourceRefs, Wn as errMsg, Xt as planComposition, d as CORE_BLOCK_ADDON_PREFIX, et as coreBlockComposition, f as CoreBlockSchema, h as DeclaredDevices, jn as composedDeviceRef, l as COMPOSER_RUNNER_ID, nt as coreBlockSourceKind, rt as coreBlocksCapability, t as ALL_CAPABILITY_DEFINITIONS, tn as replacedFieldKeys, tt as coreBlockIdFromAddonId, u as CORE_BLOCKS_ADDON_ID, yn as validateComposition } from "../../dist-MzYJVCLE.mjs";
|
|
2
|
+
import { t as describeCompositionSources } from "../../composition-sources-yonsRneP.mjs";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { customizationBlockName } from "@camstack/types/node";
|
|
3
5
|
import * as fs from "node:fs/promises";
|
|
4
6
|
import * as path$1 from "node:path";
|
|
5
7
|
//#region src/builtins/core-blocks/block-package.ts
|
|
@@ -336,7 +338,7 @@ var BlockSupervisor = class {
|
|
|
336
338
|
for (const { block } of compositions) await this.report(block.id, "failed", "no composer is available on this node — a composition cannot run here");
|
|
337
339
|
return;
|
|
338
340
|
}
|
|
339
|
-
if (runners.find((r) => r.name ===
|
|
341
|
+
if (runners.find((r) => r.name === COMPOSER_RUNNER_ID)?.state === "failed") {
|
|
340
342
|
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");
|
|
341
343
|
return;
|
|
342
344
|
}
|
|
@@ -1009,15 +1011,103 @@ function armBlocksIntegration(options) {
|
|
|
1009
1011
|
})() };
|
|
1010
1012
|
}
|
|
1011
1013
|
//#endregion
|
|
1014
|
+
//#region src/builtins/core-blocks/composition-peers.ts
|
|
1015
|
+
/**
|
|
1016
|
+
* The stored compositions a save is judged against, as graph peers (D663,
|
|
1017
|
+
* ruling S2). Each is identified by its BLOCK ID, targets the device it writes
|
|
1018
|
+
* — `composedDeviceRef(block.id)` for a new device, the customized device for an
|
|
1019
|
+
* existing one — and, on an existing device, names the fields it REPLACES, so
|
|
1020
|
+
* its self-read of a replaced field is the native value and never an edge.
|
|
1021
|
+
*
|
|
1022
|
+
* `replaced` is not stored anywhere: it is recomputed at save time from the
|
|
1023
|
+
* peer's target description (`planComposition(..., target)`), read in the same
|
|
1024
|
+
* batch as the candidate's. A peer whose device could not be read has unknown
|
|
1025
|
+
* replaced fields: it is OVER-approximated — nothing replaced, every self-read
|
|
1026
|
+
* an edge (edges only added, never a cycle hidden) — and its device is listed
|
|
1027
|
+
* in `unread`, so the validator refuses a cyclic component only when it holds
|
|
1028
|
+
* a candidate node, and names the device (`findCandidateCycles`). An unreadable
|
|
1029
|
+
* device never blocks an unrelated save. A peer whose device is GONE is treated
|
|
1030
|
+
* the same way (M-1): a stored peer validated earlier may still read it, and
|
|
1031
|
+
* assuming every field replaced would remove edges.
|
|
1032
|
+
*
|
|
1033
|
+
* The candidate's own old version is kept in the list: the graph drops it BY ID
|
|
1034
|
+
* (`compositionEdges`), never by target — that is how a second block on
|
|
1035
|
+
* the same device reaches `duplicate-target`.
|
|
1036
|
+
*/
|
|
1037
|
+
function storedCompositions(blocks) {
|
|
1038
|
+
return blocks.flatMap((block) => {
|
|
1039
|
+
const composition = coreBlockComposition(block);
|
|
1040
|
+
return composition === null ? [] : [{
|
|
1041
|
+
blockId: block.id,
|
|
1042
|
+
composition
|
|
1043
|
+
}];
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
/** The existing devices the stored customizations write — each read once with the candidate's. */
|
|
1047
|
+
function storedCustomizationTargets(blocks) {
|
|
1048
|
+
return storedCompositions(blocks).flatMap(({ composition }) => composition.target.kind === "existing" ? [composition.target.device] : []);
|
|
1049
|
+
}
|
|
1050
|
+
/** The fields a stored customization replaces, or why they are unknown. */
|
|
1051
|
+
function replacedOf(stored, description, lookupCap) {
|
|
1052
|
+
if (description === void 0) return { error: "it was not read" };
|
|
1053
|
+
if (description.kind === "unreadable") return { error: description.error };
|
|
1054
|
+
if (description.kind === "absent") return { error: "the device does not exist" };
|
|
1055
|
+
return replacedFieldKeys(planComposition(stored.composition, lookupCap, description));
|
|
1056
|
+
}
|
|
1057
|
+
function peerOf(stored, targets, lookupCap, candidateBlockId) {
|
|
1058
|
+
const { blockId, composition } = stored;
|
|
1059
|
+
if (composition.target.kind === "new") return {
|
|
1060
|
+
peer: {
|
|
1061
|
+
blockId,
|
|
1062
|
+
target: composedDeviceRef(blockId),
|
|
1063
|
+
composition
|
|
1064
|
+
},
|
|
1065
|
+
unread: null
|
|
1066
|
+
};
|
|
1067
|
+
const device = composition.target.device;
|
|
1068
|
+
const key = compositionSourceKey(device);
|
|
1069
|
+
const replaced = replacedOf(stored, targets.get(key), lookupCap);
|
|
1070
|
+
if (!("error" in replaced)) return {
|
|
1071
|
+
peer: {
|
|
1072
|
+
blockId,
|
|
1073
|
+
target: device,
|
|
1074
|
+
composition,
|
|
1075
|
+
replaced
|
|
1076
|
+
},
|
|
1077
|
+
unread: null
|
|
1078
|
+
};
|
|
1079
|
+
return {
|
|
1080
|
+
peer: {
|
|
1081
|
+
blockId,
|
|
1082
|
+
target: device,
|
|
1083
|
+
composition,
|
|
1084
|
+
replaced: /* @__PURE__ */ new Set()
|
|
1085
|
+
},
|
|
1086
|
+
unread: blockId === candidateBlockId ? null : [key, replaced.error]
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function storedPeers(blocks, targets, lookupCap, candidateBlockId) {
|
|
1090
|
+
const read = storedCompositions(blocks).map((stored) => peerOf(stored, targets, lookupCap, candidateBlockId));
|
|
1091
|
+
return {
|
|
1092
|
+
peers: read.map((r) => r.peer),
|
|
1093
|
+
unread: new Map(read.flatMap((r) => r.unread === null ? [] : [r.unread]))
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
//#endregion
|
|
1012
1097
|
//#region src/builtins/core-blocks/composition-api.ts
|
|
1013
1098
|
/**
|
|
1014
1099
|
* The composition half of the `core-blocks` provider (D659). Flat methods,
|
|
1015
1100
|
* spread into the provider: no nested sub-object.
|
|
1016
1101
|
*
|
|
1017
|
-
* Every save is VALIDATED against the live sources
|
|
1018
|
-
*
|
|
1019
|
-
* and stores nothing. The store's
|
|
1020
|
-
* code block) propagate unchanged —
|
|
1102
|
+
* Every save is VALIDATED against the live sources, its existing target (read
|
|
1103
|
+
* once: slim row + `getBindings`) and every stored composition first; a refusal
|
|
1104
|
+
* names each problem by path and reason (D391) and stores nothing. The store's
|
|
1105
|
+
* own refusals (not loaded, unknown block, a code block) propagate unchanged —
|
|
1106
|
+
* this layer never swallows them.
|
|
1107
|
+
*
|
|
1108
|
+
* A customization of an existing device (D663) has no name of its own: the
|
|
1109
|
+
* server derives it (`customizationBlockName`), refuses one the client sends,
|
|
1110
|
+
* and never lets it move to another device. One per device, by block id (S2).
|
|
1021
1111
|
*/
|
|
1022
1112
|
var NOT_LOADED_REASON = "core_blocks has not loaded yet";
|
|
1023
1113
|
function writeQueue() {
|
|
@@ -1044,24 +1134,30 @@ function refuseIfChanged(deps, blockId, expectedUpdatedAt) {
|
|
|
1044
1134
|
} });
|
|
1045
1135
|
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`);
|
|
1046
1136
|
}
|
|
1137
|
+
/** The device a composition writes, when it is an EXISTING one; null for a new device. */
|
|
1138
|
+
function existingTarget(composition) {
|
|
1139
|
+
return composition.target.kind === "existing" ? composition.target.device : null;
|
|
1140
|
+
}
|
|
1047
1141
|
async function validateAgainstStore(deps, composition, blockId) {
|
|
1048
1142
|
if (!deps.store.isLoaded()) throw new Error(`${NOT_LOADED_REASON} — a composition cannot be judged against the stored ones until it has; retry shortly`);
|
|
1049
1143
|
const self = blockId === null ? null : composedDeviceRef(blockId);
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
});
|
|
1057
|
-
const selfKey = self === null ? null : compositionSourceKey(self);
|
|
1058
|
-
const refs = compositionSourceRefs(composition).filter((r) => compositionSourceKey(r) !== selfKey);
|
|
1059
|
-
const sources = await deps.describeSources(refs);
|
|
1144
|
+
const target = existingTarget(composition);
|
|
1145
|
+
const ownKeys = new Set([self, target].flatMap((ref) => ref === null ? [] : [compositionSourceKey(ref)]));
|
|
1146
|
+
const refs = compositionSourceRefs(composition).filter((r) => !ownKeys.has(compositionSourceKey(r)));
|
|
1147
|
+
const blocks = deps.store.list();
|
|
1148
|
+
const read = await deps.describe(refs, [...target === null ? [] : [target], ...storedCustomizationTargets(blocks)]);
|
|
1149
|
+
const stored = storedPeers(blocks, read.targets, deps.lookupCap, blockId);
|
|
1060
1150
|
return validateComposition(composition, {
|
|
1061
1151
|
lookupCap: deps.lookupCap,
|
|
1062
|
-
sources,
|
|
1152
|
+
sources: read.sources,
|
|
1063
1153
|
self,
|
|
1064
|
-
|
|
1154
|
+
blockId,
|
|
1155
|
+
target: target === null ? null : read.targets.get(compositionSourceKey(target)) ?? {
|
|
1156
|
+
kind: "unreadable",
|
|
1157
|
+
error: "it was not read"
|
|
1158
|
+
},
|
|
1159
|
+
peers: stored.peers,
|
|
1160
|
+
unreadTargets: stored.unread
|
|
1065
1161
|
});
|
|
1066
1162
|
}
|
|
1067
1163
|
function describeProblems(validation) {
|
|
@@ -1096,6 +1192,16 @@ async function readStates(deps, blockIds) {
|
|
|
1096
1192
|
}]));
|
|
1097
1193
|
}
|
|
1098
1194
|
}
|
|
1195
|
+
/**
|
|
1196
|
+
* One block's composer state. A block the answer leaves out is UNREACHABLE,
|
|
1197
|
+
* never folded into "no composition" (D315, M-4).
|
|
1198
|
+
*/
|
|
1199
|
+
async function readState(deps, blockId) {
|
|
1200
|
+
return (await readStates(deps, [blockId])).get(blockId) ?? {
|
|
1201
|
+
kind: "unreachable",
|
|
1202
|
+
error: "the composer state read returned no entry for the block"
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1099
1205
|
function toViews(blocks, states) {
|
|
1100
1206
|
return blocks.flatMap((block) => {
|
|
1101
1207
|
const state = states.get(block.id);
|
|
@@ -1105,6 +1211,98 @@ function toViews(blocks, states) {
|
|
|
1105
1211
|
}];
|
|
1106
1212
|
});
|
|
1107
1213
|
}
|
|
1214
|
+
var NO_NAME_OF_ITS_OWN = "a customization has no name of its own — the device's name is shown; omit `name`";
|
|
1215
|
+
var NEW_DEVICE_NEEDS_NAME = "a new device needs a name";
|
|
1216
|
+
var DEVICE_IS_FIXED = "a customization's device cannot be changed — delete it and customize the other device";
|
|
1217
|
+
var NEVER_BECOMES_CUSTOMIZATION = "a composed device cannot become the customization of another device — create the customization instead";
|
|
1218
|
+
/**
|
|
1219
|
+
* The numeric id of a save's existing target, for the refusal line's tag
|
|
1220
|
+
* (D391: a line about a device carries `tags.deviceId`). Read only on a
|
|
1221
|
+
* refusal — one listing — and a failed read names itself on the line instead.
|
|
1222
|
+
*/
|
|
1223
|
+
async function resolveTarget(deps, target) {
|
|
1224
|
+
try {
|
|
1225
|
+
const found = (await deps.describe([], [target])).targets.get(compositionSourceKey(target));
|
|
1226
|
+
if (found?.kind === "found") return {
|
|
1227
|
+
kind: "device",
|
|
1228
|
+
deviceId: found.deviceId
|
|
1229
|
+
};
|
|
1230
|
+
return {
|
|
1231
|
+
kind: "unresolved",
|
|
1232
|
+
why: found?.kind ?? "not described"
|
|
1233
|
+
};
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
return {
|
|
1236
|
+
kind: "unresolved",
|
|
1237
|
+
why: errMsg(err)
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/** A save refused before validation, by name (D391): logged, then thrown. */
|
|
1242
|
+
async function refuseSave(deps, subject, target, reason) {
|
|
1243
|
+
const resolved = target === null ? null : await resolveTarget(deps, target);
|
|
1244
|
+
deps.logger.warn("composition save refused", {
|
|
1245
|
+
...resolved?.kind === "device" ? { tags: { deviceId: resolved.deviceId } } : {},
|
|
1246
|
+
meta: {
|
|
1247
|
+
name: subject.name,
|
|
1248
|
+
blockId: subject.blockId,
|
|
1249
|
+
target: target === null ? null : compositionSourceKey(target),
|
|
1250
|
+
...resolved?.kind === "unresolved" ? { targetUnresolved: resolved.why } : {},
|
|
1251
|
+
reason
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
throw new Error(`core-blocks: the composition "${subject.name}" was refused — ${reason}`);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* The name a new block is stored under (rule 1): a new device's own, required;
|
|
1258
|
+
* a customization's DERIVED from its device (ruling S2), never the client's.
|
|
1259
|
+
*/
|
|
1260
|
+
async function nameOnCreate(deps, block) {
|
|
1261
|
+
const target = existingTarget(block.composition);
|
|
1262
|
+
if (target === null) {
|
|
1263
|
+
if (block.name === void 0) return refuseSave(deps, {
|
|
1264
|
+
name: "(unnamed)",
|
|
1265
|
+
blockId: null
|
|
1266
|
+
}, null, NEW_DEVICE_NEEDS_NAME);
|
|
1267
|
+
return block.name;
|
|
1268
|
+
}
|
|
1269
|
+
if (block.name !== void 0) return refuseSave(deps, {
|
|
1270
|
+
name: block.name,
|
|
1271
|
+
blockId: null
|
|
1272
|
+
}, target, NO_NAME_OF_ITS_OWN);
|
|
1273
|
+
return customizationBlockName(target);
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* A customization is pinned to its device and carries no client name (rule 2);
|
|
1277
|
+
* a composed device never turns into one — its name would be the client's. An
|
|
1278
|
+
* unknown or code block falls through to the store, whose refusal names it.
|
|
1279
|
+
*/
|
|
1280
|
+
async function refuseRetarget(deps, blockId, patch) {
|
|
1281
|
+
const stored = deps.store.get(blockId);
|
|
1282
|
+
const current = stored === null ? null : coreBlockComposition(stored);
|
|
1283
|
+
if (stored === null || current === null) return;
|
|
1284
|
+
const subject = {
|
|
1285
|
+
name: stored.name,
|
|
1286
|
+
blockId
|
|
1287
|
+
};
|
|
1288
|
+
const pinned = existingTarget(current);
|
|
1289
|
+
const next = patch.composition === void 0 ? pinned : existingTarget(patch.composition);
|
|
1290
|
+
if (pinned === null) {
|
|
1291
|
+
if (next !== null) return refuseSave(deps, subject, next, NEVER_BECOMES_CUSTOMIZATION);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (patch.name !== void 0) return refuseSave(deps, subject, pinned, NO_NAME_OF_ITS_OWN);
|
|
1295
|
+
if (next === null || compositionSourceKey(next) !== compositionSourceKey(pinned)) return refuseSave(deps, subject, pinned, DEVICE_IS_FIXED);
|
|
1296
|
+
}
|
|
1297
|
+
/** The block customizing `device`, found by TARGET — never by its derived name (rule 4). */
|
|
1298
|
+
function customizationOf(blocks, device) {
|
|
1299
|
+
const key = compositionSourceKey(device);
|
|
1300
|
+
return blocks.find((block) => {
|
|
1301
|
+
const composition = coreBlockComposition(block);
|
|
1302
|
+
const target = composition === null ? null : existingTarget(composition);
|
|
1303
|
+
return target !== null && compositionSourceKey(target) === key;
|
|
1304
|
+
}) ?? null;
|
|
1305
|
+
}
|
|
1108
1306
|
/**
|
|
1109
1307
|
* Every write runs validate-then-persist on ONE chain. Validation awaits a
|
|
1110
1308
|
* source read before the save lands; two unserialized saves (A reads B, B
|
|
@@ -1115,12 +1313,16 @@ function buildCompositionApi(deps) {
|
|
|
1115
1313
|
const serialized = writeQueue();
|
|
1116
1314
|
return {
|
|
1117
1315
|
createComposition: ({ block, caller }) => serialized(async () => {
|
|
1316
|
+
const name = await nameOnCreate(deps, block);
|
|
1118
1317
|
const validation = await validateAgainstStore(deps, block.composition, null);
|
|
1119
1318
|
if (!validation.ok) refuse(deps, {
|
|
1120
|
-
name
|
|
1319
|
+
name,
|
|
1121
1320
|
blockId: null
|
|
1122
1321
|
}, validation);
|
|
1123
|
-
const created = await deps.store.createComposition(
|
|
1322
|
+
const created = await deps.store.createComposition({
|
|
1323
|
+
...block,
|
|
1324
|
+
name
|
|
1325
|
+
}, caller.userId);
|
|
1124
1326
|
deps.logger.info("composition created", { meta: {
|
|
1125
1327
|
blockId: created.id,
|
|
1126
1328
|
name: created.name,
|
|
@@ -1131,6 +1333,7 @@ function buildCompositionApi(deps) {
|
|
|
1131
1333
|
}),
|
|
1132
1334
|
updateComposition: ({ blockId, expectedUpdatedAt, block, caller }) => serialized(async () => {
|
|
1133
1335
|
refuseIfChanged(deps, blockId, expectedUpdatedAt);
|
|
1336
|
+
await refuseRetarget(deps, blockId, block);
|
|
1134
1337
|
if (block.composition !== void 0) {
|
|
1135
1338
|
const validation = await validateAgainstStore(deps, block.composition, blockId);
|
|
1136
1339
|
if (!validation.ok) refuse(deps, {
|
|
@@ -1156,10 +1359,12 @@ function buildCompositionApi(deps) {
|
|
|
1156
1359
|
state: "loaded",
|
|
1157
1360
|
view: null
|
|
1158
1361
|
};
|
|
1159
|
-
const [view] = toViews([block], await readStates(deps, [blockId]));
|
|
1160
1362
|
return {
|
|
1161
1363
|
state: "loaded",
|
|
1162
|
-
view:
|
|
1364
|
+
view: {
|
|
1365
|
+
block,
|
|
1366
|
+
state: await readState(deps, blockId)
|
|
1367
|
+
}
|
|
1163
1368
|
};
|
|
1164
1369
|
},
|
|
1165
1370
|
listCompositions: async () => {
|
|
@@ -1177,61 +1382,31 @@ function buildCompositionApi(deps) {
|
|
|
1177
1382
|
views: toViews(blocks, await readStates(deps, blocks.map((b) => b.id)))
|
|
1178
1383
|
};
|
|
1179
1384
|
},
|
|
1385
|
+
getCustomization: async ({ addonId, stableId }) => {
|
|
1386
|
+
if (!deps.store.isLoaded()) return {
|
|
1387
|
+
state: "not-loaded",
|
|
1388
|
+
reason: NOT_LOADED_REASON
|
|
1389
|
+
};
|
|
1390
|
+
const block = customizationOf(deps.store.list(), {
|
|
1391
|
+
addonId,
|
|
1392
|
+
stableId
|
|
1393
|
+
});
|
|
1394
|
+
if (block === null) return {
|
|
1395
|
+
state: "loaded",
|
|
1396
|
+
view: null
|
|
1397
|
+
};
|
|
1398
|
+
return {
|
|
1399
|
+
state: "loaded",
|
|
1400
|
+
view: {
|
|
1401
|
+
block,
|
|
1402
|
+
state: await readState(deps, block.id)
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
},
|
|
1180
1406
|
validateComposition: async ({ composition, blockId }) => validateAgainstStore(deps, composition, blockId ?? null)
|
|
1181
1407
|
};
|
|
1182
1408
|
}
|
|
1183
1409
|
//#endregion
|
|
1184
|
-
//#region src/builtins/core-blocks/composition-sources.ts
|
|
1185
|
-
/**
|
|
1186
|
-
* Describe a composition's sources for SAVE-time validation (D659): one
|
|
1187
|
-
* `listAll` per owning addon, one `getBindings` per found device. A read that
|
|
1188
|
-
* fails makes the source `unreadable`, and the save is REFUSED rather than
|
|
1189
|
-
* stored unchecked. Refusing a save destroys nothing (D49).
|
|
1190
|
-
*
|
|
1191
|
-
* Sources are keyed by `{addonId, stableId}` — never a numeric device id,
|
|
1192
|
-
* which is this node's allocation and not the composition's identity.
|
|
1193
|
-
*/
|
|
1194
|
-
async function listOwners(ports, refs) {
|
|
1195
|
-
const addons = [...new Set(refs.map((r) => r.addonId))];
|
|
1196
|
-
const listed = await Promise.all(addons.map(async (addonId) => {
|
|
1197
|
-
try {
|
|
1198
|
-
return [addonId, await ports.listByAddon(addonId)];
|
|
1199
|
-
} catch (err) {
|
|
1200
|
-
return [addonId, { error: errMsg(err) }];
|
|
1201
|
-
}
|
|
1202
|
-
}));
|
|
1203
|
-
return new Map(listed);
|
|
1204
|
-
}
|
|
1205
|
-
async function describeOne(ports, ref, listing) {
|
|
1206
|
-
if (listing === void 0) return {
|
|
1207
|
-
kind: "unreadable",
|
|
1208
|
-
error: `\`${ref.addonId}\` was not listed`
|
|
1209
|
-
};
|
|
1210
|
-
if ("error" in listing) return {
|
|
1211
|
-
kind: "unreadable",
|
|
1212
|
-
error: listing.error
|
|
1213
|
-
};
|
|
1214
|
-
const row = listing.find((r) => r.stableId === ref.stableId);
|
|
1215
|
-
if (row === void 0) return { kind: "absent" };
|
|
1216
|
-
try {
|
|
1217
|
-
return {
|
|
1218
|
-
kind: "found",
|
|
1219
|
-
deviceId: row.id,
|
|
1220
|
-
caps: new Set(await ports.boundCaps(row.id))
|
|
1221
|
-
};
|
|
1222
|
-
} catch (err) {
|
|
1223
|
-
return {
|
|
1224
|
-
kind: "unreadable",
|
|
1225
|
-
error: errMsg(err)
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
async function describeCompositionSources(ports, refs) {
|
|
1230
|
-
const owners = await listOwners(ports, refs);
|
|
1231
|
-
const described = await Promise.all(refs.map(async (ref) => [compositionSourceKey(ref), await describeOne(ports, ref, owners.get(ref.addonId))]));
|
|
1232
|
-
return new Map(described);
|
|
1233
|
-
}
|
|
1234
|
-
//#endregion
|
|
1235
1410
|
//#region src/builtins/core-blocks/core-blocks.addon.ts
|
|
1236
1411
|
/**
|
|
1237
1412
|
* The `core-blocks` provider — CRUD over user-authored TypeScript, with the
|
|
@@ -1503,9 +1678,10 @@ var CoreBlocksAddon = class extends BaseAddon {
|
|
|
1503
1678
|
};
|
|
1504
1679
|
}
|
|
1505
1680
|
/**
|
|
1506
|
-
* The composition methods (D659), flat. Sources
|
|
1507
|
-
* SAVE time — one listing per owning addon, one binding
|
|
1508
|
-
* device — and a read that fails refuses the save rather than
|
|
1681
|
+
* The composition methods (D659), flat. Sources and existing targets are read
|
|
1682
|
+
* over `ctx.api` at SAVE time — one slim listing per owning addon, one binding
|
|
1683
|
+
* read per found device — and a read that fails refuses the save rather than
|
|
1684
|
+
* skipping it.
|
|
1509
1685
|
*/
|
|
1510
1686
|
buildCompositionApi() {
|
|
1511
1687
|
const requireApi = () => {
|
|
@@ -1515,13 +1691,13 @@ var CoreBlocksAddon = class extends BaseAddon {
|
|
|
1515
1691
|
};
|
|
1516
1692
|
return buildCompositionApi({
|
|
1517
1693
|
store: this.requireStore(),
|
|
1518
|
-
|
|
1694
|
+
describe: (sources, targets) => describeCompositionSources({
|
|
1519
1695
|
listByAddon: async (addonId) => requireApi().deviceManager.listAll.query({
|
|
1520
1696
|
addonId,
|
|
1521
1697
|
projection: "slim"
|
|
1522
1698
|
}),
|
|
1523
|
-
|
|
1524
|
-
},
|
|
1699
|
+
bindings: async (deviceId) => (await requireApi().deviceManager.getBindings.query({ deviceId })).entries
|
|
1700
|
+
}, sources, targets),
|
|
1525
1701
|
lookupCap: buildCapabilityLookup(ALL_CAPABILITY_DEFINITIONS),
|
|
1526
1702
|
composerStates: async (blockIds) => (await requireApi().composer.getStates.query({ blockIds: [...blockIds] })).states,
|
|
1527
1703
|
convergeSoon: () => this.convergeSoon(),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ICapabilityRegistry, IScopedLogger } from '@camstack/types';
|
|
2
|
+
import { DeviceManagerSettings } from './device-meta-store.js';
|
|
3
|
+
import { ClaimsIndexView, DeviceStateMirror } from './device-state-mirror.js';
|
|
4
|
+
export interface ClaimedStatusOverlayDeps {
|
|
5
|
+
readonly mirror: DeviceStateMirror;
|
|
6
|
+
/** The claims index as it is NOW — loaded or not; read at every question, never captured. */
|
|
7
|
+
readonly claimsIndex: () => ClaimsIndexView;
|
|
8
|
+
/** The settings door the mirror reads a device's row through. */
|
|
9
|
+
readonly settings: () => DeviceManagerSettings;
|
|
10
|
+
readonly logger: IScopedLogger;
|
|
11
|
+
}
|
|
12
|
+
export declare function installClaimedStatusOverlay(registry: ICapabilityRegistry, deps: ClaimedStatusOverlayDeps): () => void;
|
|
@@ -30,6 +30,13 @@ export declare class DeviceManagerAddon extends BaseAddon {
|
|
|
30
30
|
* `ctx`); `null` before initialise / when `ctx.settings` is absent.
|
|
31
31
|
*/
|
|
32
32
|
private stateMirrorImpl;
|
|
33
|
+
/**
|
|
34
|
+
* Removes the claimed-status overlay this addon installed on the capability
|
|
35
|
+
* registry (D663, S3): `getStatus` of a claimed cap answers the mirror for
|
|
36
|
+
* every caller while it is installed. Run in `onShutdown`; `null` before
|
|
37
|
+
* init or when the kernel handed the addon no registry.
|
|
38
|
+
*/
|
|
39
|
+
private uninstallStatusOverlay;
|
|
33
40
|
/**
|
|
34
41
|
* Cross-process native-provider cache: deviceId (numeric) → capName → { addonId, nodeId }.
|
|
35
42
|
* Kept in sync with `DeviceBindingsChanged` push events emitted by forked
|
|
@@ -64,6 +71,15 @@ export declare class DeviceManagerAddon extends BaseAddon {
|
|
|
64
71
|
private readonly removalFence;
|
|
65
72
|
/** The runtime-state mirror, asserted present (constructed in onInitialize). */
|
|
66
73
|
private get stateMirror();
|
|
74
|
+
/**
|
|
75
|
+
* The per-field claims index (D663): which devices carry a claim, for the
|
|
76
|
+
* seed and for `listClaims`. Constructed at init and loaded before the first
|
|
77
|
+
* provider method can run; a load that fails leaves it `not-loaded`, and
|
|
78
|
+
* every seed reads its row until a later call loads it.
|
|
79
|
+
*/
|
|
80
|
+
private fieldClaimsImpl;
|
|
81
|
+
/** The claims index, asserted present (constructed in onInitialize). */
|
|
82
|
+
private get fieldClaims();
|
|
67
83
|
/** Build the `ProviderHost` the extracted provider-method modules
|
|
68
84
|
* (`device-meta-actions`, `device-queries`) reach — exposes the addon's
|
|
69
85
|
* private state/methods they need without leaking the class internals.
|