@camstack/system 1.2.23 → 1.2.25
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/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/kernel/moleculer/settings-read-guard.d.ts +49 -0
- package/dist/kernel/transport/local-child-registry.d.ts +20 -0
- package/dist/kernel/transport/local-transport.d.ts +11 -1
- package/dist/kernel/transport/socket-channel.d.ts +28 -1
- package/dist/{manifest-python-deps-CqDPWqEE.js → manifest-python-deps-COgal7Jy.js} +110 -24
- package/dist/{manifest-python-deps-57PA4rHm.mjs → manifest-python-deps-julrfvyf.mjs} +110 -24
- package/package.json +1 -1
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
2
|
+
const require_manifest_python_deps = require("./manifest-python-deps-COgal7Jy.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
|
|
4
4
|
let node_fs = require("node:fs");
|
|
5
5
|
node_fs = require_chunk.__toESM(node_fs);
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as resolveAddonClass, I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps } from "./manifest-python-deps-
|
|
1
|
+
import { Ct as resolveAddonClass, I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps } from "./manifest-python-deps-julrfvyf.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as fs from "node:fs";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
3
3
|
const require_dist = require("./dist-CS0pdC8l.js");
|
|
4
4
|
const require_model_download_service = require("./model-download-service-hf0ookyy.js");
|
|
5
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
5
|
+
const require_manifest_python_deps = require("./manifest-python-deps-COgal7Jy.js");
|
|
6
6
|
const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
|
|
7
7
|
const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
|
|
8
8
|
const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
|
|
2
2
|
import { B as logLevelAtMost, Dt as readinessKey, Et as parseJsonUnknown$1, F as isVoidInput, I as kebabToCamel, L as lifecycleJobSchema, M as isArrayOutputSchema, N as isCollectionArrayMethod, Ot as resolveCapMount, P as isObjectInput, Tt as parseJsonObject, V as looseSchema, W as objectInputDeclaresAddonId, _t as asString$1, c as RUNTIME_DEFAULTS, ct as DEVICE_STATUS_METHOD, ft as ReadinessRegistry, gt as asNumber, ht as asJsonObject$1, it as errMsg$1, jt as EventCategory$1, k as extractNestedAddonId, kt as scopeKey, m as addonSettingsCapability, ot as DATAPLANE_SECRET_HEADER$1, pt as ReadinessTimeoutError, q as procedureAuthKey, s as METHOD_ACCESS_MAP, st as DEVICE_SETTINGS_CONTRIBUTION_METHODS, t as ALL_CAPABILITY_DEFINITIONS, vt as createEvent, xt as expandCapMethods, yt as emitDownForOwnedCaps } from "./dist-CIek6F9X.mjs";
|
|
3
3
|
import { a as downloadModel, c as getModelFilePath, d as contentTypeFor, f as createAuthenticatedFileServer, h as resolveFilePath, i as downloadFile, l as isModelDownloaded, m as parseTokenizedUrl, n as collectModelFiles, o as ensureModel, p as parseRangeHeader, r as deleteModelFromDisk, s as fetchJson, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-Cp9f4dk6.mjs";
|
|
4
|
-
import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-
|
|
4
|
+
import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-julrfvyf.mjs";
|
|
5
5
|
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
|
|
6
6
|
import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
|
|
7
7
|
import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-modify-write guard for the addon settings stores.
|
|
3
|
+
*
|
|
4
|
+
* ## The data loss this prevents
|
|
5
|
+
*
|
|
6
|
+
* Every settings mutation is `read current → merge patch → write back`. The
|
|
7
|
+
* read used to answer `{}` for BOTH "this addon has no settings yet" and "the
|
|
8
|
+
* read failed", so a failed read merged the patch onto nothing and the write
|
|
9
|
+
* persisted a blob containing ONLY the patch. Everything else in it was gone.
|
|
10
|
+
*
|
|
11
|
+
* It happened on 2026-07-31. The UDS transport stalled, `settings write failed
|
|
12
|
+
* (addon-settings/root) — transport error ... UDS request timed out after
|
|
13
|
+
* 60000ms` appeared in the log, and the recording configuration for all three
|
|
14
|
+
* recording cameras came back as `{"enabled": false, "bands": []}`. Two and a
|
|
15
|
+
* half hours of footage were never written, and nothing looked broken: an
|
|
16
|
+
* emptied config is indistinguishable from an operator switching recording off.
|
|
17
|
+
*
|
|
18
|
+
* The rule this module enforces: **a read that FAILED is not an empty value.**
|
|
19
|
+
* A miss still merges onto `{}` — that is a real empty. A failure refuses the
|
|
20
|
+
* write and says so, leaving the stored blob untouched so the next successful
|
|
21
|
+
* write starts from the truth.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Outcome of a settings read. `missing` and `failed` are deliberately distinct
|
|
25
|
+
* — collapsing them is the bug.
|
|
26
|
+
*/
|
|
27
|
+
export type SettingsRead = {
|
|
28
|
+
readonly kind: 'value';
|
|
29
|
+
readonly value: Record<string, unknown>;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'missing';
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'failed';
|
|
34
|
+
readonly error: string;
|
|
35
|
+
};
|
|
36
|
+
export interface ReadModifyWriteInput {
|
|
37
|
+
readonly read: () => Promise<SettingsRead>;
|
|
38
|
+
readonly write: (next: Record<string, unknown>) => Promise<void>;
|
|
39
|
+
readonly patch: Record<string, unknown>;
|
|
40
|
+
/** Called instead of writing when the read failed — always log this. */
|
|
41
|
+
readonly onRefused: (error: string) => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Merge `patch` over the current value and write it — unless the read failed,
|
|
45
|
+
* in which case write NOTHING.
|
|
46
|
+
*
|
|
47
|
+
* Returns whether the write was attempted, so a caller can surface the refusal.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readModifyWrite(input: ReadModifyWriteInput): Promise<boolean>;
|
|
@@ -95,6 +95,18 @@ export interface LocalChildRegistryOptions {
|
|
|
95
95
|
* everything). The kernel has no cap registry, so the wiring side injects it.
|
|
96
96
|
*/
|
|
97
97
|
readonly isAggregatedCollectionMethod?: (capName: string, method: string) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Per-cap-method bound for UDS cap calls, in ms — read from the cap
|
|
100
|
+
* definition's declared `timeoutMs`. The SAME resolver the remote plane uses
|
|
101
|
+
* (`CapRouteResolver.capTimeoutMs`), so the two planes agree by construction
|
|
102
|
+
* instead of by coincidence.
|
|
103
|
+
*
|
|
104
|
+
* Without this the channel default (60 s) would apply to everything, and a
|
|
105
|
+
* legitimately long method — `applyServerUpdate` declares 16 min because it
|
|
106
|
+
* awaits a full npm install — would be cut off mid-run. Omitted ⇒ the
|
|
107
|
+
* channel default for every cap call.
|
|
108
|
+
*/
|
|
109
|
+
readonly capTimeoutMs?: (capName: string, method: string) => number | undefined;
|
|
98
110
|
}
|
|
99
111
|
/**
|
|
100
112
|
* Parent-side authority for local addon-runners reachable over a
|
|
@@ -118,6 +130,8 @@ export declare class LocalChildRegistry {
|
|
|
118
130
|
private readonly ownNodeId?;
|
|
119
131
|
/** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
|
|
120
132
|
private readonly isAggregatedCollectionMethod?;
|
|
133
|
+
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
134
|
+
private readonly capTimeoutMs?;
|
|
121
135
|
/** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
|
|
122
136
|
private readonly egressRoutedCaps;
|
|
123
137
|
/** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
|
|
@@ -227,6 +241,12 @@ export declare class LocalChildRegistry {
|
|
|
227
241
|
*/
|
|
228
242
|
isChildKnown(childId: string): boolean;
|
|
229
243
|
childProvides(childId: string, capName: string, deviceId?: number): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Per-request options for a cap call: the cap method's declared `timeoutMs`
|
|
246
|
+
* when one exists, else `undefined` so the channel applies its default.
|
|
247
|
+
* Mirrors `CapRouteResolver.callTimeout` for the remote plane.
|
|
248
|
+
*/
|
|
249
|
+
private capRequestOpts;
|
|
230
250
|
/** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
|
|
231
251
|
callCapOnChild(childId: string, input: CapCallInput): Promise<unknown>;
|
|
232
252
|
/** Forward a cap method call to the owning child over UDS; rejects if none. */
|
|
@@ -19,9 +19,19 @@ export type Frame = {
|
|
|
19
19
|
};
|
|
20
20
|
export type RequestHandler = (body: unknown) => Promise<unknown>;
|
|
21
21
|
export type EventHandler = (body: unknown) => void;
|
|
22
|
+
/** Per-request options a {@link LocalChannel} implementation may honour. */
|
|
23
|
+
export interface LocalRequestOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Bound on this request in ms; `0` or negative disables it. Omitted ⇒ the
|
|
26
|
+
* implementation's default. Cap calls pass the cap method's declared
|
|
27
|
+
* `timeoutMs` so a legitimately long method (`applyServerUpdate`, 16 min) is
|
|
28
|
+
* not cut short.
|
|
29
|
+
*/
|
|
30
|
+
readonly timeoutMs?: number;
|
|
31
|
+
}
|
|
22
32
|
/** A bidirectional request/response + one-way event channel over one socket. */
|
|
23
33
|
export interface LocalChannel {
|
|
24
|
-
request(body: unknown): Promise<unknown>;
|
|
34
|
+
request(body: unknown, opts?: LocalRequestOptions): Promise<unknown>;
|
|
25
35
|
emit(body: unknown): void;
|
|
26
36
|
onRequest(handler: RequestHandler): void;
|
|
27
37
|
onEvent(handler: EventHandler): void;
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { Socket } from 'node:net';
|
|
2
2
|
import { LocalChannel, RequestHandler, EventHandler } from './local-transport.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default bound on one UDS request, when the caller names none.
|
|
5
|
+
*
|
|
6
|
+
* Chosen to match `REMOTE_CALL_TIMEOUT_MS` in `cap-route-resolver.ts`: the SAME
|
|
7
|
+
* cap method already dies at 60 s when it resolves to a remote node, so
|
|
8
|
+
* applying the same bound locally makes the two planes consistent rather than
|
|
9
|
+
* newly restrictive. Callers with a legitimately longer method (the cap
|
|
10
|
+
* layer's declared `timeoutMs`, e.g. `applyServerUpdate` at 16 min) pass it
|
|
11
|
+
* explicitly.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_UDS_REQUEST_TIMEOUT_MS = 60000;
|
|
14
|
+
/** Per-request options for {@link SocketChannel.request}. */
|
|
15
|
+
export interface SocketRequestOptions {
|
|
16
|
+
/** Bound in ms; `0` or negative disables the timer entirely. */
|
|
17
|
+
readonly timeoutMs?: number;
|
|
18
|
+
}
|
|
3
19
|
export declare class SocketChannel implements LocalChannel {
|
|
4
20
|
private readonly socket;
|
|
5
21
|
private nextId;
|
|
@@ -11,7 +27,18 @@ export declare class SocketChannel implements LocalChannel {
|
|
|
11
27
|
private closed;
|
|
12
28
|
private closeFired;
|
|
13
29
|
constructor(socket: Socket);
|
|
14
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Send a request and wait for the peer's response.
|
|
32
|
+
*
|
|
33
|
+
* Bounded. Without a timer this settles on exactly two events — a matching
|
|
34
|
+
* `res` frame, or the socket's `close` (which drains `pending`) — so a peer
|
|
35
|
+
* that receives the request and then neither answers nor closes leaves the
|
|
36
|
+
* caller pending forever. That is a wedged handler, or a half-open socket
|
|
37
|
+
* after a child died without a clean teardown, and it is how camera 615 spent
|
|
38
|
+
* 30+ minutes with every inference permit held by a call that was never
|
|
39
|
+
* coming back.
|
|
40
|
+
*/
|
|
41
|
+
request(body: unknown, opts?: SocketRequestOptions): Promise<unknown>;
|
|
15
42
|
emit(body: unknown): void;
|
|
16
43
|
/** Replace the request handler. Only one handler is active at a time. */
|
|
17
44
|
onRequest(handler: RequestHandler): void;
|
|
@@ -1188,6 +1188,27 @@ function addSettingsActions(addon, actions) {
|
|
|
1188
1188
|
}
|
|
1189
1189
|
}
|
|
1190
1190
|
//#endregion
|
|
1191
|
+
//#region src/kernel/moleculer/settings-read-guard.ts
|
|
1192
|
+
/**
|
|
1193
|
+
* Merge `patch` over the current value and write it — unless the read failed,
|
|
1194
|
+
* in which case write NOTHING.
|
|
1195
|
+
*
|
|
1196
|
+
* Returns whether the write was attempted, so a caller can surface the refusal.
|
|
1197
|
+
*/
|
|
1198
|
+
async function readModifyWrite(input) {
|
|
1199
|
+
const current = await input.read();
|
|
1200
|
+
if (current.kind === "failed") {
|
|
1201
|
+
input.onRefused(current.error);
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
const base = current.kind === "value" ? current.value : {};
|
|
1205
|
+
await input.write({
|
|
1206
|
+
...base,
|
|
1207
|
+
...input.patch
|
|
1208
|
+
});
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
1191
1212
|
//#region src/kernel/moleculer/device-cap-proxy.ts
|
|
1192
1213
|
/**
|
|
1193
1214
|
* Max number of accessory children spawned concurrently during a single
|
|
@@ -3526,8 +3547,6 @@ function localEndpointPath(nodeId) {
|
|
|
3526
3547
|
if (process.platform === "win32") return `\\\\.\\pipe\\camstack-${safe}`;
|
|
3527
3548
|
return (0, node_path.join)((0, node_os.tmpdir)(), `camstack-${safe}.sock`);
|
|
3528
3549
|
}
|
|
3529
|
-
//#endregion
|
|
3530
|
-
//#region src/kernel/transport/socket-channel.ts
|
|
3531
3550
|
var SocketChannel = class {
|
|
3532
3551
|
socket;
|
|
3533
3552
|
nextId = 1;
|
|
@@ -3546,13 +3565,34 @@ var SocketChannel = class {
|
|
|
3546
3565
|
socket.on("close", () => this.handleClose());
|
|
3547
3566
|
socket.on("error", () => {});
|
|
3548
3567
|
}
|
|
3549
|
-
|
|
3568
|
+
/**
|
|
3569
|
+
* Send a request and wait for the peer's response.
|
|
3570
|
+
*
|
|
3571
|
+
* Bounded. Without a timer this settles on exactly two events — a matching
|
|
3572
|
+
* `res` frame, or the socket's `close` (which drains `pending`) — so a peer
|
|
3573
|
+
* that receives the request and then neither answers nor closes leaves the
|
|
3574
|
+
* caller pending forever. That is a wedged handler, or a half-open socket
|
|
3575
|
+
* after a child died without a clean teardown, and it is how camera 615 spent
|
|
3576
|
+
* 30+ minutes with every inference permit held by a call that was never
|
|
3577
|
+
* coming back.
|
|
3578
|
+
*/
|
|
3579
|
+
request(body, opts) {
|
|
3550
3580
|
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("channel closed"));
|
|
3551
3581
|
const id = this.nextId++;
|
|
3582
|
+
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
3552
3583
|
return new Promise((resolve, reject) => {
|
|
3584
|
+
let timer = null;
|
|
3585
|
+
if (timeoutMs > 0) {
|
|
3586
|
+
timer = setTimeout(() => {
|
|
3587
|
+
this.pending.delete(id);
|
|
3588
|
+
reject(/* @__PURE__ */ new Error(`UDS request timed out after ${timeoutMs}ms`));
|
|
3589
|
+
}, timeoutMs);
|
|
3590
|
+
timer.unref?.();
|
|
3591
|
+
}
|
|
3553
3592
|
this.pending.set(id, {
|
|
3554
3593
|
resolve,
|
|
3555
|
-
reject
|
|
3594
|
+
reject,
|
|
3595
|
+
timer
|
|
3556
3596
|
});
|
|
3557
3597
|
this.send({
|
|
3558
3598
|
k: "req",
|
|
@@ -3616,6 +3656,7 @@ var SocketChannel = class {
|
|
|
3616
3656
|
const p = this.pending.get(frame.id);
|
|
3617
3657
|
if (!p) return;
|
|
3618
3658
|
this.pending.delete(frame.id);
|
|
3659
|
+
if (p.timer !== null) clearTimeout(p.timer);
|
|
3619
3660
|
if (frame.ok) p.resolve(frame.body);
|
|
3620
3661
|
else p.reject(new Error(frame.err));
|
|
3621
3662
|
return;
|
|
@@ -3626,7 +3667,10 @@ var SocketChannel = class {
|
|
|
3626
3667
|
if (this.closeFired) return;
|
|
3627
3668
|
this.closeFired = true;
|
|
3628
3669
|
this.closed = true;
|
|
3629
|
-
for (const [, p] of this.pending)
|
|
3670
|
+
for (const [, p] of this.pending) {
|
|
3671
|
+
if (p.timer !== null) clearTimeout(p.timer);
|
|
3672
|
+
p.reject(/* @__PURE__ */ new Error("channel closed"));
|
|
3673
|
+
}
|
|
3630
3674
|
this.pending.clear();
|
|
3631
3675
|
this.closeHandler();
|
|
3632
3676
|
}
|
|
@@ -4305,6 +4349,8 @@ var LocalChildRegistry = class {
|
|
|
4305
4349
|
ownNodeId;
|
|
4306
4350
|
/** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
|
|
4307
4351
|
isAggregatedCollectionMethod;
|
|
4352
|
+
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
4353
|
+
capTimeoutMs;
|
|
4308
4354
|
/** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
|
|
4309
4355
|
egressRoutedCaps = /* @__PURE__ */ new Set();
|
|
4310
4356
|
/** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
|
|
@@ -4334,6 +4380,7 @@ var LocalChildRegistry = class {
|
|
|
4334
4380
|
this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
|
|
4335
4381
|
this.ownNodeId = opts.ownNodeId;
|
|
4336
4382
|
this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
|
|
4383
|
+
this.capTimeoutMs = opts.capTimeoutMs;
|
|
4337
4384
|
} else {
|
|
4338
4385
|
this.server = serverOrOptions;
|
|
4339
4386
|
this.onUnownedCall = onUnownedCallArg;
|
|
@@ -4496,11 +4543,20 @@ var LocalChildRegistry = class {
|
|
|
4496
4543
|
if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
|
|
4497
4544
|
return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
|
|
4498
4545
|
}
|
|
4546
|
+
/**
|
|
4547
|
+
* Per-request options for a cap call: the cap method's declared `timeoutMs`
|
|
4548
|
+
* when one exists, else `undefined` so the channel applies its default.
|
|
4549
|
+
* Mirrors `CapRouteResolver.callTimeout` for the remote plane.
|
|
4550
|
+
*/
|
|
4551
|
+
capRequestOpts(input) {
|
|
4552
|
+
const declared = this.capTimeoutMs?.(input.capName, input.method);
|
|
4553
|
+
return typeof declared === "number" ? { timeoutMs: declared } : void 0;
|
|
4554
|
+
}
|
|
4499
4555
|
/** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
|
|
4500
4556
|
async callCapOnChild(childId, input) {
|
|
4501
4557
|
const entry = this.children.get(childId);
|
|
4502
4558
|
if (!entry) throw new Error(`no local child "${childId}" for cap "${input.capName}"`);
|
|
4503
|
-
return entry.channel.request(this.toCapCall(input));
|
|
4559
|
+
return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
|
|
4504
4560
|
}
|
|
4505
4561
|
/** Forward a cap method call to the owning child over UDS; rejects if none. */
|
|
4506
4562
|
async callCap(input) {
|
|
@@ -4510,7 +4566,7 @@ var LocalChildRegistry = class {
|
|
|
4510
4566
|
const where = input.deviceId === void 0 ? "" : ` for device ${input.deviceId}`;
|
|
4511
4567
|
throw new Error(`no local child provides cap "${input.capName}"${where}`);
|
|
4512
4568
|
}
|
|
4513
|
-
return entry.channel.request(this.toCapCall(input));
|
|
4569
|
+
return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
|
|
4514
4570
|
}
|
|
4515
4571
|
/**
|
|
4516
4572
|
* Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
|
|
@@ -7136,8 +7192,18 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7136
7192
|
bindingCache.clear();
|
|
7137
7193
|
});
|
|
7138
7194
|
const settingsApi = Reflect.get(api, "settingsStore");
|
|
7139
|
-
|
|
7140
|
-
|
|
7195
|
+
/**
|
|
7196
|
+
* Read a settings blob, distinguishing a MISS from a FAILURE.
|
|
7197
|
+
*
|
|
7198
|
+
* Collapsing the two is a data-loss bug, not a style choice: every mutation
|
|
7199
|
+
* is read-modify-write, so a failed read that answers "empty" makes the write
|
|
7200
|
+
* persist the patch ALONE and destroy every field the patch does not mention.
|
|
7201
|
+
* That is what emptied the recording configuration on 2026-07-31 when the UDS
|
|
7202
|
+
* transport stalled — `{"enabled": false, "bands": []}`, two and a half hours
|
|
7203
|
+
* of footage never written, and nothing that looked like a fault.
|
|
7204
|
+
*/
|
|
7205
|
+
const readBlobResult = async (coll, key, namespace) => {
|
|
7206
|
+
if (!settingsApi) return { kind: "missing" };
|
|
7141
7207
|
try {
|
|
7142
7208
|
const v = await settingsApi.get.query(namespace !== void 0 ? {
|
|
7143
7209
|
namespace,
|
|
@@ -7147,11 +7213,40 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7147
7213
|
collection: coll,
|
|
7148
7214
|
key
|
|
7149
7215
|
});
|
|
7150
|
-
return v && typeof v === "object" ?
|
|
7151
|
-
|
|
7152
|
-
|
|
7216
|
+
return v && typeof v === "object" ? {
|
|
7217
|
+
kind: "value",
|
|
7218
|
+
value: v
|
|
7219
|
+
} : { kind: "missing" };
|
|
7220
|
+
} catch (err) {
|
|
7221
|
+
return {
|
|
7222
|
+
kind: "failed",
|
|
7223
|
+
error: (0, _camstack_types_addon.errMsg)(err)
|
|
7224
|
+
};
|
|
7153
7225
|
}
|
|
7154
7226
|
};
|
|
7227
|
+
/**
|
|
7228
|
+
* Soft read for CONSUMERS: a failure reads as empty so a boot path degrades
|
|
7229
|
+
* to defaults instead of throwing. Safe here precisely because the result is
|
|
7230
|
+
* never written back — {@link readBlobResult} is what the write paths use.
|
|
7231
|
+
*/
|
|
7232
|
+
const readBlob = async (coll, key, namespace) => {
|
|
7233
|
+
const r = await readBlobResult(coll, key, namespace);
|
|
7234
|
+
return r.kind === "value" ? r.value : {};
|
|
7235
|
+
};
|
|
7236
|
+
/** Read-modify-write that REFUSES to write when the read failed. */
|
|
7237
|
+
const mergeBlob = async (coll, key, patch, namespace) => {
|
|
7238
|
+
await readModifyWrite({
|
|
7239
|
+
read: () => readBlobResult(coll, key, namespace),
|
|
7240
|
+
write: (next) => writeBlob(coll, key, next, namespace),
|
|
7241
|
+
patch,
|
|
7242
|
+
onRefused: (error) => {
|
|
7243
|
+
scopedLogger.error(`settings merge REFUSED (${coll}/${key}) — read failed, refusing to overwrite with a partial blob`, { meta: {
|
|
7244
|
+
error,
|
|
7245
|
+
patchKeys: Object.keys(patch)
|
|
7246
|
+
} });
|
|
7247
|
+
}
|
|
7248
|
+
});
|
|
7249
|
+
};
|
|
7155
7250
|
const writeBlob = async (coll, key, value, namespace) => {
|
|
7156
7251
|
if (!settingsApi) return;
|
|
7157
7252
|
try {
|
|
@@ -7190,10 +7285,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7190
7285
|
},
|
|
7191
7286
|
async writeAddonStore(patch) {
|
|
7192
7287
|
return serializeRmw("addon-settings", "root", async () => {
|
|
7193
|
-
await
|
|
7194
|
-
...await readBlob("addon-settings", "root", addonId),
|
|
7195
|
-
...patch
|
|
7196
|
-
}, addonId);
|
|
7288
|
+
await mergeBlob("addon-settings", "root", patch, addonId);
|
|
7197
7289
|
});
|
|
7198
7290
|
},
|
|
7199
7291
|
async readDeviceStore(deviceId) {
|
|
@@ -7202,10 +7294,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7202
7294
|
async writeDeviceStore(deviceId, patch) {
|
|
7203
7295
|
const key = String(deviceId);
|
|
7204
7296
|
return serializeRmw("addon-devices", key, async () => {
|
|
7205
|
-
await
|
|
7206
|
-
...await readBlob("addon-devices", key, addonId),
|
|
7207
|
-
...patch
|
|
7208
|
-
}, addonId);
|
|
7297
|
+
await mergeBlob("addon-devices", key, patch, addonId);
|
|
7209
7298
|
});
|
|
7210
7299
|
},
|
|
7211
7300
|
async clearDeviceStore(deviceId) {
|
|
@@ -7225,10 +7314,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7225
7314
|
},
|
|
7226
7315
|
async setSection(section, patch) {
|
|
7227
7316
|
return serializeRmw("sections", section, async () => {
|
|
7228
|
-
await
|
|
7229
|
-
...await readBlob("sections", section),
|
|
7230
|
-
...patch
|
|
7231
|
-
});
|
|
7317
|
+
await mergeBlob("sections", section, patch);
|
|
7232
7318
|
});
|
|
7233
7319
|
}
|
|
7234
7320
|
};
|
|
@@ -1186,6 +1186,27 @@ function addSettingsActions(addon, actions) {
|
|
|
1186
1186
|
}
|
|
1187
1187
|
}
|
|
1188
1188
|
//#endregion
|
|
1189
|
+
//#region src/kernel/moleculer/settings-read-guard.ts
|
|
1190
|
+
/**
|
|
1191
|
+
* Merge `patch` over the current value and write it — unless the read failed,
|
|
1192
|
+
* in which case write NOTHING.
|
|
1193
|
+
*
|
|
1194
|
+
* Returns whether the write was attempted, so a caller can surface the refusal.
|
|
1195
|
+
*/
|
|
1196
|
+
async function readModifyWrite(input) {
|
|
1197
|
+
const current = await input.read();
|
|
1198
|
+
if (current.kind === "failed") {
|
|
1199
|
+
input.onRefused(current.error);
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
const base = current.kind === "value" ? current.value : {};
|
|
1203
|
+
await input.write({
|
|
1204
|
+
...base,
|
|
1205
|
+
...input.patch
|
|
1206
|
+
});
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
//#endregion
|
|
1189
1210
|
//#region src/kernel/moleculer/device-cap-proxy.ts
|
|
1190
1211
|
/**
|
|
1191
1212
|
* Max number of accessory children spawned concurrently during a single
|
|
@@ -3524,8 +3545,6 @@ function localEndpointPath(nodeId) {
|
|
|
3524
3545
|
if (process.platform === "win32") return `\\\\.\\pipe\\camstack-${safe}`;
|
|
3525
3546
|
return join(tmpdir(), `camstack-${safe}.sock`);
|
|
3526
3547
|
}
|
|
3527
|
-
//#endregion
|
|
3528
|
-
//#region src/kernel/transport/socket-channel.ts
|
|
3529
3548
|
var SocketChannel = class {
|
|
3530
3549
|
socket;
|
|
3531
3550
|
nextId = 1;
|
|
@@ -3544,13 +3563,34 @@ var SocketChannel = class {
|
|
|
3544
3563
|
socket.on("close", () => this.handleClose());
|
|
3545
3564
|
socket.on("error", () => {});
|
|
3546
3565
|
}
|
|
3547
|
-
|
|
3566
|
+
/**
|
|
3567
|
+
* Send a request and wait for the peer's response.
|
|
3568
|
+
*
|
|
3569
|
+
* Bounded. Without a timer this settles on exactly two events — a matching
|
|
3570
|
+
* `res` frame, or the socket's `close` (which drains `pending`) — so a peer
|
|
3571
|
+
* that receives the request and then neither answers nor closes leaves the
|
|
3572
|
+
* caller pending forever. That is a wedged handler, or a half-open socket
|
|
3573
|
+
* after a child died without a clean teardown, and it is how camera 615 spent
|
|
3574
|
+
* 30+ minutes with every inference permit held by a call that was never
|
|
3575
|
+
* coming back.
|
|
3576
|
+
*/
|
|
3577
|
+
request(body, opts) {
|
|
3548
3578
|
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("channel closed"));
|
|
3549
3579
|
const id = this.nextId++;
|
|
3580
|
+
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
3550
3581
|
return new Promise((resolve, reject) => {
|
|
3582
|
+
let timer = null;
|
|
3583
|
+
if (timeoutMs > 0) {
|
|
3584
|
+
timer = setTimeout(() => {
|
|
3585
|
+
this.pending.delete(id);
|
|
3586
|
+
reject(/* @__PURE__ */ new Error(`UDS request timed out after ${timeoutMs}ms`));
|
|
3587
|
+
}, timeoutMs);
|
|
3588
|
+
timer.unref?.();
|
|
3589
|
+
}
|
|
3551
3590
|
this.pending.set(id, {
|
|
3552
3591
|
resolve,
|
|
3553
|
-
reject
|
|
3592
|
+
reject,
|
|
3593
|
+
timer
|
|
3554
3594
|
});
|
|
3555
3595
|
this.send({
|
|
3556
3596
|
k: "req",
|
|
@@ -3614,6 +3654,7 @@ var SocketChannel = class {
|
|
|
3614
3654
|
const p = this.pending.get(frame.id);
|
|
3615
3655
|
if (!p) return;
|
|
3616
3656
|
this.pending.delete(frame.id);
|
|
3657
|
+
if (p.timer !== null) clearTimeout(p.timer);
|
|
3617
3658
|
if (frame.ok) p.resolve(frame.body);
|
|
3618
3659
|
else p.reject(new Error(frame.err));
|
|
3619
3660
|
return;
|
|
@@ -3624,7 +3665,10 @@ var SocketChannel = class {
|
|
|
3624
3665
|
if (this.closeFired) return;
|
|
3625
3666
|
this.closeFired = true;
|
|
3626
3667
|
this.closed = true;
|
|
3627
|
-
for (const [, p] of this.pending)
|
|
3668
|
+
for (const [, p] of this.pending) {
|
|
3669
|
+
if (p.timer !== null) clearTimeout(p.timer);
|
|
3670
|
+
p.reject(/* @__PURE__ */ new Error("channel closed"));
|
|
3671
|
+
}
|
|
3628
3672
|
this.pending.clear();
|
|
3629
3673
|
this.closeHandler();
|
|
3630
3674
|
}
|
|
@@ -4303,6 +4347,8 @@ var LocalChildRegistry = class {
|
|
|
4303
4347
|
ownNodeId;
|
|
4304
4348
|
/** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
|
|
4305
4349
|
isAggregatedCollectionMethod;
|
|
4350
|
+
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
4351
|
+
capTimeoutMs;
|
|
4306
4352
|
/** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
|
|
4307
4353
|
egressRoutedCaps = /* @__PURE__ */ new Set();
|
|
4308
4354
|
/** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
|
|
@@ -4332,6 +4378,7 @@ var LocalChildRegistry = class {
|
|
|
4332
4378
|
this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
|
|
4333
4379
|
this.ownNodeId = opts.ownNodeId;
|
|
4334
4380
|
this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
|
|
4381
|
+
this.capTimeoutMs = opts.capTimeoutMs;
|
|
4335
4382
|
} else {
|
|
4336
4383
|
this.server = serverOrOptions;
|
|
4337
4384
|
this.onUnownedCall = onUnownedCallArg;
|
|
@@ -4494,11 +4541,20 @@ var LocalChildRegistry = class {
|
|
|
4494
4541
|
if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
|
|
4495
4542
|
return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
|
|
4496
4543
|
}
|
|
4544
|
+
/**
|
|
4545
|
+
* Per-request options for a cap call: the cap method's declared `timeoutMs`
|
|
4546
|
+
* when one exists, else `undefined` so the channel applies its default.
|
|
4547
|
+
* Mirrors `CapRouteResolver.callTimeout` for the remote plane.
|
|
4548
|
+
*/
|
|
4549
|
+
capRequestOpts(input) {
|
|
4550
|
+
const declared = this.capTimeoutMs?.(input.capName, input.method);
|
|
4551
|
+
return typeof declared === "number" ? { timeoutMs: declared } : void 0;
|
|
4552
|
+
}
|
|
4497
4553
|
/** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
|
|
4498
4554
|
async callCapOnChild(childId, input) {
|
|
4499
4555
|
const entry = this.children.get(childId);
|
|
4500
4556
|
if (!entry) throw new Error(`no local child "${childId}" for cap "${input.capName}"`);
|
|
4501
|
-
return entry.channel.request(this.toCapCall(input));
|
|
4557
|
+
return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
|
|
4502
4558
|
}
|
|
4503
4559
|
/** Forward a cap method call to the owning child over UDS; rejects if none. */
|
|
4504
4560
|
async callCap(input) {
|
|
@@ -4508,7 +4564,7 @@ var LocalChildRegistry = class {
|
|
|
4508
4564
|
const where = input.deviceId === void 0 ? "" : ` for device ${input.deviceId}`;
|
|
4509
4565
|
throw new Error(`no local child provides cap "${input.capName}"${where}`);
|
|
4510
4566
|
}
|
|
4511
|
-
return entry.channel.request(this.toCapCall(input));
|
|
4567
|
+
return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
|
|
4512
4568
|
}
|
|
4513
4569
|
/**
|
|
4514
4570
|
* Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
|
|
@@ -7134,8 +7190,18 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7134
7190
|
bindingCache.clear();
|
|
7135
7191
|
});
|
|
7136
7192
|
const settingsApi = Reflect.get(api, "settingsStore");
|
|
7137
|
-
|
|
7138
|
-
|
|
7193
|
+
/**
|
|
7194
|
+
* Read a settings blob, distinguishing a MISS from a FAILURE.
|
|
7195
|
+
*
|
|
7196
|
+
* Collapsing the two is a data-loss bug, not a style choice: every mutation
|
|
7197
|
+
* is read-modify-write, so a failed read that answers "empty" makes the write
|
|
7198
|
+
* persist the patch ALONE and destroy every field the patch does not mention.
|
|
7199
|
+
* That is what emptied the recording configuration on 2026-07-31 when the UDS
|
|
7200
|
+
* transport stalled — `{"enabled": false, "bands": []}`, two and a half hours
|
|
7201
|
+
* of footage never written, and nothing that looked like a fault.
|
|
7202
|
+
*/
|
|
7203
|
+
const readBlobResult = async (coll, key, namespace) => {
|
|
7204
|
+
if (!settingsApi) return { kind: "missing" };
|
|
7139
7205
|
try {
|
|
7140
7206
|
const v = await settingsApi.get.query(namespace !== void 0 ? {
|
|
7141
7207
|
namespace,
|
|
@@ -7145,11 +7211,40 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7145
7211
|
collection: coll,
|
|
7146
7212
|
key
|
|
7147
7213
|
});
|
|
7148
|
-
return v && typeof v === "object" ?
|
|
7149
|
-
|
|
7150
|
-
|
|
7214
|
+
return v && typeof v === "object" ? {
|
|
7215
|
+
kind: "value",
|
|
7216
|
+
value: v
|
|
7217
|
+
} : { kind: "missing" };
|
|
7218
|
+
} catch (err) {
|
|
7219
|
+
return {
|
|
7220
|
+
kind: "failed",
|
|
7221
|
+
error: errMsg(err)
|
|
7222
|
+
};
|
|
7151
7223
|
}
|
|
7152
7224
|
};
|
|
7225
|
+
/**
|
|
7226
|
+
* Soft read for CONSUMERS: a failure reads as empty so a boot path degrades
|
|
7227
|
+
* to defaults instead of throwing. Safe here precisely because the result is
|
|
7228
|
+
* never written back — {@link readBlobResult} is what the write paths use.
|
|
7229
|
+
*/
|
|
7230
|
+
const readBlob = async (coll, key, namespace) => {
|
|
7231
|
+
const r = await readBlobResult(coll, key, namespace);
|
|
7232
|
+
return r.kind === "value" ? r.value : {};
|
|
7233
|
+
};
|
|
7234
|
+
/** Read-modify-write that REFUSES to write when the read failed. */
|
|
7235
|
+
const mergeBlob = async (coll, key, patch, namespace) => {
|
|
7236
|
+
await readModifyWrite({
|
|
7237
|
+
read: () => readBlobResult(coll, key, namespace),
|
|
7238
|
+
write: (next) => writeBlob(coll, key, next, namespace),
|
|
7239
|
+
patch,
|
|
7240
|
+
onRefused: (error) => {
|
|
7241
|
+
scopedLogger.error(`settings merge REFUSED (${coll}/${key}) — read failed, refusing to overwrite with a partial blob`, { meta: {
|
|
7242
|
+
error,
|
|
7243
|
+
patchKeys: Object.keys(patch)
|
|
7244
|
+
} });
|
|
7245
|
+
}
|
|
7246
|
+
});
|
|
7247
|
+
};
|
|
7153
7248
|
const writeBlob = async (coll, key, value, namespace) => {
|
|
7154
7249
|
if (!settingsApi) return;
|
|
7155
7250
|
try {
|
|
@@ -7188,10 +7283,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7188
7283
|
},
|
|
7189
7284
|
async writeAddonStore(patch) {
|
|
7190
7285
|
return serializeRmw("addon-settings", "root", async () => {
|
|
7191
|
-
await
|
|
7192
|
-
...await readBlob("addon-settings", "root", addonId),
|
|
7193
|
-
...patch
|
|
7194
|
-
}, addonId);
|
|
7286
|
+
await mergeBlob("addon-settings", "root", patch, addonId);
|
|
7195
7287
|
});
|
|
7196
7288
|
},
|
|
7197
7289
|
async readDeviceStore(deviceId) {
|
|
@@ -7200,10 +7292,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7200
7292
|
async writeDeviceStore(deviceId, patch) {
|
|
7201
7293
|
const key = String(deviceId);
|
|
7202
7294
|
return serializeRmw("addon-devices", key, async () => {
|
|
7203
|
-
await
|
|
7204
|
-
...await readBlob("addon-devices", key, addonId),
|
|
7205
|
-
...patch
|
|
7206
|
-
}, addonId);
|
|
7295
|
+
await mergeBlob("addon-devices", key, patch, addonId);
|
|
7207
7296
|
});
|
|
7208
7297
|
},
|
|
7209
7298
|
async clearDeviceStore(deviceId) {
|
|
@@ -7223,10 +7312,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
|
|
|
7223
7312
|
},
|
|
7224
7313
|
async setSection(section, patch) {
|
|
7225
7314
|
return serializeRmw("sections", section, async () => {
|
|
7226
|
-
await
|
|
7227
|
-
...await readBlob("sections", section),
|
|
7228
|
-
...patch
|
|
7229
|
-
});
|
|
7315
|
+
await mergeBlob("sections", section, patch);
|
|
7230
7316
|
});
|
|
7231
7317
|
}
|
|
7232
7318
|
};
|