@camstack/system 1.2.171 → 1.2.173
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.
|
@@ -5151,6 +5151,86 @@ var DeviceRowStore = class {
|
|
|
5151
5151
|
});
|
|
5152
5152
|
}
|
|
5153
5153
|
/** Drop the device's row. Idempotent. */
|
|
5154
|
+
/**
|
|
5155
|
+
* Exchange the ids of two devices, carrying their children with them.
|
|
5156
|
+
*
|
|
5157
|
+
* ## Why this exists as a primitive
|
|
5158
|
+
*
|
|
5159
|
+
* Replacing a camera means the new hardware has to become the device the rest
|
|
5160
|
+
* of the system already knows. Everything general about a camera is keyed on
|
|
5161
|
+
* the NUMERIC id and on nothing else — the recording path on disk is
|
|
5162
|
+
* `<deviceId>/profile/…`, the hour ledger, the media store and zone ownership
|
|
5163
|
+
* carry `deviceId` and not one `stableId` between them, and all four
|
|
5164
|
+
* ecosystem exports (Home Assistant, Alexa, HomeKit, Google) key on it too.
|
|
5165
|
+
* Moving that work is the expensive, lossy operation. Moving the NUMBER is
|
|
5166
|
+
* two rows.
|
|
5167
|
+
*
|
|
5168
|
+
* ## Children are re-pointed, not renumbered
|
|
5169
|
+
*
|
|
5170
|
+
* A child keeps its own id and follows its parent: every row whose
|
|
5171
|
+
* `parentDeviceId` was `a` now names `b`, and the reverse. Renumbering the
|
|
5172
|
+
* children as well would move THEIR general work for no reason — the
|
|
5173
|
+
* accessory's own recordings and rules are keyed on the child id, which is
|
|
5174
|
+
* not the id being inherited.
|
|
5175
|
+
*
|
|
5176
|
+
* ## There are no transactions here, so the ORDER is the safety
|
|
5177
|
+
*
|
|
5178
|
+
* The backing store is a collection API (`set` / `delete` / `updateWhere`);
|
|
5179
|
+
* there is no rollback to lean on. So the sequence is chosen to make every
|
|
5180
|
+
* crash point LOUD rather than plausible: `a` first moves to a freshly
|
|
5181
|
+
* allocated id, which the monotonic counter guarantees was never used and
|
|
5182
|
+
* will never be reused. A crash before the end therefore leaves a device
|
|
5183
|
+
* missing from its number and a stray at an unfamiliar one — visible in the
|
|
5184
|
+
* device list at a glance. What it can never leave is two rows on one number,
|
|
5185
|
+
* or a row that looks correct while its children point elsewhere.
|
|
5186
|
+
*
|
|
5187
|
+
* The caller holds {@link DeviceMetaStore.withMetaWriteLock}. This method does
|
|
5188
|
+
* not take it: taking it here would deadlock the migration that has to disable
|
|
5189
|
+
* functions on both devices in the same critical section.
|
|
5190
|
+
*/
|
|
5191
|
+
async swapIds(a, b, tempId) {
|
|
5192
|
+
if (a === b) throw new Error(`[device-manager] swapIds: ${a} and ${b} are the same device`);
|
|
5193
|
+
await this.declare();
|
|
5194
|
+
const rowA = await this.get(a);
|
|
5195
|
+
const rowB = await this.get(b);
|
|
5196
|
+
if (rowA === null) throw new Error(`[device-manager] swapIds: device ${a} does not exist`);
|
|
5197
|
+
if (rowB === null) throw new Error(`[device-manager] swapIds: device ${b} does not exist`);
|
|
5198
|
+
if (await this.get(tempId) !== null) throw new Error(`[device-manager] swapIds: temp id ${tempId} is taken`);
|
|
5199
|
+
const childrenOfA = (await this.listByParent(a)).map((r) => r.meta.id);
|
|
5200
|
+
const childrenOfB = (await this.listByParent(b)).map((r) => r.meta.id);
|
|
5201
|
+
await this.moveRow(rowA, tempId);
|
|
5202
|
+
await this.moveRow(rowB, a);
|
|
5203
|
+
await this.moveRow({
|
|
5204
|
+
...rowA,
|
|
5205
|
+
meta: {
|
|
5206
|
+
...rowA.meta,
|
|
5207
|
+
id: tempId
|
|
5208
|
+
}
|
|
5209
|
+
}, b);
|
|
5210
|
+
for (const childId of childrenOfA) await this.patch(childId, { parentDeviceId: b });
|
|
5211
|
+
for (const childId of childrenOfB) await this.patch(childId, { parentDeviceId: a });
|
|
5212
|
+
this.logger.info("device ids swapped", {
|
|
5213
|
+
tags: { deviceId: a },
|
|
5214
|
+
meta: {
|
|
5215
|
+
a,
|
|
5216
|
+
b,
|
|
5217
|
+
childrenOfA,
|
|
5218
|
+
childrenOfB
|
|
5219
|
+
}
|
|
5220
|
+
});
|
|
5221
|
+
}
|
|
5222
|
+
/** Rewrite a row under a new id and drop the old key. Not exported: the only
|
|
5223
|
+
* legitimate reason to move a row is {@link swapIds}. */
|
|
5224
|
+
async moveRow(row, toId) {
|
|
5225
|
+
await this.upsert({
|
|
5226
|
+
...row.meta,
|
|
5227
|
+
id: toId
|
|
5228
|
+
}, {
|
|
5229
|
+
registered: row.registered,
|
|
5230
|
+
metadata: row.metadata
|
|
5231
|
+
});
|
|
5232
|
+
await this.remove(row.meta.id);
|
|
5233
|
+
}
|
|
5154
5234
|
async remove(deviceId) {
|
|
5155
5235
|
await this.declare();
|
|
5156
5236
|
await this.backend.delete({
|
|
@@ -5146,6 +5146,86 @@ var DeviceRowStore = class {
|
|
|
5146
5146
|
});
|
|
5147
5147
|
}
|
|
5148
5148
|
/** Drop the device's row. Idempotent. */
|
|
5149
|
+
/**
|
|
5150
|
+
* Exchange the ids of two devices, carrying their children with them.
|
|
5151
|
+
*
|
|
5152
|
+
* ## Why this exists as a primitive
|
|
5153
|
+
*
|
|
5154
|
+
* Replacing a camera means the new hardware has to become the device the rest
|
|
5155
|
+
* of the system already knows. Everything general about a camera is keyed on
|
|
5156
|
+
* the NUMERIC id and on nothing else — the recording path on disk is
|
|
5157
|
+
* `<deviceId>/profile/…`, the hour ledger, the media store and zone ownership
|
|
5158
|
+
* carry `deviceId` and not one `stableId` between them, and all four
|
|
5159
|
+
* ecosystem exports (Home Assistant, Alexa, HomeKit, Google) key on it too.
|
|
5160
|
+
* Moving that work is the expensive, lossy operation. Moving the NUMBER is
|
|
5161
|
+
* two rows.
|
|
5162
|
+
*
|
|
5163
|
+
* ## Children are re-pointed, not renumbered
|
|
5164
|
+
*
|
|
5165
|
+
* A child keeps its own id and follows its parent: every row whose
|
|
5166
|
+
* `parentDeviceId` was `a` now names `b`, and the reverse. Renumbering the
|
|
5167
|
+
* children as well would move THEIR general work for no reason — the
|
|
5168
|
+
* accessory's own recordings and rules are keyed on the child id, which is
|
|
5169
|
+
* not the id being inherited.
|
|
5170
|
+
*
|
|
5171
|
+
* ## There are no transactions here, so the ORDER is the safety
|
|
5172
|
+
*
|
|
5173
|
+
* The backing store is a collection API (`set` / `delete` / `updateWhere`);
|
|
5174
|
+
* there is no rollback to lean on. So the sequence is chosen to make every
|
|
5175
|
+
* crash point LOUD rather than plausible: `a` first moves to a freshly
|
|
5176
|
+
* allocated id, which the monotonic counter guarantees was never used and
|
|
5177
|
+
* will never be reused. A crash before the end therefore leaves a device
|
|
5178
|
+
* missing from its number and a stray at an unfamiliar one — visible in the
|
|
5179
|
+
* device list at a glance. What it can never leave is two rows on one number,
|
|
5180
|
+
* or a row that looks correct while its children point elsewhere.
|
|
5181
|
+
*
|
|
5182
|
+
* The caller holds {@link DeviceMetaStore.withMetaWriteLock}. This method does
|
|
5183
|
+
* not take it: taking it here would deadlock the migration that has to disable
|
|
5184
|
+
* functions on both devices in the same critical section.
|
|
5185
|
+
*/
|
|
5186
|
+
async swapIds(a, b, tempId) {
|
|
5187
|
+
if (a === b) throw new Error(`[device-manager] swapIds: ${a} and ${b} are the same device`);
|
|
5188
|
+
await this.declare();
|
|
5189
|
+
const rowA = await this.get(a);
|
|
5190
|
+
const rowB = await this.get(b);
|
|
5191
|
+
if (rowA === null) throw new Error(`[device-manager] swapIds: device ${a} does not exist`);
|
|
5192
|
+
if (rowB === null) throw new Error(`[device-manager] swapIds: device ${b} does not exist`);
|
|
5193
|
+
if (await this.get(tempId) !== null) throw new Error(`[device-manager] swapIds: temp id ${tempId} is taken`);
|
|
5194
|
+
const childrenOfA = (await this.listByParent(a)).map((r) => r.meta.id);
|
|
5195
|
+
const childrenOfB = (await this.listByParent(b)).map((r) => r.meta.id);
|
|
5196
|
+
await this.moveRow(rowA, tempId);
|
|
5197
|
+
await this.moveRow(rowB, a);
|
|
5198
|
+
await this.moveRow({
|
|
5199
|
+
...rowA,
|
|
5200
|
+
meta: {
|
|
5201
|
+
...rowA.meta,
|
|
5202
|
+
id: tempId
|
|
5203
|
+
}
|
|
5204
|
+
}, b);
|
|
5205
|
+
for (const childId of childrenOfA) await this.patch(childId, { parentDeviceId: b });
|
|
5206
|
+
for (const childId of childrenOfB) await this.patch(childId, { parentDeviceId: a });
|
|
5207
|
+
this.logger.info("device ids swapped", {
|
|
5208
|
+
tags: { deviceId: a },
|
|
5209
|
+
meta: {
|
|
5210
|
+
a,
|
|
5211
|
+
b,
|
|
5212
|
+
childrenOfA,
|
|
5213
|
+
childrenOfB
|
|
5214
|
+
}
|
|
5215
|
+
});
|
|
5216
|
+
}
|
|
5217
|
+
/** Rewrite a row under a new id and drop the old key. Not exported: the only
|
|
5218
|
+
* legitimate reason to move a row is {@link swapIds}. */
|
|
5219
|
+
async moveRow(row, toId) {
|
|
5220
|
+
await this.upsert({
|
|
5221
|
+
...row.meta,
|
|
5222
|
+
id: toId
|
|
5223
|
+
}, {
|
|
5224
|
+
registered: row.registered,
|
|
5225
|
+
metadata: row.metadata
|
|
5226
|
+
});
|
|
5227
|
+
await this.remove(row.meta.id);
|
|
5228
|
+
}
|
|
5149
5229
|
async remove(deviceId) {
|
|
5150
5230
|
await this.declare();
|
|
5151
5231
|
await this.backend.delete({
|
|
@@ -340,6 +340,47 @@ export declare class DeviceRowStore {
|
|
|
340
340
|
*/
|
|
341
341
|
upsertRegistration(row: DeviceRegistrationRow): Promise<void>;
|
|
342
342
|
/** Drop the device's row. Idempotent. */
|
|
343
|
+
/**
|
|
344
|
+
* Exchange the ids of two devices, carrying their children with them.
|
|
345
|
+
*
|
|
346
|
+
* ## Why this exists as a primitive
|
|
347
|
+
*
|
|
348
|
+
* Replacing a camera means the new hardware has to become the device the rest
|
|
349
|
+
* of the system already knows. Everything general about a camera is keyed on
|
|
350
|
+
* the NUMERIC id and on nothing else — the recording path on disk is
|
|
351
|
+
* `<deviceId>/profile/…`, the hour ledger, the media store and zone ownership
|
|
352
|
+
* carry `deviceId` and not one `stableId` between them, and all four
|
|
353
|
+
* ecosystem exports (Home Assistant, Alexa, HomeKit, Google) key on it too.
|
|
354
|
+
* Moving that work is the expensive, lossy operation. Moving the NUMBER is
|
|
355
|
+
* two rows.
|
|
356
|
+
*
|
|
357
|
+
* ## Children are re-pointed, not renumbered
|
|
358
|
+
*
|
|
359
|
+
* A child keeps its own id and follows its parent: every row whose
|
|
360
|
+
* `parentDeviceId` was `a` now names `b`, and the reverse. Renumbering the
|
|
361
|
+
* children as well would move THEIR general work for no reason — the
|
|
362
|
+
* accessory's own recordings and rules are keyed on the child id, which is
|
|
363
|
+
* not the id being inherited.
|
|
364
|
+
*
|
|
365
|
+
* ## There are no transactions here, so the ORDER is the safety
|
|
366
|
+
*
|
|
367
|
+
* The backing store is a collection API (`set` / `delete` / `updateWhere`);
|
|
368
|
+
* there is no rollback to lean on. So the sequence is chosen to make every
|
|
369
|
+
* crash point LOUD rather than plausible: `a` first moves to a freshly
|
|
370
|
+
* allocated id, which the monotonic counter guarantees was never used and
|
|
371
|
+
* will never be reused. A crash before the end therefore leaves a device
|
|
372
|
+
* missing from its number and a stray at an unfamiliar one — visible in the
|
|
373
|
+
* device list at a glance. What it can never leave is two rows on one number,
|
|
374
|
+
* or a row that looks correct while its children point elsewhere.
|
|
375
|
+
*
|
|
376
|
+
* The caller holds {@link DeviceMetaStore.withMetaWriteLock}. This method does
|
|
377
|
+
* not take it: taking it here would deadlock the migration that has to disable
|
|
378
|
+
* functions on both devices in the same critical section.
|
|
379
|
+
*/
|
|
380
|
+
swapIds(a: number, b: number, tempId: number): Promise<void>;
|
|
381
|
+
/** Rewrite a row under a new id and drop the old key. Not exported: the only
|
|
382
|
+
* legitimate reason to move a row is {@link swapIds}. */
|
|
383
|
+
private moveRow;
|
|
343
384
|
remove(deviceId: number): Promise<void>;
|
|
344
385
|
/**
|
|
345
386
|
* The retirement door for the three blobs this collection replaced.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CameraSwitchId } from '@camstack/types';
|
|
2
|
+
/** What actually happened to one switch on one device. */
|
|
3
|
+
export type SwitchOutcome = 'off' | 'on' | 'not-offered' | 'unreachable';
|
|
4
|
+
export interface SwitchReport {
|
|
5
|
+
readonly deviceId: number;
|
|
6
|
+
readonly switchId: CameraSwitchId;
|
|
7
|
+
readonly outcome: SwitchOutcome;
|
|
8
|
+
/** The refusal, verbatim, when the outcome is not the requested one. */
|
|
9
|
+
readonly detail?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface MigrateDeviceResult {
|
|
12
|
+
readonly sourceId: number;
|
|
13
|
+
readonly targetId: number;
|
|
14
|
+
/** Every switch write attempted, in order, on both devices. */
|
|
15
|
+
readonly switches: readonly SwitchReport[];
|
|
16
|
+
/**
|
|
17
|
+
* Switches that could NOT be turned off on the source. Empty is the only
|
|
18
|
+
* value that means "the old camera is fully quiet"; a caller that renders
|
|
19
|
+
* only a success flag is not entitled to one.
|
|
20
|
+
*/
|
|
21
|
+
readonly sourceStillLive: readonly CameraSwitchId[];
|
|
22
|
+
readonly swapped: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface MigrateDeviceDeps {
|
|
25
|
+
/** `pipelineOrchestrator.setCameraSwitch`, through `ctx.api`. */
|
|
26
|
+
readonly setSwitch: (deviceId: number, switchId: CameraSwitchId, enabled: boolean) => Promise<void>;
|
|
27
|
+
/** `DeviceRowStore.swapIds`, already under the meta write lock. */
|
|
28
|
+
readonly swapIds: (a: number, b: number) => Promise<void>;
|
|
29
|
+
/** Respawn the runners that hold either number in memory. Disabling stops
|
|
30
|
+
* data PRODUCTION; it does not flush a cache that maps id → addon. */
|
|
31
|
+
readonly respawnRunners: (deviceIds: readonly number[]) => Promise<void>;
|
|
32
|
+
readonly log: (message: string, meta: Record<string, unknown>) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Put both cameras to sleep, exchange their identities, wake the target.
|
|
36
|
+
*
|
|
37
|
+
* Throws only if the swap itself fails — a switch that could not be written is
|
|
38
|
+
* reported, not raised, because refusing to migrate a camera whose old hardware
|
|
39
|
+
* is unreachable would refuse the exact case this exists for.
|
|
40
|
+
*/
|
|
41
|
+
export declare function migrateDevice(deps: MigrateDeviceDeps, input: {
|
|
42
|
+
readonly sourceId: number;
|
|
43
|
+
readonly targetId: number;
|
|
44
|
+
}): Promise<MigrateDeviceResult>;
|