@camstack/system 1.2.39 → 1.2.40
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/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/block-package.d.ts +59 -0
- package/dist/builtins/core-blocks/block-supervisor.d.ts +86 -0
- package/dist/builtins/core-blocks/core-blocks.addon.d.ts +18 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +533 -68
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +528 -69
- package/dist/builtins/core-blocks/index.d.ts +5 -3
- package/dist/builtins/core-blocks/index.js +6 -0
- package/dist/builtins/core-blocks/index.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +14 -6
- package/dist/builtins/device-manager/device-manager.addon.mjs +14 -6
- 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 +281 -16
- package/dist/builtins/snapshot/index.mjs +282 -17
- package/dist/builtins/snapshot/snapshot-cache.d.ts +45 -0
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +32 -2
- package/dist/builtins/snapshot/snapshot-resize.d.ts +50 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +13 -0
- 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 +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- 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/{dist-DO76YVO2.mjs → dist-BXS0DP92.mjs} +24 -2
- package/dist/{dist-CFinlH7b.js → dist-Cj0LAuXi.js} +24 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,410 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-Cj0LAuXi.js");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
node_path = require_chunk.__toESM(node_path);
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let node_fs_promises = require("node:fs/promises");
|
|
8
8
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
9
|
+
//#region src/builtins/core-blocks/block-package.ts
|
|
10
|
+
/**
|
|
11
|
+
* Turning a stored block into something the addon runner can fork.
|
|
12
|
+
*
|
|
13
|
+
* The runner already knows how to do everything a block needs — fork a
|
|
14
|
+
* process, build the full UDS `ctx`, redirect host-provided imports, and hand
|
|
15
|
+
* the whole thing to `CrashSupervisor` (D6). What it does NOT know how to do is
|
|
16
|
+
* load code out of a database row: `addon-runner` reads a package.json off
|
|
17
|
+
* disk. So a block is MATERIALISED as a minimal addon package and spawned like
|
|
18
|
+
* any other addon. No second runner, no second supervisor, no `node:vm`.
|
|
19
|
+
*
|
|
20
|
+
* Two files are generated per block:
|
|
21
|
+
*
|
|
22
|
+
* `dist/block.js` — the author's TypeScript, transpiled.
|
|
23
|
+
* `dist/index.js` — a generated wrapper that adapts the author's contract
|
|
24
|
+
* (`export default (ctx) => disposer`) to `ICamstackAddon`.
|
|
25
|
+
*
|
|
26
|
+
* The wrapper exists so the author never writes `super({})`, which is the trap
|
|
27
|
+
* that killed `CoreBlocksAddon` in production while the build stayed green and
|
|
28
|
+
* the topology reported it running.
|
|
29
|
+
*/
|
|
30
|
+
/** Prefix for the generated addon id AND the runner id — one addon, one
|
|
31
|
+
* process, so the two are the same string (D2). Grep-able on purpose: an
|
|
32
|
+
* operator reading `$process.list` must be able to tell a block runner from
|
|
33
|
+
* an addon runner at a glance. */
|
|
34
|
+
var BLOCK_ADDON_PREFIX = "core-block-";
|
|
35
|
+
/** The generated addon/runner id for a block. */
|
|
36
|
+
function blockAddonId(blockId) {
|
|
37
|
+
return `${BLOCK_ADDON_PREFIX}${blockId}`;
|
|
38
|
+
}
|
|
39
|
+
/** The block id behind a generated addon/runner id, or null when the id
|
|
40
|
+
* belongs to something else. The inverse of {@link blockAddonId} — used to
|
|
41
|
+
* read the live process list back into block terms. */
|
|
42
|
+
function blockIdFromAddonId(addonId) {
|
|
43
|
+
if (!addonId.startsWith("core-block-")) return null;
|
|
44
|
+
const rest = addonId.slice(11);
|
|
45
|
+
return rest.length > 0 ? rest : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The wrapper module, generated verbatim.
|
|
49
|
+
*
|
|
50
|
+
* A string rather than a file copied from `dist` because it must land NEXT to
|
|
51
|
+
* the author's code inside the block package: the runner imports one entry, and
|
|
52
|
+
* relative imports have to resolve inside that directory.
|
|
53
|
+
*
|
|
54
|
+
* Three things it guarantees, all of them failures this repo has already paid
|
|
55
|
+
* for once:
|
|
56
|
+
*
|
|
57
|
+
* - `super({})` is here, so no author can omit it;
|
|
58
|
+
* - a module with no default function FAILS LOUDLY with the block's name in
|
|
59
|
+
* the message, instead of starting a process that does nothing;
|
|
60
|
+
* - the disposer is awaited on shutdown, so a block that owns a timer or a
|
|
61
|
+
* socket releases it on `setEnabled(false)` rather than on the next reboot.
|
|
62
|
+
*/
|
|
63
|
+
var WRAPPER_SOURCE = `// GENERATED by @camstack/system core-blocks. Do not edit — rewritten on every
|
|
64
|
+
// deploy of the block it wraps.
|
|
65
|
+
import { BaseAddon } from '@camstack/types'
|
|
66
|
+
// NAMESPACE import, not \`import blockMain from\`. A default import of a module
|
|
67
|
+
// that has no default export is a LINK-time SyntaxError: the module never
|
|
68
|
+
// executes, so the friendly check below would never run and the author would
|
|
69
|
+
// get a bare Node stack trace naming neither their block nor the mistake.
|
|
70
|
+
// Verified, not assumed — a test spawns exactly that module.
|
|
71
|
+
import * as blockModule from './block.js'
|
|
72
|
+
|
|
73
|
+
const BLOCK_ID = __BLOCK_ID__
|
|
74
|
+
const BLOCK_NAME = __BLOCK_NAME__
|
|
75
|
+
const blockMain = blockModule.default
|
|
76
|
+
|
|
77
|
+
export default class CoreBlockAddon extends BaseAddon {
|
|
78
|
+
constructor() {
|
|
79
|
+
// NOT optional: BaseAddon.resolveConfig reads Object.keys(this.defaults),
|
|
80
|
+
// so omitting this kills the process at init while the topology still
|
|
81
|
+
// reports the addon running.
|
|
82
|
+
super({})
|
|
83
|
+
this.disposer = null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async onInitialize() {
|
|
87
|
+
if (typeof blockMain !== 'function') {
|
|
88
|
+
throw new Error(
|
|
89
|
+
\`core block "\${BLOCK_NAME}" (\${BLOCK_ID}) has no default export function — \` +
|
|
90
|
+
'a block must "export default" a function taking ctx',
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
const result = await blockMain(this.ctx)
|
|
94
|
+
this.disposer = typeof result === 'function' ? result : null
|
|
95
|
+
// One line, always. A block that started and does nothing visible is
|
|
96
|
+
// indistinguishable from one that never loaded without it.
|
|
97
|
+
this.ctx.logger.info('core block started', {
|
|
98
|
+
meta: { blockId: BLOCK_ID, name: BLOCK_NAME, hasDisposer: this.disposer !== null },
|
|
99
|
+
})
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async onShutdown() {
|
|
104
|
+
if (this.disposer === null) return
|
|
105
|
+
const dispose = this.disposer
|
|
106
|
+
this.disposer = null
|
|
107
|
+
await dispose()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
/** Build the wrapper for one block. Exported for the test that asserts the
|
|
112
|
+
* generated source actually carries the block's identity. */
|
|
113
|
+
function renderBlockWrapper(blockId, blockName) {
|
|
114
|
+
return WRAPPER_SOURCE.replace("__BLOCK_ID__", JSON.stringify(blockId)).replace("__BLOCK_NAME__", JSON.stringify(blockName));
|
|
115
|
+
}
|
|
116
|
+
/** The package manifest the addon runner reads to find the entry point. */
|
|
117
|
+
function renderBlockManifest(blockId, blockName) {
|
|
118
|
+
return `${JSON.stringify({
|
|
119
|
+
name: `@camstack/${blockAddonId(blockId)}`,
|
|
120
|
+
version: "0.0.0",
|
|
121
|
+
private: true,
|
|
122
|
+
type: "module",
|
|
123
|
+
main: "./dist/index.js",
|
|
124
|
+
camstack: { addons: [{
|
|
125
|
+
id: blockAddonId(blockId),
|
|
126
|
+
name: blockName,
|
|
127
|
+
category: "system",
|
|
128
|
+
description: "A user-authored core block.",
|
|
129
|
+
entry: "./dist/index.js",
|
|
130
|
+
capabilities: []
|
|
131
|
+
}] }
|
|
132
|
+
}, null, 2)}\n`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Write (or rewrite) a block's package. Idempotent, and deliberately a full
|
|
136
|
+
* rewrite: the author's code is the thing that changes, and a partial update
|
|
137
|
+
* that left a stale `block.js` behind would run yesterday's code while the
|
|
138
|
+
* editor showed today's.
|
|
139
|
+
*/
|
|
140
|
+
async function materializeBlock(input) {
|
|
141
|
+
const addonDir = node_path.join(input.root, input.blockId);
|
|
142
|
+
const distDir = node_path.join(addonDir, "dist");
|
|
143
|
+
await node_fs_promises.mkdir(distDir, { recursive: true });
|
|
144
|
+
await Promise.all([
|
|
145
|
+
node_fs_promises.writeFile(node_path.join(addonDir, "package.json"), renderBlockManifest(input.blockId, input.blockName), "utf-8"),
|
|
146
|
+
node_fs_promises.writeFile(node_path.join(distDir, "block.js"), input.js, "utf-8"),
|
|
147
|
+
node_fs_promises.writeFile(node_path.join(distDir, "index.js"), renderBlockWrapper(input.blockId, input.blockName), "utf-8")
|
|
148
|
+
]);
|
|
149
|
+
return {
|
|
150
|
+
addonDir,
|
|
151
|
+
addonId: blockAddonId(input.blockId)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Remove a block's package. Idempotent — a missing directory is the desired
|
|
155
|
+
* end state, not an error. */
|
|
156
|
+
async function removeBlockPackage(root, blockId) {
|
|
157
|
+
await node_fs_promises.rm(node_path.join(root, blockId), {
|
|
158
|
+
recursive: true,
|
|
159
|
+
force: true
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/builtins/core-blocks/block-supervisor.ts
|
|
164
|
+
/** Runner state → what the author should read. `crashed` is the window between
|
|
165
|
+
* an exit and the scheduled respawn, so it reads as `starting` with the reason
|
|
166
|
+
* attached rather than as a terminal failure it is not. */
|
|
167
|
+
var RUNNER_STATE_TO_STATUS = {
|
|
168
|
+
starting: "starting",
|
|
169
|
+
running: "running",
|
|
170
|
+
crashed: "starting",
|
|
171
|
+
failed: "failed",
|
|
172
|
+
stopping: null,
|
|
173
|
+
stopped: null
|
|
174
|
+
};
|
|
175
|
+
var BlockSupervisor = class {
|
|
176
|
+
deps;
|
|
177
|
+
/**
|
|
178
|
+
* `blockId → updatedAt` of the source this supervisor last materialised.
|
|
179
|
+
* The only way to know a running block is running YESTERDAY'S code: the
|
|
180
|
+
* process is alive either way, and nothing on disk carries the version.
|
|
181
|
+
* In-memory is correct — a hub restart takes every child with it.
|
|
182
|
+
*/
|
|
183
|
+
materialized = /* @__PURE__ */ new Map();
|
|
184
|
+
timer = null;
|
|
185
|
+
/** Serialises passes so a timer tick cannot interleave with a write-driven
|
|
186
|
+
* converge and spawn the same runner twice. */
|
|
187
|
+
inFlight = Promise.resolve();
|
|
188
|
+
constructor(deps) {
|
|
189
|
+
this.deps = deps;
|
|
190
|
+
}
|
|
191
|
+
/** Start the reconcile timer. The first pass runs immediately. */
|
|
192
|
+
start(intervalMs) {
|
|
193
|
+
if (this.timer !== null) return;
|
|
194
|
+
this.converge();
|
|
195
|
+
this.timer = setInterval(() => void this.converge(), intervalMs);
|
|
196
|
+
this.timer.unref?.();
|
|
197
|
+
}
|
|
198
|
+
stop() {
|
|
199
|
+
if (this.timer === null) return;
|
|
200
|
+
clearInterval(this.timer);
|
|
201
|
+
this.timer = null;
|
|
202
|
+
}
|
|
203
|
+
/** One reconcile pass. Never throws — a pass that failed must not stop the
|
|
204
|
+
* next one, and the reason belongs in the log, not in a rejected promise
|
|
205
|
+
* nobody awaits. */
|
|
206
|
+
converge() {
|
|
207
|
+
this.inFlight = this.inFlight.then(() => this.runPass(), () => this.runPass());
|
|
208
|
+
return this.inFlight;
|
|
209
|
+
}
|
|
210
|
+
async runPass() {
|
|
211
|
+
try {
|
|
212
|
+
await this.reconcile();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
this.deps.logger.error("core-blocks converge failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async reconcile() {
|
|
218
|
+
const blocks = this.deps.store.list();
|
|
219
|
+
const mine = blocks.filter((b) => b.enabled && this.isPlacedHere(b));
|
|
220
|
+
const runners = new Map((await this.deps.host.list()).map((r) => [blockIdFromAddonId(r.name), r]).filter((pair) => pair[0] !== null));
|
|
221
|
+
for (const block of blocks) {
|
|
222
|
+
if (!block.enabled || this.isPlacedHere(block)) continue;
|
|
223
|
+
await this.report(block.id, "failed", `no core-block supervisor runs on "${block.placement}" — this block is not running anywhere. Move it to "${this.deps.nodeId}" to run it.`);
|
|
224
|
+
}
|
|
225
|
+
const wanted = new Set(mine.map((b) => b.id));
|
|
226
|
+
for (const [blockId, runner] of runners) {
|
|
227
|
+
if (wanted.has(blockId)) continue;
|
|
228
|
+
this.deps.logger.info("stopping core block runner", { meta: {
|
|
229
|
+
blockId,
|
|
230
|
+
runnerState: runner.state,
|
|
231
|
+
reason: this.stopReason(blockId)
|
|
232
|
+
} });
|
|
233
|
+
await this.deps.host.stop(blockAddonId(blockId));
|
|
234
|
+
this.materialized.delete(blockId);
|
|
235
|
+
if (this.deps.store.get(blockId) !== null) await this.report(blockId, "stopped");
|
|
236
|
+
else await removeBlockPackage(this.deps.root, blockId);
|
|
237
|
+
}
|
|
238
|
+
for (const block of mine) {
|
|
239
|
+
const runner = runners.get(block.id);
|
|
240
|
+
const stale = runner !== void 0 && this.materialized.get(block.id) !== block.updatedAt;
|
|
241
|
+
if (runner !== void 0 && !stale) {
|
|
242
|
+
await this.reportRunnerState(block, runner);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (stale) {
|
|
246
|
+
this.deps.logger.info("core block code changed — restarting its runner", { meta: {
|
|
247
|
+
blockId: block.id,
|
|
248
|
+
name: block.name
|
|
249
|
+
} });
|
|
250
|
+
await this.deps.host.stop(blockAddonId(block.id));
|
|
251
|
+
this.materialized.delete(block.id);
|
|
252
|
+
}
|
|
253
|
+
await this.launch(block);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Compile, write the package, spawn. Each failure is reported ON the block
|
|
257
|
+
* — the author asked for this to run, so they are owed the reason it does
|
|
258
|
+
* not. */
|
|
259
|
+
async launch(block) {
|
|
260
|
+
const compiled = await this.deps.compile(block.code);
|
|
261
|
+
if (!compiled.ok || compiled.js === void 0) {
|
|
262
|
+
await this.report(block.id, "failed", `the block does not compile: ${compiled.error ?? "unknown"}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const pkg = await materializeBlock({
|
|
267
|
+
root: this.deps.root,
|
|
268
|
+
blockId: block.id,
|
|
269
|
+
blockName: block.name,
|
|
270
|
+
js: compiled.js
|
|
271
|
+
});
|
|
272
|
+
await this.deps.host.spawn({
|
|
273
|
+
runnerId: pkg.addonId,
|
|
274
|
+
addonId: pkg.addonId,
|
|
275
|
+
addonDir: pkg.addonDir
|
|
276
|
+
});
|
|
277
|
+
this.materialized.set(block.id, block.updatedAt);
|
|
278
|
+
this.deps.logger.info("core block runner spawned", { meta: {
|
|
279
|
+
blockId: block.id,
|
|
280
|
+
name: block.name,
|
|
281
|
+
runnerId: pkg.addonId
|
|
282
|
+
} });
|
|
283
|
+
await this.report(block.id, "starting");
|
|
284
|
+
} catch (err) {
|
|
285
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
286
|
+
if (/already running/i.test(message)) {
|
|
287
|
+
this.materialized.set(block.id, block.updatedAt);
|
|
288
|
+
await this.report(block.id, "starting");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.deps.logger.error("core block failed to start", { meta: {
|
|
292
|
+
blockId: block.id,
|
|
293
|
+
name: block.name,
|
|
294
|
+
error: message
|
|
295
|
+
} });
|
|
296
|
+
await this.report(block.id, "failed", `could not start the block: ${message}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async reportRunnerState(block, runner) {
|
|
300
|
+
const status = RUNNER_STATE_TO_STATUS[runner.state];
|
|
301
|
+
if (status === null) return;
|
|
302
|
+
if (runner.state === "failed") {
|
|
303
|
+
await this.report(block.id, "failed", "the crash circuit-breaker tripped — the block crashed repeatedly and will not be respawned. Fix it and save, or disable and re-enable it.");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (runner.state === "crashed") {
|
|
307
|
+
await this.report(block.id, "starting", "the block crashed — a respawn is scheduled");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
await this.report(block.id, status);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Write a status only when it actually changed.
|
|
314
|
+
*
|
|
315
|
+
* A reconcile pass runs on a timer, and every write rewrites the row AND
|
|
316
|
+
* moves `lastChangedAt` — which is the only thing telling an operator WHEN a
|
|
317
|
+
* block last did something. Rewriting it every tick would make it read as
|
|
318
|
+
* "just now", forever.
|
|
319
|
+
*/
|
|
320
|
+
async report(blockId, status, error) {
|
|
321
|
+
const current = this.deps.store.get(blockId);
|
|
322
|
+
if (current !== null && current.status === status && current.lastError === error) return;
|
|
323
|
+
await this.deps.store.setStatus(blockId, status, error);
|
|
324
|
+
}
|
|
325
|
+
isPlacedHere(block) {
|
|
326
|
+
return block.placement === this.deps.nodeId;
|
|
327
|
+
}
|
|
328
|
+
stopReason(blockId) {
|
|
329
|
+
const block = this.deps.store.get(blockId);
|
|
330
|
+
if (block === null) return "deleted";
|
|
331
|
+
if (!block.enabled) return "disabled";
|
|
332
|
+
return `moved to "${block.placement}"`;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/builtins/core-blocks/block-type-defs.ts
|
|
337
|
+
/**
|
|
338
|
+
* The declaration files a block is authored against.
|
|
339
|
+
*
|
|
340
|
+
* Served from the node rather than bundled into the admin UI: the graph is
|
|
341
|
+
* 3.4 MB across 344 files. Served rather than hand-stubbed: a block runs with
|
|
342
|
+
* the same `ctx` an addon gets, so a stub would drift from it and the editor
|
|
343
|
+
* would confidently autocomplete methods that do not exist.
|
|
344
|
+
*
|
|
345
|
+
* The paths matter more than they look. Monaco resolves imports against the
|
|
346
|
+
* extra-lib paths, so a file registered as anything other than
|
|
347
|
+
* `file:///node_modules/@camstack/types/…` loads and then resolves NOTHING —
|
|
348
|
+
* the failure that looks most like success, because the editor keeps working
|
|
349
|
+
* and simply stops knowing anything.
|
|
350
|
+
*/
|
|
351
|
+
/** Where Monaco must think the files live for `@camstack/types` to resolve. */
|
|
352
|
+
var VIRTUAL_ROOT = "file:///node_modules/@camstack/types";
|
|
353
|
+
/**
|
|
354
|
+
* Read every `.d.ts` under the installed `@camstack/types` dist.
|
|
355
|
+
*
|
|
356
|
+
* Best-effort by design: a node that cannot find them serves an EMPTY list, and
|
|
357
|
+
* the editor then registers an empty lib set rather than keeping a stale one —
|
|
358
|
+
* so an author sees "no types" instead of validating against a contract that
|
|
359
|
+
* has moved.
|
|
360
|
+
*/
|
|
361
|
+
async function loadBlockTypeDefs(logger) {
|
|
362
|
+
const root = await resolveTypesDist();
|
|
363
|
+
if (root === null) {
|
|
364
|
+
logger.warn("core-blocks: @camstack/types declarations not found — the editor gets no types");
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
const out = [];
|
|
368
|
+
await walk(root, root, out);
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
async function resolveTypesDist() {
|
|
372
|
+
try {
|
|
373
|
+
const entry = await {}.resolve?.("@camstack/types");
|
|
374
|
+
if (typeof entry === "string") {
|
|
375
|
+
const dir = node_path.dirname(entry.replace("file://", ""));
|
|
376
|
+
if (await exists(node_path.join(dir, "index.d.ts"))) return dir;
|
|
377
|
+
}
|
|
378
|
+
} catch {}
|
|
379
|
+
const guess = node_path.resolve(process.cwd(), "node_modules/@camstack/types/dist");
|
|
380
|
+
return await exists(guess) ? guess : null;
|
|
381
|
+
}
|
|
382
|
+
async function exists(p) {
|
|
383
|
+
try {
|
|
384
|
+
await node_fs_promises.access(p);
|
|
385
|
+
return true;
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function walk(root, dir, out) {
|
|
391
|
+
const entries = await node_fs_promises.readdir(dir, { withFileTypes: true });
|
|
392
|
+
for (const entry of entries) {
|
|
393
|
+
const full = node_path.join(dir, entry.name);
|
|
394
|
+
if (entry.isDirectory()) {
|
|
395
|
+
await walk(root, full, out);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (!entry.name.endsWith(".d.ts")) continue;
|
|
399
|
+
const content = await node_fs_promises.readFile(full, "utf-8");
|
|
400
|
+
const relative = node_path.relative(root, full).split(node_path.sep).join("/");
|
|
401
|
+
out.push({
|
|
402
|
+
filePath: `${VIRTUAL_ROOT}/${relative}`,
|
|
403
|
+
content
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
9
408
|
//#region src/builtins/core-blocks/compile-block.ts
|
|
10
409
|
/** An esbuild error carries the position; the shape is narrow enough to read
|
|
11
410
|
* defensively rather than to type against the package. */
|
|
@@ -58,6 +457,19 @@ async function compileBlock(transpiler, code) {
|
|
|
58
457
|
}
|
|
59
458
|
//#endregion
|
|
60
459
|
//#region src/builtins/core-blocks/core-block-store.ts
|
|
460
|
+
/**
|
|
461
|
+
* Durable storage for core blocks.
|
|
462
|
+
*
|
|
463
|
+
* A kernel table, not an addon's: a block is not owned by any addon — it is
|
|
464
|
+
* user code the kernel runs. The name is bare for the same reason `users` and
|
|
465
|
+
* `system-settings` are (the collection-namespacing guard scans
|
|
466
|
+
* `packages/addon-*` only, and this is deliberately not one).
|
|
467
|
+
*
|
|
468
|
+
* The write path is the interesting part. **Source is never stored without
|
|
469
|
+
* compiling it first.** A block that does not compile is not a broken row, it
|
|
470
|
+
* is a fork failure the operator meets minutes later inside a log — so the
|
|
471
|
+
* compile happens where they are still looking at the editor.
|
|
472
|
+
*/
|
|
61
473
|
var CORE_BLOCKS_COLLECTION = "core_blocks";
|
|
62
474
|
var CORE_BLOCKS_COLUMNS = [
|
|
63
475
|
{
|
|
@@ -203,7 +615,7 @@ var CoreBlockStore = class {
|
|
|
203
615
|
lastChangedAt: this.now(),
|
|
204
616
|
...error !== void 0 ? { lastError: error } : {}
|
|
205
617
|
};
|
|
206
|
-
if (error === void 0 && next.lastError !== void 0
|
|
618
|
+
if (error === void 0 && next.lastError !== void 0) delete next.lastError;
|
|
207
619
|
await this.persist(next);
|
|
208
620
|
this.byId.set(blockId, next);
|
|
209
621
|
}
|
|
@@ -230,78 +642,34 @@ var CoreBlockStore = class {
|
|
|
230
642
|
}
|
|
231
643
|
};
|
|
232
644
|
//#endregion
|
|
233
|
-
//#region src/builtins/core-blocks/block-type-defs.ts
|
|
234
|
-
/** Where Monaco must think the files live for `@camstack/types` to resolve. */
|
|
235
|
-
var VIRTUAL_ROOT = "file:///node_modules/@camstack/types";
|
|
236
|
-
/**
|
|
237
|
-
* Read every `.d.ts` under the installed `@camstack/types` dist.
|
|
238
|
-
*
|
|
239
|
-
* Best-effort by design: a node that cannot find them serves an EMPTY list, and
|
|
240
|
-
* the editor then registers an empty lib set rather than keeping a stale one —
|
|
241
|
-
* so an author sees "no types" instead of validating against a contract that
|
|
242
|
-
* has moved.
|
|
243
|
-
*/
|
|
244
|
-
async function loadBlockTypeDefs(logger) {
|
|
245
|
-
const root = await resolveTypesDist();
|
|
246
|
-
if (root === null) {
|
|
247
|
-
logger.warn("core-blocks: @camstack/types declarations not found — the editor gets no types");
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
250
|
-
const out = [];
|
|
251
|
-
await walk(root, root, out);
|
|
252
|
-
return out;
|
|
253
|
-
}
|
|
254
|
-
async function resolveTypesDist() {
|
|
255
|
-
try {
|
|
256
|
-
const entry = await {}.resolve?.("@camstack/types");
|
|
257
|
-
if (typeof entry === "string") {
|
|
258
|
-
const dir = node_path.dirname(entry.replace("file://", ""));
|
|
259
|
-
if (await exists(node_path.join(dir, "index.d.ts"))) return dir;
|
|
260
|
-
}
|
|
261
|
-
} catch {}
|
|
262
|
-
const guess = node_path.resolve(process.cwd(), "node_modules/@camstack/types/dist");
|
|
263
|
-
return await exists(guess) ? guess : null;
|
|
264
|
-
}
|
|
265
|
-
async function exists(p) {
|
|
266
|
-
try {
|
|
267
|
-
await node_fs_promises.access(p);
|
|
268
|
-
return true;
|
|
269
|
-
} catch {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
async function walk(root, dir, out) {
|
|
274
|
-
const entries = await node_fs_promises.readdir(dir, { withFileTypes: true });
|
|
275
|
-
for (const entry of entries) {
|
|
276
|
-
const full = node_path.join(dir, entry.name);
|
|
277
|
-
if (entry.isDirectory()) {
|
|
278
|
-
await walk(root, full, out);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (!entry.name.endsWith(".d.ts")) continue;
|
|
282
|
-
const content = await node_fs_promises.readFile(full, "utf-8");
|
|
283
|
-
const relative = node_path.relative(root, full).split(node_path.sep).join("/");
|
|
284
|
-
out.push({
|
|
285
|
-
filePath: `${VIRTUAL_ROOT}/${relative}`,
|
|
286
|
-
content
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
//#endregion
|
|
291
645
|
//#region src/builtins/core-blocks/core-blocks.addon.ts
|
|
292
646
|
/**
|
|
293
647
|
* The `core-blocks` provider — CRUD over user-authored TypeScript, with the
|
|
294
|
-
* compile gate in front of every write
|
|
648
|
+
* compile gate in front of every write, and the supervisor that turns a stored
|
|
649
|
+
* block into a running process.
|
|
295
650
|
*
|
|
296
651
|
* A builtin because a block belongs to the kernel, not to an addon: it is user
|
|
297
652
|
* code the node runs, and it must exist on every install with no opt-in step.
|
|
298
653
|
*
|
|
299
|
-
* **
|
|
300
|
-
* block
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
654
|
+
* **Execution is HUB-ONLY for now**, and that limit is loud rather than
|
|
655
|
+
* implied: a block placed on any other node is reported `failed` with a message
|
|
656
|
+
* naming the node, because an enabled block nothing runs must never look like
|
|
657
|
+
* an idle one. The supervisor is already written per-node (`converge()` only
|
|
658
|
+
* ever considers blocks placed on `nodeId`), so the agent case is an
|
|
659
|
+
* implementation of `BlockProcessHost` plus a way to reach the hub's store —
|
|
660
|
+
* not a redesign.
|
|
661
|
+
*/
|
|
662
|
+
/**
|
|
663
|
+
* How often the supervisor re-reads the world.
|
|
664
|
+
*
|
|
665
|
+
* It is not the primary trigger — every write converges immediately — but the
|
|
666
|
+
* one that catches what a write cannot: a runner that crashed, a breaker that
|
|
667
|
+
* tripped, a spawn that never landed. D8: events may be dropped, so the
|
|
668
|
+
* reconcile is what makes the outcome eventually true.
|
|
304
669
|
*/
|
|
670
|
+
var CONVERGE_INTERVAL_MS = 15e3;
|
|
671
|
+
/** Where block packages are materialised, under this node's data root. */
|
|
672
|
+
var BLOCK_PACKAGE_DIRNAME = "core-blocks";
|
|
305
673
|
var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
306
674
|
/**
|
|
307
675
|
* `super({})` is NOT optional, and its absence is invisible until runtime:
|
|
@@ -316,6 +684,7 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
316
684
|
store = null;
|
|
317
685
|
transpiler = null;
|
|
318
686
|
typeDefs = null;
|
|
687
|
+
supervisor = null;
|
|
319
688
|
async onInitialize() {
|
|
320
689
|
const settingsStore = this.ctx.api?.settingsStore;
|
|
321
690
|
if (!settingsStore) throw new Error("core-blocks: settings-store API not available — refusing to register the cap");
|
|
@@ -326,9 +695,12 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
326
695
|
});
|
|
327
696
|
await this.store.load();
|
|
328
697
|
this.transpiler = await loadTranspiler(this.ctx.logger);
|
|
698
|
+
this.supervisor = this.buildSupervisor();
|
|
699
|
+
this.supervisor?.start(CONVERGE_INTERVAL_MS);
|
|
329
700
|
this.ctx.logger.info("core-blocks ready", { meta: {
|
|
330
701
|
blocks: this.store.list().length,
|
|
331
|
-
compile: this.transpiler !== null
|
|
702
|
+
compile: this.transpiler !== null,
|
|
703
|
+
supervising: this.supervisor !== null
|
|
332
704
|
} });
|
|
333
705
|
return [{
|
|
334
706
|
capability: require_dist.coreBlocksCapability,
|
|
@@ -336,10 +708,65 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
336
708
|
}];
|
|
337
709
|
}
|
|
338
710
|
async onShutdown() {
|
|
711
|
+
this.supervisor?.stop();
|
|
712
|
+
this.supervisor = null;
|
|
339
713
|
this.store = null;
|
|
340
714
|
this.transpiler = null;
|
|
341
715
|
this.typeDefs = null;
|
|
342
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* Build the supervisor, or explain in one line why this node runs no blocks.
|
|
719
|
+
*
|
|
720
|
+
* Two ways to end up with none, and both are logged rather than inferred:
|
|
721
|
+
* this is not the hub (execution is hub-only for now), or the kernel exposed
|
|
722
|
+
* no cluster broker, which is the only door to `$process.*`.
|
|
723
|
+
*/
|
|
724
|
+
buildSupervisor() {
|
|
725
|
+
const rawNodeId = this.ctx.kernel.localNodeId ?? "hub";
|
|
726
|
+
const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] ?? "hub" : rawNodeId;
|
|
727
|
+
if (nodeId !== "hub") {
|
|
728
|
+
this.ctx.logger.info("core-blocks execution is hub-only — no blocks will run on this node", { meta: {
|
|
729
|
+
nodeId,
|
|
730
|
+
rawNodeId
|
|
731
|
+
} });
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const broker = this.ctx.kernel.cluster?.broker;
|
|
735
|
+
if (!broker) {
|
|
736
|
+
this.ctx.logger.warn("core-blocks: no cluster broker — blocks can be stored and compiled, but nothing can run them");
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
const store = this.requireStore();
|
|
740
|
+
return new BlockSupervisor({
|
|
741
|
+
nodeId,
|
|
742
|
+
root: node_path.join(this.ctx.nodeDataDir, BLOCK_PACKAGE_DIRNAME),
|
|
743
|
+
store: {
|
|
744
|
+
list: () => store.list(),
|
|
745
|
+
get: (blockId) => store.get(blockId),
|
|
746
|
+
setStatus: (blockId, status, error) => store.setStatus(blockId, status, error)
|
|
747
|
+
},
|
|
748
|
+
host: createBrokerProcessHost(broker),
|
|
749
|
+
compile: async (code) => {
|
|
750
|
+
if (this.transpiler === null) return {
|
|
751
|
+
ok: false,
|
|
752
|
+
error: "the compiler is unavailable on this node"
|
|
753
|
+
};
|
|
754
|
+
return compileBlock(this.transpiler, code);
|
|
755
|
+
},
|
|
756
|
+
logger: this.ctx.logger.child("supervisor")
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Reconcile now, after a write.
|
|
761
|
+
*
|
|
762
|
+
* The timer would get there on its own, but an operator who presses Save and
|
|
763
|
+
* watches a block sit at `stopped` for fifteen seconds has been told nothing
|
|
764
|
+
* happened. Deliberately NOT awaited by the cap method: converging forks a
|
|
765
|
+
* process, and the write is already durable.
|
|
766
|
+
*/
|
|
767
|
+
convergeSoon() {
|
|
768
|
+
this.supervisor?.converge();
|
|
769
|
+
}
|
|
343
770
|
requireStore() {
|
|
344
771
|
if (this.store === null) throw new Error("core-blocks: not initialized");
|
|
345
772
|
return this.store;
|
|
@@ -371,6 +798,7 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
371
798
|
name: created.name,
|
|
372
799
|
by: caller.userId
|
|
373
800
|
} });
|
|
801
|
+
this.convergeSoon();
|
|
374
802
|
return { block: created };
|
|
375
803
|
},
|
|
376
804
|
update: async ({ blockId, block, caller }) => {
|
|
@@ -380,14 +808,18 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
380
808
|
blockId,
|
|
381
809
|
by: caller.userId
|
|
382
810
|
} });
|
|
811
|
+
this.convergeSoon();
|
|
383
812
|
return { block: updated };
|
|
384
813
|
},
|
|
385
814
|
delete: async ({ blockId }) => {
|
|
386
815
|
await this.requireStore().delete(blockId);
|
|
816
|
+
this.convergeSoon();
|
|
387
817
|
return { success: true };
|
|
388
818
|
},
|
|
389
819
|
setEnabled: async ({ blockId, enabled }) => {
|
|
390
|
-
|
|
820
|
+
const updated = await this.requireStore().update(blockId, { enabled });
|
|
821
|
+
this.convergeSoon();
|
|
822
|
+
return { block: updated };
|
|
391
823
|
},
|
|
392
824
|
compile: async ({ code }) => {
|
|
393
825
|
if (this.transpiler === null) return {
|
|
@@ -407,6 +839,33 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
|
|
|
407
839
|
};
|
|
408
840
|
}
|
|
409
841
|
};
|
|
842
|
+
/**
|
|
843
|
+
* The process plane, over the kernel's cluster broker.
|
|
844
|
+
*
|
|
845
|
+
* `$process.*` is the SAME infrastructure every addon runner uses — no new
|
|
846
|
+
* Moleculer action, no second spawner, no parallel supervisor. That is the
|
|
847
|
+
* whole point: a block gets fork, the UDS `ctx`, `CrashSupervisor` (D6) and
|
|
848
|
+
* `execution.placement` because it is spawned exactly like an addon.
|
|
849
|
+
*/
|
|
850
|
+
function createBrokerProcessHost(broker) {
|
|
851
|
+
return {
|
|
852
|
+
list: async () => {
|
|
853
|
+
return await broker.call("$process.list");
|
|
854
|
+
},
|
|
855
|
+
spawn: async ({ runnerId, addonId, addonDir }) => {
|
|
856
|
+
await broker.call("$process.spawnRunner", {
|
|
857
|
+
runnerId,
|
|
858
|
+
addons: [{
|
|
859
|
+
addonId,
|
|
860
|
+
addonDir
|
|
861
|
+
}]
|
|
862
|
+
});
|
|
863
|
+
},
|
|
864
|
+
stop: async (runnerId) => {
|
|
865
|
+
await broker.call("$process.stop", { name: runnerId });
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
}
|
|
410
869
|
/** Load esbuild lazily. Returns null — never throws — so a node without it
|
|
411
870
|
* still boots, with the compile gate refusing writes rather than the node
|
|
412
871
|
* refusing to start. */
|
|
@@ -421,8 +880,14 @@ async function loadTranspiler(logger) {
|
|
|
421
880
|
}
|
|
422
881
|
}
|
|
423
882
|
//#endregion
|
|
883
|
+
exports.BLOCK_ADDON_PREFIX = BLOCK_ADDON_PREFIX;
|
|
884
|
+
exports.BlockSupervisor = BlockSupervisor;
|
|
424
885
|
exports.CORE_BLOCKS_COLLECTION = CORE_BLOCKS_COLLECTION;
|
|
425
886
|
exports.CoreBlockStore = CoreBlockStore;
|
|
426
887
|
exports.CoreBlocksAddon = CoreBlocksAddon;
|
|
888
|
+
exports.blockAddonId = blockAddonId;
|
|
889
|
+
exports.blockIdFromAddonId = blockIdFromAddonId;
|
|
427
890
|
exports.compileBlock = compileBlock;
|
|
428
891
|
exports.loadBlockTypeDefs = loadBlockTypeDefs;
|
|
892
|
+
exports.materializeBlock = materializeBlock;
|
|
893
|
+
exports.removeBlockPackage = removeBlockPackage;
|