@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
|
@@ -2,8 +2,12 @@ import { AddonContext, ICapabilityRegistry, IDeviceAdoptionProvider, IDeviceMana
|
|
|
2
2
|
import { AdoptionJobEngine } from './adoption-job-engine.js';
|
|
3
3
|
import { BindingsDeps } from './device-bindings-store.js';
|
|
4
4
|
import { DeviceManagerSettings, DeviceMetaStore } from './device-meta-store.js';
|
|
5
|
+
import { FieldClaimsIndex } from './field-claims-index.js';
|
|
6
|
+
import { OwnedCaps } from './field-ownership.js';
|
|
5
7
|
import { MigrationGuard } from './migrate-guard.js';
|
|
6
8
|
import { RemovalFence } from './removal-fence.js';
|
|
9
|
+
import { SeedMode } from './device-state-mirror.js';
|
|
10
|
+
import { MirrorWriteHold } from './mirror-row-writer.js';
|
|
7
11
|
type IDeviceProviderCap = InferProvider<typeof deviceProviderCapability>;
|
|
8
12
|
/**
|
|
9
13
|
* The addon-instance surface the extracted provider functions need. Exposed by
|
|
@@ -18,10 +22,28 @@ export interface ProviderHost {
|
|
|
18
22
|
addonId: string;
|
|
19
23
|
nodeId: string;
|
|
20
24
|
}>>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Seed the runtime-state mirror for a device (no events). `fill` (the boot
|
|
27
|
+
* path) only fills caps memory lacks; `replace` (the migration reseed) makes
|
|
28
|
+
* the mirror answer exactly this blob — the row now belongs to different
|
|
29
|
+
* hardware, so whatever memory held is the wrong device's state (D663).
|
|
30
|
+
*/
|
|
31
|
+
seedMirror(deviceId: number, blob: Record<string, unknown>, mode?: SeedMode): void;
|
|
32
|
+
/**
|
|
33
|
+
* Drain the mirror's debounced writer on these devices and hold it off their
|
|
34
|
+
* rows until `release` — a migration swapping the rows must not race a write
|
|
35
|
+
* that read the pre-swap hardware's blob (D663).
|
|
36
|
+
*/
|
|
37
|
+
holdMirrorWrites(deviceIds: readonly number[], settings: DeviceManagerSettings): Promise<MirrorWriteHold>;
|
|
38
|
+
/**
|
|
39
|
+
* The claims the mirror holds for a device — accepted ones included, whose
|
|
40
|
+
* write-through may not have landed yet. A migration's second look (D663).
|
|
41
|
+
*/
|
|
42
|
+
mirrorOwnedCaps(deviceId: number): OwnedCaps;
|
|
23
43
|
/** Reset a stale per-session `feature-probe` timestamp before mirror seeding. */
|
|
24
44
|
withResetSessionProbe(blob: Record<string, unknown>): Record<string, unknown>;
|
|
45
|
+
/** The per-field claims index (D663) — `removeDevice` prunes a removed device from it. */
|
|
46
|
+
readonly fieldClaims: FieldClaimsIndex;
|
|
25
47
|
/** Resolve a device's real `online` flag from the mirrored device-status slice. */
|
|
26
48
|
resolveDeviceOnline(deviceId: number, fallbackOnline: boolean): boolean;
|
|
27
49
|
/** Resolve a device's `probed` flag from the mirrored feature-probe slice. */
|
|
@@ -378,6 +378,8 @@ export declare class DeviceRowStore {
|
|
|
378
378
|
* functions on both devices in the same critical section.
|
|
379
379
|
*/
|
|
380
380
|
swapIds(a: number, b: number, tempId: number): Promise<void>;
|
|
381
|
+
/** The read-only half of {@link swapIds}: every refusal, every read, no write. */
|
|
382
|
+
private planSwap;
|
|
381
383
|
/** Rewrite a row under a new id and drop the old key. Not exported: the only
|
|
382
384
|
* legitimate reason to move a row is {@link swapIds}. */
|
|
383
385
|
private moveRow;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ClaimOutcome, IScopedLogger } from '@camstack/types';
|
|
3
|
+
import { MergedSlice, OwnedCap, OwnedCaps } from './field-ownership.js';
|
|
4
|
+
/** Resolves a cap's `runtimeState` schema by name; null when it declares none. */
|
|
5
|
+
export type RuntimeStateSchemaLookup = (capName: string) => z.ZodType | null;
|
|
6
|
+
/** Per (device, cap) state of a claimed cap: what the merge last said, and the
|
|
7
|
+
* last GOOD merged slice — kept readable while a bad overlay holds the cap. */
|
|
8
|
+
export interface ClaimedView {
|
|
9
|
+
readonly merged: MergedSlice;
|
|
10
|
+
readonly visible: Readonly<Record<string, unknown>> | null;
|
|
11
|
+
}
|
|
12
|
+
/** What the views need from their owner: the schema, the log and the public emit. */
|
|
13
|
+
export interface ClaimViewSink {
|
|
14
|
+
readonly logger: IScopedLogger;
|
|
15
|
+
readonly schemaFor: RuntimeStateSchemaLookup;
|
|
16
|
+
emitSlice(deviceId: number, capName: string, slice: Record<string, unknown>): void;
|
|
17
|
+
}
|
|
18
|
+
/** Lazily indexes the codegen'd cap set — the hub already carries the barrel. */
|
|
19
|
+
export declare function defaultSchemaFor(capName: string): z.ZodType | null;
|
|
20
|
+
export declare function sameOwnedCap(a: OwnedCap, b: OwnedCap): boolean;
|
|
21
|
+
type ClaimRefusal = Extract<ClaimOutcome, {
|
|
22
|
+
ok: false;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function refused(code: ClaimRefusal['code'], message: string): ClaimOutcome;
|
|
25
|
+
export declare class ClaimViews {
|
|
26
|
+
private readonly sink;
|
|
27
|
+
/** Claims by device, by cap. Absent for every device without one. */
|
|
28
|
+
private readonly owned;
|
|
29
|
+
/** The merged view of each claimed cap; recomputed on every native or owned write. */
|
|
30
|
+
private readonly views;
|
|
31
|
+
constructor(sink: ClaimViewSink);
|
|
32
|
+
claim(deviceId: number, capName: string): OwnedCap | undefined;
|
|
33
|
+
setClaim(deviceId: number, capName: string, claim: OwnedCap): void;
|
|
34
|
+
/** Removes the claim AND its view. */
|
|
35
|
+
deleteClaim(deviceId: number, capName: string): void;
|
|
36
|
+
/** Fill gaps only: a claim already in memory was written through and is newer than the row. */
|
|
37
|
+
seedClaims(deviceId: number, claims: OwnedCaps): void;
|
|
38
|
+
clearDevice(deviceId: number): void;
|
|
39
|
+
ownedCaps(deviceId: number): OwnedCaps;
|
|
40
|
+
claimsOf(deviceId: number): Iterable<[string, OwnedCap]>;
|
|
41
|
+
/** Devices with at least one view — for the whole-system dump. */
|
|
42
|
+
deviceIds(): Iterable<number>;
|
|
43
|
+
isClaimed(deviceId: number, capName: string): boolean;
|
|
44
|
+
/** `undefined` when the cap is not claimed; `null` when it is claimed and held with nothing to show. */
|
|
45
|
+
visible(deviceId: number, capName: string): Readonly<Record<string, unknown>> | null | undefined;
|
|
46
|
+
/** Every claimed cap of a device that currently has something to show. */
|
|
47
|
+
visibleCaps(deviceId: number): Iterable<[string, Readonly<Record<string, unknown>>]>;
|
|
48
|
+
/** What the merge says for a claimed cap; null when the cap is not claimed. */
|
|
49
|
+
effective(deviceId: number, capName: string): MergedSlice | null;
|
|
50
|
+
view(deviceId: number, capName: string): ClaimedView | undefined;
|
|
51
|
+
/** Put a view back (rollback of a failed write-through); `undefined` removes it. */
|
|
52
|
+
restoreView(deviceId: number, capName: string, view: ClaimedView | undefined): void;
|
|
53
|
+
/**
|
|
54
|
+
* Recompute a claimed cap's merged view and emit it when it is a slice and
|
|
55
|
+
* either changed or `emitUnchanged` (the native moved). A held view emits
|
|
56
|
+
* nothing and logs once per transition, naming the fields; a bad overlay
|
|
57
|
+
* keeps the last good slice readable, a missing value or native does not.
|
|
58
|
+
*/
|
|
59
|
+
recompute(deviceId: number, capName: string, claim: OwnedCap, native: Readonly<Record<string, unknown>> | null, emitUnchanged: boolean): void;
|
|
60
|
+
/**
|
|
61
|
+
* The view a claim has NOW — set, never emitted, never logged. A claim is
|
|
62
|
+
* written through, and readers must see it from the moment it is accepted
|
|
63
|
+
* (S5): without this, `capSlice` answered the NATIVE for an already-claimed
|
|
64
|
+
* field for the whole row round-trip (D663 re-review N-5). The emit is judged
|
|
65
|
+
* later, by `recompute`, against what consumers last saw.
|
|
66
|
+
*/
|
|
67
|
+
previewView(deviceId: number, capName: string, claim: OwnedCap, native: Readonly<Record<string, unknown>> | null): void;
|
|
68
|
+
/** The one rule for what a claimed cap shows: the merge when it is a slice; on
|
|
69
|
+
* a bad overlay the last good slice stays readable; a value or native not yet
|
|
70
|
+
* arrived shows nothing. */
|
|
71
|
+
private derive;
|
|
72
|
+
/** The merged view for a claim that arrived by seed: computed, never emitted. */
|
|
73
|
+
seedView(deviceId: number, capName: string, claim: OwnedCap, native: Readonly<Record<string, unknown>> | null): void;
|
|
74
|
+
private ownedFor;
|
|
75
|
+
private viewsFor;
|
|
76
|
+
}
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ClaimOutcome, ClaimsListing, FieldClaimMode, IScopedLogger } from '@camstack/types';
|
|
2
|
+
import { DeviceManagerSettings } from './device-meta-store.js';
|
|
3
|
+
import { DeviceStateMirror } from './device-state-mirror.js';
|
|
4
|
+
import { FieldClaimsIndex } from './field-claims-index.js';
|
|
5
|
+
export interface ClaimFieldsInput {
|
|
6
|
+
readonly deviceId: number;
|
|
7
|
+
readonly capName: string;
|
|
8
|
+
readonly owner: string;
|
|
9
|
+
readonly mode: FieldClaimMode;
|
|
10
|
+
readonly fields: readonly string[];
|
|
11
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
12
|
+
}
|
|
13
|
+
export interface PatchOwnedFieldsInput {
|
|
14
|
+
readonly deviceId: number;
|
|
15
|
+
readonly capName: string;
|
|
16
|
+
readonly owner: string;
|
|
17
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
export interface ReleaseClaimInput {
|
|
20
|
+
readonly deviceId: number;
|
|
21
|
+
readonly capName: string;
|
|
22
|
+
readonly owner: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ListClaimsInput {
|
|
25
|
+
readonly ownerPrefix?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DeviceStateClaimMethods {
|
|
28
|
+
claimFields(input: ClaimFieldsInput): Promise<ClaimOutcome>;
|
|
29
|
+
patchOwnedFields(input: PatchOwnedFieldsInput): Promise<ClaimOutcome>;
|
|
30
|
+
releaseClaim(input: ReleaseClaimInput): Promise<ClaimOutcome>;
|
|
31
|
+
listClaims(input: ListClaimsInput): Promise<ClaimsListing>;
|
|
32
|
+
}
|
|
33
|
+
export interface DeviceStateClaimDeps {
|
|
34
|
+
readonly mirror: Pick<DeviceStateMirror, 'claim' | 'patchOwned' | 'release'>;
|
|
35
|
+
readonly index: FieldClaimsIndex;
|
|
36
|
+
readonly settings: DeviceManagerSettings;
|
|
37
|
+
readonly logger: IScopedLogger;
|
|
38
|
+
/** Is this a device the ledger knows? `resolvePersistedById(id) !== null`. */
|
|
39
|
+
deviceExists(deviceId: number): Promise<boolean>;
|
|
40
|
+
/** Is a migration involving this device running right now? */
|
|
41
|
+
migrationInFlight(deviceId: number): boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function createDeviceStateClaimMethods(deps: DeviceStateClaimDeps): DeviceStateClaimMethods;
|
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import { AddonContext } from '@camstack/types';
|
|
1
|
+
import { AddonContext, ClaimOutcome } from '@camstack/types';
|
|
2
2
|
import { DeviceManagerSettings } from './device-meta-store.js';
|
|
3
|
+
import { RuntimeStateSchemaLookup } from './device-state-claim-views.js';
|
|
4
|
+
import { MergedSlice, OwnedCap, OwnedCaps } from './field-ownership.js';
|
|
5
|
+
import { MirrorWriteHold } from './mirror-row-writer.js';
|
|
3
6
|
import { RuntimeStatePolicyLookup } from './runtime-state-persist-gate.js';
|
|
7
|
+
/**
|
|
8
|
+
* What the seed consults first: the claims index (Task 6). It is written
|
|
9
|
+
* BEFORE the row, so a loaded index is a superset of the rows' claims and a
|
|
10
|
+
* device it does not name has nothing to merge — no row read (D447).
|
|
11
|
+
*/
|
|
12
|
+
export type ClaimsIndexView = {
|
|
13
|
+
readonly state: 'loaded';
|
|
14
|
+
hasClaims(deviceId: number): boolean;
|
|
15
|
+
} | {
|
|
16
|
+
readonly state: 'not-loaded';
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* How `seedMirror` treats what memory already holds. `fill` (boot, first
|
|
20
|
+
* write) only adds caps memory lacks — memory is never behind the row.
|
|
21
|
+
* `replace` (the migration reseed) drops the device's slices, claims and views
|
|
22
|
+
* first: the row now describes different hardware, so everything memory held
|
|
23
|
+
* under that number is the wrong device's state.
|
|
24
|
+
*/
|
|
25
|
+
export type SeedMode = 'fill' | 'replace';
|
|
4
26
|
export declare class DeviceStateMirror {
|
|
5
27
|
private readonly ctx;
|
|
6
28
|
/**
|
|
@@ -10,46 +32,41 @@ export declare class DeviceStateMirror {
|
|
|
10
32
|
*/
|
|
11
33
|
private readonly policyFor;
|
|
12
34
|
/**
|
|
13
|
-
* Hub-side mirror of every device's cap-keyed runtime state
|
|
14
|
-
* Key: deviceId. Value: per-cap slice map. Empty by default —
|
|
35
|
+
* Hub-side mirror of every device's cap-keyed NATIVE runtime state — what the
|
|
36
|
+
* provider wrote. Key: deviceId. Value: per-cap slice map. Empty by default —
|
|
15
37
|
* slices show up as `setCapSlice` calls trickle in.
|
|
16
38
|
*/
|
|
17
39
|
private readonly stateMirror;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
private readonly
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* compared with its immediate predecessor.
|
|
31
|
-
*/
|
|
32
|
-
private readonly lastPersisted;
|
|
33
|
-
/**
|
|
34
|
-
* Per-device count of writes the effective-change gate skipped since the last
|
|
35
|
-
* real one. Reported on the next write that DOES happen, so the log says how
|
|
36
|
-
* much churn the gate absorbed instead of saying nothing at all — a branch
|
|
37
|
-
* that drops work silently reads as "never happened".
|
|
38
|
-
*/
|
|
39
|
-
private readonly skippedSinceWrite;
|
|
40
|
+
/** The claims and the merged view of every claimed cap (D663). */
|
|
41
|
+
private readonly claims;
|
|
42
|
+
/** Devices whose row has been consulted (or ruled empty by the index). */
|
|
43
|
+
private readonly seeded;
|
|
44
|
+
/** Devices whose row was actually READ into memory — a write-through needs this. */
|
|
45
|
+
private readonly rowRead;
|
|
46
|
+
/** One row read per device, however many writes arrive during it. */
|
|
47
|
+
private readonly seeding;
|
|
48
|
+
/** Devices whose seed refusal is already logged — once per transition. */
|
|
49
|
+
private readonly seedRefusalLogged;
|
|
50
|
+
/** Every write of a device's row, and what is believed to be on it. */
|
|
51
|
+
private readonly writer;
|
|
40
52
|
constructor(ctx: AddonContext,
|
|
41
53
|
/**
|
|
42
54
|
* Per-cap durability + volatile-field policy. Defaults to the codegen'd
|
|
43
55
|
* projection of every `*.cap.ts` (D4: the cap declares, the kernel reads).
|
|
44
56
|
* Injectable so a spec can exercise the gate without the real cap set.
|
|
45
57
|
*/
|
|
46
|
-
policyFor?: RuntimeStatePolicyLookup
|
|
58
|
+
policyFor?: RuntimeStatePolicyLookup,
|
|
59
|
+
/** The cap's `runtimeState` schema a merged slice is parsed with (D663). */
|
|
60
|
+
schemaFor?: RuntimeStateSchemaLookup);
|
|
47
61
|
/**
|
|
48
62
|
* Single-cap mirror update — diff against the current mirror,
|
|
49
63
|
* persist the new slice in-memory, emit `DeviceStateChanged` for
|
|
50
64
|
* this cap. No-op on identical writes (both same shape and same
|
|
51
65
|
* values). Called by `setCapSlice` provider.
|
|
52
66
|
*
|
|
67
|
+
* This is the NATIVE path, unchanged for every writer. Under a claim the
|
|
68
|
+
* native slice is stored as the shadow and what is emitted is the merge.
|
|
69
|
+
*
|
|
53
70
|
* @returns whether the mirror actually changed. The caller uses this to
|
|
54
71
|
* decide whether the (synchronous, disk-blocking) runtime-state write is
|
|
55
72
|
* worth scheduling — see `scheduleRuntimeStateDiskWrite`. Returning void
|
|
@@ -58,41 +75,95 @@ export declare class DeviceStateMirror {
|
|
|
58
75
|
*/
|
|
59
76
|
applySingleCapUpdate(deviceId: number, capName: string, slice: Record<string, unknown>): boolean;
|
|
60
77
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* genuinely every second, and their restored value is worthless.
|
|
67
|
-
*
|
|
68
|
-
* GATE — EFFECTIVE CHANGE (at flush): the persistable blob is compared with
|
|
69
|
-
* what is believed to be on disk, ignoring the clock fields each cap declared
|
|
70
|
-
* volatile. `battery` wrote its blob 526 times in 25 minutes without one
|
|
71
|
-
* percentage moving; this is the gate that turns those into one write.
|
|
72
|
-
*
|
|
73
|
-
* The blob is read from the live mirror at flush time, so the disk picture is
|
|
74
|
-
* always the latest state — no risk of writing a stale snapshot.
|
|
78
|
+
* Claim `cap.fields` of `capName` on `deviceId` for `cap.owner`. Written
|
|
79
|
+
* through: resolves after the row write. A re-claim by the same owner
|
|
80
|
+
* replaces `fields`; values of fields no longer claimed are dropped and the
|
|
81
|
+
* native shows for them (ruling S1b). An empty `fields` is what the composer
|
|
82
|
+
* never sends — it releases — and is refused.
|
|
75
83
|
*/
|
|
76
|
-
|
|
84
|
+
claim(deviceId: number, capName: string, cap: OwnedCap, settings: DeviceManagerSettings): Promise<ClaimOutcome>;
|
|
85
|
+
/** Is this exact claim what the row carries, per `lastPersisted`? */
|
|
86
|
+
private isClaimPersisted;
|
|
77
87
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* can report how much churn was absorbed.
|
|
88
|
+
* Write owned values. Partial patches MERGE into `values` (ruling S5); a key
|
|
89
|
+
* outside `fields` is refused. Debounced like a native slice.
|
|
81
90
|
*/
|
|
82
|
-
|
|
83
|
-
/**
|
|
91
|
+
patchOwned(deviceId: number, capName: string, owner: string, values: Readonly<Record<string, unknown>>, settings: DeviceManagerSettings): Promise<ClaimOutcome>;
|
|
92
|
+
/**
|
|
93
|
+
* Remove the claim. A `replace` hands the cap back: the native slice is
|
|
94
|
+
* emitted exactly as last written. An `add` had no native — the cap leaves
|
|
95
|
+
* the mirror and nothing is emitted (consumer mirrors keep the last value
|
|
96
|
+
* until they re-read; recorded for Task 15). Written through.
|
|
97
|
+
*/
|
|
98
|
+
release(deviceId: number, capName: string, owner: string, settings: DeviceManagerSettings): Promise<ClaimOutcome>;
|
|
99
|
+
/** The claims on a device; empty for every device without one. */
|
|
100
|
+
ownedCaps(deviceId: number): OwnedCaps;
|
|
101
|
+
/** The NATIVE shadow of one cap — for the owner-side mechanism only (D224). */
|
|
102
|
+
nativeSlice(deviceId: number, cap: string): Record<string, unknown> | null;
|
|
103
|
+
/** What the merge says for a claimed cap; null when the cap is not claimed. */
|
|
104
|
+
effectiveIfClaimed(deviceId: number, cap: string): MergedSlice | null;
|
|
105
|
+
isSeeded(deviceId: number): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* The SYNCHRONOUS half of {@link ensureSeeded}, branch (c): a LOADED claims
|
|
108
|
+
* index that does not name the device vouches for it — seeded, no read.
|
|
109
|
+
* Refused (false) while a read is in flight, because that read may be
|
|
110
|
+
* landing a claim the index does not know yet; refused when the index cannot
|
|
111
|
+
* vouch. The overlay's `peek` calls this instead of `ensureSeeded`: a sync
|
|
112
|
+
* path must never hold a promise that can reject (D391 — an unhandled
|
|
113
|
+
* rejection is an untagged line on hub-main).
|
|
114
|
+
*/
|
|
115
|
+
seedIfIndexClears(deviceId: number, index: ClaimsIndexView): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Make sure the device's claims are known before a native write is applied
|
|
118
|
+
* (D49). Index first, single-flight (ruling S6):
|
|
119
|
+
* (a) already seeded → nothing;
|
|
120
|
+
* (b) a seed in flight → await it;
|
|
121
|
+
* (c) a LOADED index that does not name the device → seeded, no read;
|
|
122
|
+
* (d) otherwise read the row once and seed it.
|
|
123
|
+
* A read that throws propagates — the write is refused — and is logged once
|
|
124
|
+
* per device transition (D391).
|
|
125
|
+
*/
|
|
126
|
+
ensureSeeded(deviceId: number, settings: DeviceManagerSettings, index: ClaimsIndexView): Promise<void>;
|
|
127
|
+
/** A write-through replaces the whole row: memory must hold what the row held. */
|
|
128
|
+
private ensureRowRead;
|
|
129
|
+
/** An unreadable row is a NAMED refusal (D49): the answer is unknown — a
|
|
130
|
+
* throw could be read as "gone", `not-claimed` as "never was". Logged by `readRowOnce`. */
|
|
131
|
+
private readRowOrRefuse;
|
|
132
|
+
private readRowOnce;
|
|
133
|
+
private nativeOf;
|
|
134
|
+
/**
|
|
135
|
+
* Write the row now — a claim is atomic with the row — and refresh the
|
|
136
|
+
* baseline. Queued behind any write in flight for the device (one row write
|
|
137
|
+
* at a time, N-4), and the blob is taken when the write RUNS, so it carries
|
|
138
|
+
* whatever memory holds by then. A failed write is rolled back in memory IF
|
|
139
|
+
* what this op put there is still there (the two never disagree); when a
|
|
140
|
+
* newer same-owner state landed in the window it stands, and the debounced
|
|
141
|
+
* writer converges the row to it. Either way the failure is logged hub-side
|
|
142
|
+
* (D391) and rethrown; a repeat of the same claim is then a real write, never
|
|
143
|
+
* `changed: false`, and consumers are handed the truth that stands.
|
|
144
|
+
*/
|
|
145
|
+
private writeThrough;
|
|
146
|
+
/** After a rollback, re-emit what is true now: the restored claim's merge, or the native. */
|
|
147
|
+
private reemitAfterRollback;
|
|
148
|
+
/** Arm the debounced writer for a change to `changedCap` — see `MirrorRowWriter.schedule`. */
|
|
149
|
+
scheduleRuntimeStateDiskWrite(deviceId: number, settings: DeviceManagerSettings, changedCap: string): void;
|
|
150
|
+
/** Hold the writer off these rows for a migration — see `MirrorRowWriter.holdDiskWrites`. */
|
|
151
|
+
holdDiskWrites(deviceIds: readonly number[], settings: DeviceManagerSettings): Promise<MirrorWriteHold>;
|
|
152
|
+
/** Flush every pending debounced disk write (graceful shutdown). */
|
|
153
|
+
flushPendingWrites(settings: DeviceManagerSettings | undefined): Promise<void>;
|
|
154
|
+
/** The device's row as it may reach disk: NATIVE slices plus `$owned`, restricted by durability. */
|
|
84
155
|
private persistableForDevice;
|
|
85
156
|
/**
|
|
86
|
-
* One-shot mirror seed used by `loadRuntimeState` at boot so the
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* `
|
|
157
|
+
* One-shot mirror seed used by `loadRuntimeState` at boot so the hub knows
|
|
158
|
+
* every persisted slice before the first `setCapSlice`. No events emitted —
|
|
159
|
+
* population, not a transition. `fill` (default) fills GAPS and never
|
|
160
|
+
* overwrites memory (ruling S6: a slice in memory is at least as new as the
|
|
161
|
+
* row, a claim in memory was written through); `replace` (the migration
|
|
162
|
+
* reseed) drops the device's memory first — the row now belongs to different
|
|
163
|
+
* hardware. Malformed `$owned` entries are dropped by name and logged once.
|
|
164
|
+
* Callers pass the blob through `withResetSessionProbe` first.
|
|
94
165
|
*/
|
|
95
|
-
seedMirror(deviceId: number, blob: Record<string, unknown
|
|
166
|
+
seedMirror(deviceId: number, blob: Record<string, unknown>, mode?: SeedMode): void;
|
|
96
167
|
/**
|
|
97
168
|
* The hub mirror's `feature-probe.lastProbedAt` is a PER-SESSION liveness
|
|
98
169
|
* signal — it means "this worker process completed a probe THIS session".
|
|
@@ -140,16 +211,29 @@ export declare class DeviceStateMirror {
|
|
|
140
211
|
* `true` — a brief transient window, same as `resolveDeviceOnline`.
|
|
141
212
|
*/
|
|
142
213
|
resolveDeviceProbed(deviceId: number): boolean;
|
|
214
|
+
/** The EFFECTIVE slices of a device: native for an unclaimed cap, the merge
|
|
215
|
+
* for a claimed one (a held cap is not reported). */
|
|
143
216
|
snapshotForDevice(deviceId: number): Record<string, Record<string, unknown>>;
|
|
144
|
-
/** One cap's
|
|
145
|
-
* it — callers read that as "the cap has no state", never as an empty slice
|
|
217
|
+
/** One cap's EFFECTIVE slice, cloned. Null when the device has never written
|
|
218
|
+
* it — callers read that as "the cap has no state", never as an empty slice —
|
|
219
|
+
* and null while a claim holds it. */
|
|
146
220
|
capSlice(deviceId: number, cap: string): Record<string, unknown> | null;
|
|
221
|
+
/** The device's NATIVE slices — what the row stores under the cap names. */
|
|
222
|
+
private nativeSnapshotForDevice;
|
|
147
223
|
/** Whole-system mirror dump. Backs `deviceState.getAllSnapshots` — one
|
|
148
224
|
* round-trip for a warm boot instead of N per-device calls. */
|
|
149
225
|
allSnapshots(): Record<string, Record<string, Record<string, unknown>>>;
|
|
226
|
+
/**
|
|
227
|
+
* The PUBLIC event: exactly `{ deviceId, capName, slice }` for every device,
|
|
228
|
+
* claimed or not. The native shadow never rides here (D224) — see
|
|
229
|
+
* `emitNativeShadow`.
|
|
230
|
+
*/
|
|
150
231
|
private emitStateChanged;
|
|
151
|
-
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
232
|
+
/**
|
|
233
|
+
* The owner-only event (D224, D663): the native slice as the provider wrote
|
|
234
|
+
* it, for the composer's native self-reads. Its category is refused by the
|
|
235
|
+
* public event routers (`isOwnerOnlyEventCategory`), so no UI or share-scope
|
|
236
|
+
* subscriber ever sees a second truth beside the merged slice.
|
|
237
|
+
*/
|
|
238
|
+
private emitNativeShadow;
|
|
155
239
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ClaimsListing, FieldClaim, IScopedLogger } from '@camstack/types';
|
|
2
|
+
import { DeviceManagerSettings } from './device-meta-store.js';
|
|
3
|
+
import { ClaimsIndexView } from './device-state-mirror.js';
|
|
4
|
+
import { WriteLock } from './write-lock.js';
|
|
5
|
+
/** The addon-store key the index lives under. */
|
|
6
|
+
export declare const FIELD_CLAIMS_INDEX_KEY = "field-claims-index";
|
|
7
|
+
/** One claim's identity in the index: a cap on a device has at most one owner. */
|
|
8
|
+
export interface ClaimKey {
|
|
9
|
+
readonly deviceId: number;
|
|
10
|
+
readonly capName: string;
|
|
11
|
+
}
|
|
12
|
+
/** A read-modify-write step over the loaded entries; null means nothing to write. */
|
|
13
|
+
export type IndexTransform = (current: ReadonlyMap<string, FieldClaim>) => ReadonlyMap<string, FieldClaim> | null;
|
|
14
|
+
export interface FieldClaimsIndexDeps {
|
|
15
|
+
readonly logger: IScopedLogger;
|
|
16
|
+
/**
|
|
17
|
+
* The addon's own settings row-set lock, shared with every other writer of
|
|
18
|
+
* it: `writeAddonStore` is a read-modify-write of the WHOLE key range on the
|
|
19
|
+
* store side, so unordered writers destroy each other's keys.
|
|
20
|
+
*/
|
|
21
|
+
readonly withAddonStoreWriteLock: WriteLock;
|
|
22
|
+
}
|
|
23
|
+
export declare class FieldClaimsIndex {
|
|
24
|
+
private readonly deps;
|
|
25
|
+
/** The loaded entries by key, or null until a load has succeeded. */
|
|
26
|
+
private entries;
|
|
27
|
+
/** Devices with at least one entry — what the seed asks, per native write. */
|
|
28
|
+
private claimedDevices;
|
|
29
|
+
/** Why the last load did not succeed. */
|
|
30
|
+
private notLoadedReason;
|
|
31
|
+
/** One load in flight at a time; concurrent callers join it. */
|
|
32
|
+
private loading;
|
|
33
|
+
constructor(deps: FieldClaimsIndexDeps);
|
|
34
|
+
/**
|
|
35
|
+
* Load the blob. Resolves `true` once the index is loaded (now or already);
|
|
36
|
+
* `false` when the store could not answer — the reason is kept for `list`.
|
|
37
|
+
* Never throws: an index that cannot load is a state, not a failure.
|
|
38
|
+
*/
|
|
39
|
+
load(settings: DeviceManagerSettings): Promise<boolean>;
|
|
40
|
+
/** What the seed consults: loaded with a device predicate, or not loaded. */
|
|
41
|
+
view(): ClaimsIndexView;
|
|
42
|
+
/** Every entry, optionally those whose owner starts with `ownerPrefix`; or why there is no answer. */
|
|
43
|
+
list(ownerPrefix?: string): ClaimsListing;
|
|
44
|
+
/** The entry for a key, when the index is loaded and has one. */
|
|
45
|
+
entry(key: ClaimKey): FieldClaim | undefined;
|
|
46
|
+
/** Add or replace one entry — written before the row it announces. Throws when the index cannot be loaded or written. */
|
|
47
|
+
upsert(claim: FieldClaim, settings: DeviceManagerSettings): Promise<void>;
|
|
48
|
+
/** Remove one entry — written after the row no longer carries the claim. No write when absent. */
|
|
49
|
+
remove(key: ClaimKey, settings: DeviceManagerSettings): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Drop every entry of a removed device (condition i). Best effort: an index
|
|
52
|
+
* that cannot be loaded or written keeps a stale entry, which costs the next
|
|
53
|
+
* seed of that number one row read and is removed by the next release that
|
|
54
|
+
* answers `not-claimed` — so it is logged, never thrown, and the removal
|
|
55
|
+
* proceeds.
|
|
56
|
+
*/
|
|
57
|
+
pruneDevice(deviceId: number, settings: DeviceManagerSettings): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Read-modify-write of the whole blob under the addon-store lock, from the
|
|
60
|
+
* LOADED entries: the index is never written from a partial picture. `next`
|
|
61
|
+
* returning null means nothing to write.
|
|
62
|
+
*/
|
|
63
|
+
private mutate;
|
|
64
|
+
private install;
|
|
65
|
+
/** The three-way read (D651): a failed read is a REASON, never an empty index. */
|
|
66
|
+
private readBlob;
|
|
67
|
+
/** Not loaded, by name: the index stays unknown and says why. */
|
|
68
|
+
private refuseMalformed;
|
|
69
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FieldClaimMode } from '@camstack/types';
|
|
3
|
+
import { RuntimeStatePolicyLookup } from './runtime-state-persist-gate.js';
|
|
4
|
+
/** The row key under which claims travel. Not a cap name: `$` is not legal in one. */
|
|
5
|
+
export declare const OWNED_FIELDS_KEY = "$owned";
|
|
6
|
+
/**
|
|
7
|
+
* One claimed cap. `fields` is the claimed set, `values` what has been written
|
|
8
|
+
* so far — a claimed field with no value is HELD (ruling S5), never answered
|
|
9
|
+
* from the native slice.
|
|
10
|
+
*/
|
|
11
|
+
export interface OwnedCap {
|
|
12
|
+
readonly owner: string;
|
|
13
|
+
readonly mode: FieldClaimMode;
|
|
14
|
+
readonly fields: readonly string[];
|
|
15
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
16
|
+
}
|
|
17
|
+
export type OwnedCaps = ReadonlyMap<string, OwnedCap>;
|
|
18
|
+
export interface SplitBlob {
|
|
19
|
+
readonly native: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
20
|
+
readonly owned: OwnedCaps;
|
|
21
|
+
/** `$owned` entries that failed `OwnedCapSchema`, by cap name — logged once per device, never applied. */
|
|
22
|
+
readonly dropped: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export type MergedSlice = {
|
|
25
|
+
readonly kind: 'slice';
|
|
26
|
+
readonly slice: Readonly<Record<string, unknown>>;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'held';
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>>;
|
|
32
|
+
/**
|
|
33
|
+
* Split a runtime-state row into its native slices and its claims. A row with
|
|
34
|
+
* no `$owned` splits to its slices and an empty map — the shape every device
|
|
35
|
+
* without a claim has always had. Non-object entries are dropped, as the
|
|
36
|
+
* mirror seed and `DeviceRuntimeState.fromInitial` have always dropped them.
|
|
37
|
+
*/
|
|
38
|
+
export declare function splitRuntimeBlob(blob: Readonly<Record<string, unknown>>): SplitBlob;
|
|
39
|
+
/**
|
|
40
|
+
* Join native slices and claims into one row. `values` are kept only for caps
|
|
41
|
+
* whose policy is `durability: 'restored'`; a session-only cap keeps its CLAIM
|
|
42
|
+
* (so a restart still knows who owns what) but not its values, and the claimed
|
|
43
|
+
* fields are held after boot until the owner writes again. A device with no
|
|
44
|
+
* claims gets no `$owned` key at all — its row is byte-identical to before.
|
|
45
|
+
*/
|
|
46
|
+
export declare function joinRuntimeBlob(native: SplitBlob['native'], owned: OwnedCaps, policyFor: RuntimeStatePolicyLookup): Record<string, Record<string, unknown>>;
|
|
47
|
+
/**
|
|
48
|
+
* The one merge. Held when a claimed field has no value yet (S5: never
|
|
49
|
+
* answered from the native), when a `replace` has no native slice to overlay
|
|
50
|
+
* (a partial slice is never emitted), or when the merged slice fails the cap's
|
|
51
|
+
* `runtimeState` schema (the native value never leaks through a bad overlay).
|
|
52
|
+
*/
|
|
53
|
+
export declare function mergeOwned(native: Readonly<Record<string, unknown>> | null, owned: OwnedCap, schema: z.ZodType | null): MergedSlice;
|
|
@@ -28,6 +28,13 @@ export interface MigrateDeviceResult {
|
|
|
28
28
|
readonly sourceStillLive: readonly CameraSwitchId[];
|
|
29
29
|
readonly swapped: boolean;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A camera's switch states as read before the disable phase: every switch the
|
|
33
|
+
* camera OFFERS, with its state. A switch it does not offer is absent.
|
|
34
|
+
*/
|
|
35
|
+
export type SwitchStates = ReadonlyMap<CameraSwitchId, boolean>;
|
|
36
|
+
/** Severity of a per-device migration line. */
|
|
37
|
+
export type MigrationLogLevel = 'warn' | 'error';
|
|
31
38
|
export interface MigrateDeviceDeps {
|
|
32
39
|
/** `pipelineOrchestrator.setCameraSwitch`, through `ctx.api`. */
|
|
33
40
|
readonly setSwitch: (deviceId: number, switchId: CameraSwitchId, enabled: boolean) => Promise<void>;
|
|
@@ -46,6 +53,15 @@ export interface MigrateDeviceDeps {
|
|
|
46
53
|
* reported as a success.
|
|
47
54
|
*/
|
|
48
55
|
readonly swapHardwareState: (sourceId: number, targetId: number) => Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Refuse a migration over a device under customization
|
|
58
|
+
* (`migrate-hardware-state.ts::assertNoFieldClaims`, D663): a field claim
|
|
59
|
+
* names the NUMBER while its owner names the STABLE ID the swap moves, so no
|
|
60
|
+
* order of operations keeps both right. THROWS to refuse. Asked FIRST —
|
|
61
|
+
* before any switch is flipped, so a refused migration leaves both cameras
|
|
62
|
+
* exactly as they were.
|
|
63
|
+
*/
|
|
64
|
+
readonly assertNoFieldClaims: (sourceId: number, targetId: number) => Promise<void>;
|
|
49
65
|
/**
|
|
50
66
|
* Tell the stream broker the hardware behind this number is gone
|
|
51
67
|
* (`streamBroker.forgetDeviceHardware`): every DERIVED stream definition is
|
|
@@ -67,6 +83,17 @@ export interface MigrateDeviceDeps {
|
|
|
67
83
|
*/
|
|
68
84
|
readonly forgetStreamState: (deviceId: number) => Promise<void>;
|
|
69
85
|
readonly log: (message: string, meta: Record<string, unknown>) => void;
|
|
86
|
+
/**
|
|
87
|
+
* The state of every switch the camera offers, read BEFORE the disable
|
|
88
|
+
* phase, so a migration refused before any row moved can put each switch
|
|
89
|
+
* back exactly as it was (D663 5b ruling I-1) — never blanket-on, which
|
|
90
|
+
* would undo a function the operator had switched off (D62). Bounded like a
|
|
91
|
+
* switch write; a read that fails is reported and the rollback falls back to
|
|
92
|
+
* switching that camera on.
|
|
93
|
+
*/
|
|
94
|
+
readonly readSwitches: (deviceId: number) => Promise<SwitchStates>;
|
|
95
|
+
/** One line about ONE device, tagged with its id (D391). */
|
|
96
|
+
readonly logDevice: (level: MigrationLogLevel, deviceId: number, message: string, meta: Record<string, unknown>) => void;
|
|
70
97
|
/**
|
|
71
98
|
* Override for {@link SWITCH_WRITE_TIMEOUT_MS}, for the specs. A write that
|
|
72
99
|
* outlives the bound is reported `unreachable` — never `off`/`on` — and may
|
|
@@ -26,6 +26,13 @@ export interface MigrationGuard {
|
|
|
26
26
|
begin(sourceId: number, targetId: number): void;
|
|
27
27
|
/** Release both ids. Always paired with {@link begin} via try/finally. */
|
|
28
28
|
end(sourceId: number, targetId: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Is a migration involving this device running right now? A per-field claim,
|
|
31
|
+
* patch or release asks before touching the device's row (D663): the row is
|
|
32
|
+
* about to be swapped with another device's, and a claim written into that
|
|
33
|
+
* window would travel with the wrong hardware.
|
|
34
|
+
*/
|
|
35
|
+
isInFlight(deviceId: number): boolean;
|
|
29
36
|
/** Record a completed swap's fingerprint for replay matching. */
|
|
30
37
|
recordCompleted(record: CompletedMigration): void;
|
|
31
38
|
/**
|