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