@camstack/system 1.2.143 → 1.2.145
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/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +157 -35
- package/dist/builtins/device-manager/device-manager.addon.mjs +157 -35
- package/dist/builtins/device-manager/device-queries.d.ts +22 -0
- package/dist/builtins/device-manager/device-row-store.d.ts +18 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- 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-backend.d.ts +14 -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 +2 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -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/{dist-CQs1b7uh.js → dist-BVU5JADq.js} +488 -29
- package/dist/{dist-CVuVKA-O.mjs → dist-CG7kwIZp.mjs} +488 -29
- package/dist/index.js +28 -9
- package/dist/index.mjs +28 -9
- package/dist/kernel/index.d.ts +1 -1
- package/dist/kernel/moleculer/children-batch-loader.d.ts +46 -0
- package/dist/kernel/moleculer/core-cap-service.d.ts +17 -2
- package/dist/kernel/moleculer/process-service.d.ts +15 -6
- package/dist/{manifest-python-deps-BuhBXCg0.mjs → manifest-python-deps-BRyNCnnG.mjs} +191 -1
- package/dist/{manifest-python-deps-B_mCU6gz.js → manifest-python-deps-GjlyPjm0.js} +191 -1
- package/dist/{retired-settings-keys-DnMYSIEr.mjs → retired-settings-keys-Dm1z4mO4.mjs} +1 -1
- package/dist/{retired-settings-keys-Dvei9nhG.js → retired-settings-keys-_PLI9w0k.js} +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
+
require("./dist-BVU5JADq.js");
|
|
2
3
|
let node_crypto = require("node:crypto");
|
|
3
4
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
4
5
|
let _camstack_types_node = require("@camstack/types/node");
|
|
@@ -1653,6 +1654,182 @@ async function readModifyWrite(input) {
|
|
|
1653
1654
|
return true;
|
|
1654
1655
|
}
|
|
1655
1656
|
//#endregion
|
|
1657
|
+
//#region src/kernel/moleculer/children-batch-loader.ts
|
|
1658
|
+
/**
|
|
1659
|
+
* "Who are my children?" asked by a thousand devices, answered in a handful of
|
|
1660
|
+
* calls.
|
|
1661
|
+
*
|
|
1662
|
+
* ## The number
|
|
1663
|
+
*
|
|
1664
|
+
* Every `BaseDevice` inherits a `getAccessoryChildren()` that returns `[]`, so
|
|
1665
|
+
* the accessory reconcile in `device-cap-proxy.ts` runs its orphan-prune step
|
|
1666
|
+
* for EVERY registered device — including the leaf accessories that can never
|
|
1667
|
+
* own one. Each pass asked `device-manager.getChildren` for its own id.
|
|
1668
|
+
* Measured on the live hub 2026-08-27, 120-second boot window, 1 017 devices:
|
|
1669
|
+
* **1 024 calls returning 919 rows in total**. A thousand round-trips, and a
|
|
1670
|
+
* thousand indexed scans behind them, to move less than one row each.
|
|
1671
|
+
*
|
|
1672
|
+
* ## Why a loader and not a caller that loops
|
|
1673
|
+
*
|
|
1674
|
+
* There is no loop to unroll. Reconcile is per-device by construction: it runs
|
|
1675
|
+
* inside `register()`, and again whenever a late feature-probe writes the
|
|
1676
|
+
* slice. The repetition is a THOUSAND INDEPENDENT CALLERS, not one caller
|
|
1677
|
+
* asking a thousand times — so the collapse has to happen between them.
|
|
1678
|
+
*
|
|
1679
|
+
* ## Why the batch is self-clocking, with no window to tune
|
|
1680
|
+
*
|
|
1681
|
+
* At most ONE batch call is in flight. Anything that arrives while it is in
|
|
1682
|
+
* flight lands in the next batch, dispatched the moment the current one
|
|
1683
|
+
* settles. That makes the batch size adapt to the arrival rate on its own:
|
|
1684
|
+
*
|
|
1685
|
+
* - Idle: the first caller flushes on the next macrotask. No added wait, and
|
|
1686
|
+
* no timer to pick a value for and be wrong about.
|
|
1687
|
+
* - Boot: `BaseDeviceProvider.onRestoreDevices` issues every top-level
|
|
1688
|
+
* `create()` at once and `runBounded(specs, 12)` spawns accessory children
|
|
1689
|
+
* twelve wide, so reconciles arrive in bursts — measured as 2 372 device
|
|
1690
|
+
* log lines in one 5-second bucket at boot+16s, against 62 at boot+142s.
|
|
1691
|
+
* Every arrival inside one round-trip collapses into the next call.
|
|
1692
|
+
* - Worst case: arrivals slower than a round-trip give batches of one, which
|
|
1693
|
+
* is exactly today's cost. The loader can never be worse than what it
|
|
1694
|
+
* replaces.
|
|
1695
|
+
*
|
|
1696
|
+
* A fixed debounce window would have to be guessed against that distribution,
|
|
1697
|
+
* and would tax the idle case to pay for the busy one.
|
|
1698
|
+
*
|
|
1699
|
+
* ## Degrading, rather than breaking, against an older hub
|
|
1700
|
+
*
|
|
1701
|
+
* `getChildrenBatch` is a NEW method: a hub that predates it rejects the call
|
|
1702
|
+
* (it does not silently strip the input and answer a different question, which
|
|
1703
|
+
* is what adding a `parentDeviceIds` FIELD to `getChildren` would have done).
|
|
1704
|
+
* Every waiter in a failed batch is then answered by the per-parent
|
|
1705
|
+
* `getChildren` it would have used anyway, so the reconcile is never denied
|
|
1706
|
+
* its answer. A rejection that names the method as unknown latches the
|
|
1707
|
+
* fallback permanently — re-probing a method the hub will never have, once per
|
|
1708
|
+
* batch for a whole boot, is the storm this exists to remove.
|
|
1709
|
+
*/
|
|
1710
|
+
/**
|
|
1711
|
+
* Narrow an untyped cap answer to the fields the reconcile reads.
|
|
1712
|
+
*
|
|
1713
|
+
* `callDeviceManager` is an untyped boundary, and the answer may come from a
|
|
1714
|
+
* hub of a different vintage. A row that does not carry an id, a stableId and
|
|
1715
|
+
* a name is not a child this code can prune ON, so it is dropped rather than
|
|
1716
|
+
* cast into shape — the prune step DELETES, and a half-parsed row is the input
|
|
1717
|
+
* that would make it delete the wrong thing.
|
|
1718
|
+
*/
|
|
1719
|
+
function parseChildRows(value) {
|
|
1720
|
+
if (!Array.isArray(value)) return null;
|
|
1721
|
+
const rows = [];
|
|
1722
|
+
for (const entry of value) {
|
|
1723
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1724
|
+
const id = Reflect.get(entry, "id");
|
|
1725
|
+
const stableId = Reflect.get(entry, "stableId");
|
|
1726
|
+
const name = Reflect.get(entry, "name");
|
|
1727
|
+
const linkDeviceId = Reflect.get(entry, "linkDeviceId");
|
|
1728
|
+
if (typeof id !== "number" || typeof stableId !== "string" || typeof name !== "string") return null;
|
|
1729
|
+
rows.push({
|
|
1730
|
+
id,
|
|
1731
|
+
stableId,
|
|
1732
|
+
name,
|
|
1733
|
+
...typeof linkDeviceId === "number" || linkDeviceId === null ? { linkDeviceId } : {}
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
return rows;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Narrow a `getChildrenBatch` answer: a record keyed by parent id as a string.
|
|
1740
|
+
*
|
|
1741
|
+
* An EMPTY record is a valid answer — "none of these parents has children" —
|
|
1742
|
+
* and so is what a hub that answers the call but knows none of the parents
|
|
1743
|
+
* would send. Both mean "nothing to prune", which is the safe direction. Only
|
|
1744
|
+
* a shape that is not a record at all is `null`, and the loader treats that as
|
|
1745
|
+
* a failed batch and re-asks per parent.
|
|
1746
|
+
*/
|
|
1747
|
+
function parseChildrenByParent(value) {
|
|
1748
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
1749
|
+
const out = /* @__PURE__ */ new Map();
|
|
1750
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1751
|
+
const parentDeviceId = Number(key);
|
|
1752
|
+
if (!Number.isInteger(parentDeviceId)) return null;
|
|
1753
|
+
const rows = parseChildRows(entry);
|
|
1754
|
+
if (rows === null) return null;
|
|
1755
|
+
out.set(parentDeviceId, rows);
|
|
1756
|
+
}
|
|
1757
|
+
return out;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* True when an error says the hub has no such procedure — the version-skew
|
|
1761
|
+
* case, and the only one worth latching on. Anything else (a timeout, a busy
|
|
1762
|
+
* broker) is transient and must not disable batching for the process lifetime.
|
|
1763
|
+
*/
|
|
1764
|
+
function looksLikeUnknownMethod(err) {
|
|
1765
|
+
const lowered = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
1766
|
+
return lowered.includes("not_found") || lowered.includes("no procedure") || lowered.includes("unknown method") || lowered.includes("no such method") || lowered.includes("not available");
|
|
1767
|
+
}
|
|
1768
|
+
function createChildrenBatchLoader(opts) {
|
|
1769
|
+
const maxBatch = Math.max(1, opts.maxBatch ?? 256);
|
|
1770
|
+
let pending = [];
|
|
1771
|
+
let flushScheduled = false;
|
|
1772
|
+
let flushing = false;
|
|
1773
|
+
let batchUnsupported = false;
|
|
1774
|
+
const answerOneByOne = async (waiters) => {
|
|
1775
|
+
await Promise.all(waiters.map(async (waiter) => {
|
|
1776
|
+
try {
|
|
1777
|
+
waiter.resolve(await opts.fetchOne(waiter.parentDeviceId));
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
waiter.reject(err);
|
|
1780
|
+
}
|
|
1781
|
+
}));
|
|
1782
|
+
};
|
|
1783
|
+
const runBatch = async (waiters) => {
|
|
1784
|
+
const parentDeviceIds = [...new Set(waiters.map((w) => w.parentDeviceId))];
|
|
1785
|
+
try {
|
|
1786
|
+
const byParent = await opts.fetchBatch(parentDeviceIds);
|
|
1787
|
+
for (const waiter of waiters) waiter.resolve(byParent.get(waiter.parentDeviceId) ?? []);
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
const unknown = looksLikeUnknownMethod(err);
|
|
1790
|
+
if (unknown) batchUnsupported = true;
|
|
1791
|
+
opts.logger.warn(unknown ? "device-manager.getChildrenBatch is not available on this hub — falling back to per-parent getChildren for the process lifetime" : "device-manager.getChildrenBatch failed — answering this batch per parent", { meta: {
|
|
1792
|
+
parents: parentDeviceIds.length,
|
|
1793
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1794
|
+
} });
|
|
1795
|
+
await answerOneByOne(waiters);
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
const flush = async () => {
|
|
1799
|
+
flushing = true;
|
|
1800
|
+
try {
|
|
1801
|
+
while (pending.length > 0) {
|
|
1802
|
+
const batch = pending.slice(0, maxBatch);
|
|
1803
|
+
pending = pending.slice(maxBatch);
|
|
1804
|
+
if (batchUnsupported) await answerOneByOne(batch);
|
|
1805
|
+
else await runBatch(batch);
|
|
1806
|
+
}
|
|
1807
|
+
} finally {
|
|
1808
|
+
flushing = false;
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
const scheduleFlush = () => {
|
|
1812
|
+
if (flushScheduled || flushing) return;
|
|
1813
|
+
flushScheduled = true;
|
|
1814
|
+
setTimeout(() => {
|
|
1815
|
+
flushScheduled = false;
|
|
1816
|
+
flush().catch((err) => {
|
|
1817
|
+
opts.logger.warn("children batch flush threw", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
1818
|
+
});
|
|
1819
|
+
}, 0);
|
|
1820
|
+
};
|
|
1821
|
+
return { load(parentDeviceId) {
|
|
1822
|
+
return new Promise((resolve, reject) => {
|
|
1823
|
+
pending.push({
|
|
1824
|
+
parentDeviceId,
|
|
1825
|
+
resolve,
|
|
1826
|
+
reject
|
|
1827
|
+
});
|
|
1828
|
+
scheduleFlush();
|
|
1829
|
+
});
|
|
1830
|
+
} };
|
|
1831
|
+
}
|
|
1832
|
+
//#endregion
|
|
1656
1833
|
//#region src/kernel/moleculer/device-cap-proxy.ts
|
|
1657
1834
|
/**
|
|
1658
1835
|
* Max number of accessory children spawned concurrently during a single
|
|
@@ -1943,6 +2120,19 @@ function createBrokerDeviceManagerApi(opts) {
|
|
|
1943
2120
|
api
|
|
1944
2121
|
});
|
|
1945
2122
|
let selfApi;
|
|
2123
|
+
const childrenLoader = createChildrenBatchLoader({
|
|
2124
|
+
logger: opts.logger,
|
|
2125
|
+
fetchBatch: async (parentDeviceIds) => {
|
|
2126
|
+
const parsed = parseChildrenByParent(await callDeviceManager(api, "getChildrenBatch", { parentDeviceIds }));
|
|
2127
|
+
if (parsed === null) throw new Error("getChildrenBatch returned a shape this kernel cannot read");
|
|
2128
|
+
return parsed;
|
|
2129
|
+
},
|
|
2130
|
+
fetchOne: async (parentDeviceId) => {
|
|
2131
|
+
const parsed = parseChildRows(await callDeviceManager(api, "getChildren", { parentDeviceId }));
|
|
2132
|
+
if (parsed === null) throw new Error("getChildren returned a shape this kernel cannot read");
|
|
2133
|
+
return parsed;
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
1946
2136
|
const deviceRebuildFactories = /* @__PURE__ */ new Map();
|
|
1947
2137
|
const buildContext = (stableId, id, parentDeviceId = null, initialRuntimeState = {}, persistedConfig = {}, deviceMeta = null) => {
|
|
1948
2138
|
let runtimeStateRef = null;
|
|
@@ -2360,7 +2550,7 @@ function createBrokerDeviceManagerApi(opts) {
|
|
|
2360
2550
|
return;
|
|
2361
2551
|
}
|
|
2362
2552
|
try {
|
|
2363
|
-
const currentChildren = await
|
|
2553
|
+
const currentChildren = await childrenLoader.load(device.id);
|
|
2364
2554
|
for (const child of currentChildren) {
|
|
2365
2555
|
if (child.linkDeviceId !== device.id) continue;
|
|
2366
2556
|
if (expected.has(child.stableId)) continue;
|