@zenbujs/core 0.0.9 → 0.0.13

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.
Files changed (81) hide show
  1. package/dist/advice-config-BiYhyeTz.d.mts +41 -0
  2. package/dist/advice.d.mts +2 -36
  3. package/dist/advice.mjs +2 -2
  4. package/dist/{base-window-BxBZ2md_.mjs → base-window-4P-fVvC_.mjs} +37 -26
  5. package/dist/{build-config-Dzg2frpk.d.mts → build-config-GF0XzR_Y.d.mts} +42 -18
  6. package/dist/{build-config-pWdmLnrk.mjs → build-config-HMMqpXI1.mjs} +0 -8
  7. package/dist/{build-electron-Dsbb1EMl.mjs → build-electron-Di_FE62r.mjs} +10 -6
  8. package/dist/{build-source-d1J3shV8.mjs → build-source-BIaWpaxE.mjs} +2 -2
  9. package/dist/cli/bin.mjs +7 -7
  10. package/dist/cli/build.d.mts +1 -1
  11. package/dist/cli/build.mjs +1 -1
  12. package/dist/cli/resolve-config.mjs +6 -1
  13. package/dist/{cli-kL6mPgBE.mjs → cli-5jFDJWM4.mjs} +4 -4
  14. package/dist/config.d.mts +3 -3
  15. package/dist/config.mjs +2 -2
  16. package/dist/{db-Bc292RYo.mjs → db-MkOccvBS.mjs} +2 -2
  17. package/dist/db.d.mts +3 -2
  18. package/dist/db.mjs +2 -10
  19. package/dist/{dev-B2emj0HZ.mjs → dev-BSDyzO4j.mjs} +3 -9
  20. package/dist/env-bootstrap.d.mts +1 -1
  21. package/dist/events.d.mts +0 -9
  22. package/dist/{host-version-BIrF8tX7.mjs → host-version-Cog_odmD.mjs} +4 -3
  23. package/dist/{index-CVF768Xs.d.mts → index-C0mXKol5.d.mts} +188 -143
  24. package/dist/{index-DeDxePAa.d.mts → index-FaexRVl_.d.mts} +13 -1
  25. package/dist/index.d.mts +4 -4
  26. package/dist/index.mjs +2 -2
  27. package/dist/launcher.mjs +64 -6
  28. package/dist/link-Bt3LB_NW.mjs +586 -0
  29. package/dist/{load-config-C4Oe2qZO.mjs → load-config-C2XloBaQ.mjs} +68 -5
  30. package/dist/node-loader.mjs +1 -1
  31. package/dist/{publish-source-Dq2c0iOw.mjs → publish-source-v93eB9kA.mjs} +6 -2
  32. package/dist/react.d.mts +6 -6
  33. package/dist/react.mjs +4 -4
  34. package/dist/registry-generated.d.mts +19 -14
  35. package/dist/registry-saQDMUhT.d.mts +13 -0
  36. package/dist/registry.d.mts +1 -1
  37. package/dist/{reloader-B22UiNA2.mjs → reloader-CFzxYa67.mjs} +3 -3
  38. package/dist/{renderer-host-DD16MXhI.mjs → renderer-host-Cw38dSDe.mjs} +35 -24
  39. package/dist/{rpc-C4_NQmpT.mjs → rpc-Dg9zwZ33.mjs} +4 -4
  40. package/dist/rpc.d.mts +1 -1
  41. package/dist/rpc.mjs +1 -1
  42. package/dist/runtime-DYUONc3S.mjs +861 -0
  43. package/dist/{runtime-BQWntcOb.d.mts → runtime-fnPDZFYM.d.mts} +100 -3
  44. package/dist/runtime.d.mts +2 -2
  45. package/dist/runtime.mjs +2 -578
  46. package/dist/{schema-CjrMVk36.d.mts → schema-brYpUjYO.d.mts} +13 -25
  47. package/dist/schema.d.mts +2 -2
  48. package/dist/schema.mjs +9 -2
  49. package/dist/{server-CZLMF8Dj.mjs → server-BJ2ZC2z2.mjs} +2 -2
  50. package/dist/services/default.d.mts +1 -5
  51. package/dist/services/default.mjs +12 -16
  52. package/dist/services/index.d.mts +1 -1
  53. package/dist/services/index.mjs +7 -7
  54. package/dist/setup-gate.d.mts +1 -1
  55. package/dist/setup-gate.mjs +20 -12
  56. package/dist/{transport-F2hv_OEm.mjs → transport-Bqlv9pmJ.mjs} +1 -1
  57. package/dist/updater-Bs1Jtem6.mjs +480 -0
  58. package/dist/{vite-plugins-tt6KAtyE.mjs → vite-plugins-Df-cfldF.mjs} +2 -49
  59. package/dist/vite.d.mts +0 -5
  60. package/dist/vite.mjs +1 -1
  61. package/dist/{window-YFKvAM0l.mjs → window-DgB70qeZ.mjs} +113 -22
  62. package/dist/{write-DgIRjo23.mjs → write-7IfKa_nq.mjs} +1 -1
  63. package/dist/zenbu-bg-parse-CIyPkJOY.mjs +46 -0
  64. package/package.json +6 -5
  65. package/dist/advice-config-DXSIo0sg.mjs +0 -154
  66. package/dist/link-glX89NV5.mjs +0 -673
  67. package/dist/registry-CMp8FYgS.d.mts +0 -47
  68. package/dist/updater-BtB_Ki1r.mjs +0 -1011
  69. /package/dist/{config-BK78JDRI.mjs → config-DfciRzDu.mjs} +0 -0
  70. /package/dist/{env-bootstrap-rTs8KR3-.d.mts → env-bootstrap-UBug-4Kw.d.mts} +0 -0
  71. /package/dist/{index-C-ALz_SH.d.mts → index-CSMHYi3u.d.mts} +0 -0
  72. /package/dist/{index-ClXLQ1fw.d.mts → index-DJOHDG5e.d.mts} +0 -0
  73. /package/dist/{log-6rzaCV0I.mjs → log-BkRqDwwB.mjs} +0 -0
  74. /package/dist/{mirror-sync-pYU6f3-c.mjs → mirror-sync-snqh9kEp.mjs} +0 -0
  75. /package/dist/{monorepo-Dct-kkbQ.mjs → monorepo-CBzK3l2i.mjs} +0 -0
  76. /package/dist/{node-BhfLKYCi.mjs → node-BuHlEsE4.mjs} +0 -0
  77. /package/dist/{schema-Ca7SxXgS.mjs → schema-C6k0SroY.mjs} +0 -0
  78. /package/dist/{setup-gate-BQq0QgZH.d.mts → setup-gate-DkysEZQO.d.mts} +0 -0
  79. /package/dist/{src-Cven45mq.mjs → src-BpZAt9zL.mjs} +0 -0
  80. /package/dist/{trace-BaVg0rnY.mjs → trace-BVcQSD59.mjs} +0 -0
  81. /package/dist/{transform-BzrwkEdf.mjs → transform-czrcGnVV.mjs} +0 -0
@@ -0,0 +1,861 @@
1
+ import { n as __exportAll } from "./chunk-DsiFFCwN.mjs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region src/services/advice-config.ts
6
+ var advice_config_exports = /* @__PURE__ */ __exportAll({
7
+ addAdvice: () => addAdvice,
8
+ addContentScript: () => addContentScript,
9
+ getAdvice: () => getAdvice,
10
+ getAllAdviceTypes: () => getAllAdviceTypes,
11
+ getAllContentScriptPaths: () => getAllContentScriptPaths,
12
+ getAllTypes: () => getAllTypes,
13
+ getContentScripts: () => getContentScripts
14
+ });
15
+ const RESOLVED_PREFIX = "\0@advice-prelude/";
16
+ const APP_RENDERER_RELOADER_ID = "app";
17
+ const adviceEntries = /* @__PURE__ */ new Map();
18
+ const contentScripts = /* @__PURE__ */ new Map();
19
+ function resolveAgainstPlugin(modulePath, pluginDir) {
20
+ if (path.isAbsolute(modulePath)) return modulePath;
21
+ return path.resolve(pluginDir, modulePath);
22
+ }
23
+ function invalidatePrelude(type) {
24
+ try {
25
+ const reloader = runtime.getSlot("reloader")?.instance;
26
+ if (!reloader) return;
27
+ const coreEntry = reloader.get(APP_RENDERER_RELOADER_ID);
28
+ if (!coreEntry?.viteServer) return;
29
+ const graph = coreEntry.viteServer.moduleGraph;
30
+ const invalidateMatching = (test) => {
31
+ const ids = [];
32
+ for (const id of graph.idToModuleMap.keys()) {
33
+ if (typeof id !== "string") continue;
34
+ if (test(id)) ids.push(id);
35
+ }
36
+ for (const id of ids) {
37
+ const mod = graph.getModuleById(id);
38
+ if (mod) graph.invalidateModule(mod);
39
+ }
40
+ };
41
+ if (type === "*") invalidateMatching((id) => id.startsWith(RESOLVED_PREFIX));
42
+ else {
43
+ const prefix = RESOLVED_PREFIX + type;
44
+ invalidateMatching((id) => id === prefix || id.startsWith(prefix + "?"));
45
+ }
46
+ } catch {}
47
+ }
48
+ function emitReload(type) {
49
+ invalidatePrelude(type);
50
+ try {
51
+ const rpc = runtime.getSlot("rpc")?.instance;
52
+ if (!rpc) return;
53
+ rpc.emit.core.advice.reload({ type });
54
+ } catch {}
55
+ }
56
+ /**
57
+ * Internal advice registrar. Called by `Service#advise` after the runtime
58
+ * has resolved the calling plugin's root directory from its service slot.
59
+ * User code does not call this directly; use `service.advise({...})`
60
+ * instead.
61
+ */
62
+ function addAdvice(pluginDir, spec) {
63
+ const { view, ...entry } = spec;
64
+ const resolvedEntry = {
65
+ ...entry,
66
+ modulePath: resolveAgainstPlugin(entry.modulePath, pluginDir)
67
+ };
68
+ const list = adviceEntries.get(view) ?? [];
69
+ list.push(resolvedEntry);
70
+ adviceEntries.set(view, list);
71
+ emitReload(view);
72
+ return () => {
73
+ const current = adviceEntries.get(view);
74
+ if (!current) return;
75
+ const idx = current.indexOf(resolvedEntry);
76
+ if (idx >= 0) current.splice(idx, 1);
77
+ if (current.length === 0) adviceEntries.delete(view);
78
+ emitReload(view);
79
+ };
80
+ }
81
+ function getAdvice(type) {
82
+ return adviceEntries.get(type) ?? [];
83
+ }
84
+ function getAllAdviceTypes() {
85
+ return [...adviceEntries.keys()];
86
+ }
87
+ /**
88
+ * Internal content-script registrar. Called by `Service#contentScript`
89
+ * after the runtime has resolved the calling plugin's root directory.
90
+ * User code uses `service.contentScript({...})`.
91
+ */
92
+ function addContentScript(pluginDir, spec) {
93
+ const { view, modulePath } = spec;
94
+ const entry = { path: resolveAgainstPlugin(modulePath, pluginDir) };
95
+ const list = contentScripts.get(view) ?? [];
96
+ list.push(entry);
97
+ contentScripts.set(view, list);
98
+ emitReload(view === "*" ? "*" : view);
99
+ return () => {
100
+ const current = contentScripts.get(view);
101
+ if (!current) return;
102
+ const idx = current.indexOf(entry);
103
+ if (idx >= 0) current.splice(idx, 1);
104
+ if (current.length === 0) contentScripts.delete(view);
105
+ emitReload(view === "*" ? "*" : view);
106
+ };
107
+ }
108
+ function getContentScripts(type) {
109
+ const scoped = (contentScripts.get(type) ?? []).map((e) => e.path);
110
+ return [...type !== "*" ? (contentScripts.get("*") ?? []).map((e) => e.path) : [], ...scoped];
111
+ }
112
+ function getAllContentScriptPaths() {
113
+ const paths = [];
114
+ for (const list of contentScripts.values()) for (const entry of list) paths.push(entry.path);
115
+ return paths;
116
+ }
117
+ function getAllTypes() {
118
+ const types = /* @__PURE__ */ new Set();
119
+ for (const k of adviceEntries.keys()) types.add(k);
120
+ for (const k of contentScripts.keys()) if (k !== "*") types.add(k);
121
+ return [...types];
122
+ }
123
+ //#endregion
124
+ //#region src/runtime.ts
125
+ /**
126
+ * The synthetic plugin name used for everything that ships inside
127
+ * `@zenbujs/core`. Mirrors the `core` DB section so RPC, events, and the
128
+ * database all use the same top-level namespace.
129
+ */
130
+ const CORE_PLUGIN_NAME = "core";
131
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
132
+ function readShutdownTimeoutMs() {
133
+ const raw = process.env.ZENBU_SHUTDOWN_TIMEOUT_MS;
134
+ if (!raw) return DEFAULT_SHUTDOWN_TIMEOUT_MS;
135
+ const value = Number(raw);
136
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_SHUTDOWN_TIMEOUT_MS;
137
+ }
138
+ function resolveDepKey(entry) {
139
+ if (typeof entry === "string") return entry;
140
+ return entry.key;
141
+ }
142
+ var Service = class {
143
+ static deps = {};
144
+ /**
145
+ * Define a Service base class. The returned abstract class has
146
+ * `static key`, `static deps`, and a typed `this.ctx` already set up;
147
+ * extend it and add your `evaluate()` body.
148
+ *
149
+ * export class WindowService extends Service.create({
150
+ * key: "window",
151
+ * deps: {
152
+ * baseWindow: BaseWindowService,
153
+ * http: HttpService,
154
+ * },
155
+ * }) {
156
+ * evaluate() {
157
+ * this.ctx.baseWindow // BaseWindowService
158
+ * this.ctx.http // HttpService
159
+ * }
160
+ * }
161
+ *
162
+ * `key` is a required field on the config object, so TypeScript errors
163
+ * if you forget it. `deps` is optional (defaults to no deps).
164
+ *
165
+ * For dynamic / optional access to another service, use
166
+ * `runtime.get(SomeService, cb)` instead of declaring it in `deps`.
167
+ */
168
+ static create(config) {
169
+ const { key, deps } = config;
170
+ const resolvedDeps = deps ?? {};
171
+ class ConfiguredService extends Service {
172
+ static key = key;
173
+ static deps = resolvedDeps;
174
+ }
175
+ return ConfiguredService;
176
+ }
177
+ ctx;
178
+ /** @internal */
179
+ __setupCleanups = /* @__PURE__ */ new Map();
180
+ evaluate() {}
181
+ /**
182
+ * Register advice (component wrap/replace, function before/after/around)
183
+ * targeting another plugin's exports. The owning plugin's root directory
184
+ * is resolved automatically from this service's slot, so relative
185
+ * `modulePath` values just work.
186
+ *
187
+ * this.setup("wrap-counter", () =>
188
+ * this.advise({
189
+ * view: "app",
190
+ * moduleId: "App.tsx",
191
+ * name: "Counter",
192
+ * type: "around",
193
+ * modulePath: "src/content/wrap-counter.tsx",
194
+ * exportName: "WrapCounter",
195
+ * }),
196
+ * )
197
+ *
198
+ * Returns an unregister function — wrap the call in `this.setup(...)`
199
+ * so cleanup runs on hot reload.
200
+ */
201
+ advise(spec) {
202
+ return addAdvice(this.__getPluginDir("advise"), spec);
203
+ }
204
+ /**
205
+ * Inject a content script into a view. Same plugin-root resolution
206
+ * rules as `advise`.
207
+ *
208
+ * this.setup("inject", () =>
209
+ * this.injectContentScript({
210
+ * view: "*",
211
+ * modulePath: "src/content/toolbar.tsx",
212
+ * }),
213
+ * )
214
+ */
215
+ injectContentScript(spec) {
216
+ return addContentScript(this.__getPluginDir("injectContentScript"), spec);
217
+ }
218
+ /** @internal */
219
+ __getPluginDir(method) {
220
+ const ctor = this.constructor;
221
+ const key = ctor.key;
222
+ const dir = (key ? runtime.getSlot(key) : void 0)?.pluginDir ?? null;
223
+ if (!dir) {
224
+ const label = ctor.name ?? key ?? "<anonymous>";
225
+ throw new Error(`[runtime] Service "${label}" called this.${method}() but its slot has no pluginDir. This service was registered from a file outside any plugin's directory; ${method}() is only valid for services declared inside a plugin.`);
226
+ }
227
+ return dir;
228
+ }
229
+ setup(key, fn) {
230
+ const existing = this.__setupCleanups.get(key);
231
+ if (existing) try {
232
+ existing("reload");
233
+ } catch (e) {
234
+ console.error(`[hot] setup cleanup "${key}" failed:`, e);
235
+ }
236
+ const cleanup = fn();
237
+ if (cleanup) this.__setupCleanups.set(key, cleanup);
238
+ else this.__setupCleanups.delete(key);
239
+ }
240
+ /**
241
+ * Run `fn` and return its result. Historically reported a boot-trace span;
242
+ * now a thin wrapper preserved for caller ergonomics inside service
243
+ * `evaluate()` bodies.
244
+ */
245
+ trace(_name, fn, _meta) {
246
+ return Promise.resolve(fn());
247
+ }
248
+ traceSync(_name, fn, _meta) {
249
+ return fn();
250
+ }
251
+ /** @internal */
252
+ async __cleanupAllSetups(reason = "shutdown") {
253
+ for (const [key, cleanup] of this.__setupCleanups) try {
254
+ await cleanup(reason);
255
+ } catch (e) {
256
+ console.error(`[hot] setup cleanup "${key}" failed:`, e);
257
+ }
258
+ this.__setupCleanups.clear();
259
+ }
260
+ };
261
+ const SERVICE_BASE_METHODS = new Set(Object.getOwnPropertyNames(Service.prototype));
262
+ var ServiceRuntime = class {
263
+ definitions = /* @__PURE__ */ new Map();
264
+ dependentsIndex = /* @__PURE__ */ new Map();
265
+ dirtyKeys = /* @__PURE__ */ new Set();
266
+ drainError = null;
267
+ draining = null;
268
+ registrationTokens = /* @__PURE__ */ new Map();
269
+ slots = /* @__PURE__ */ new Map();
270
+ onReconciledCallbacks = [];
271
+ subscribers = /* @__PURE__ */ new Map();
272
+ register(ServiceClass, importMeta) {
273
+ const hot = importMeta?.hot ?? null;
274
+ const slotKey = ServiceClass.key;
275
+ if (typeof slotKey !== "string" || slotKey.length === 0) {
276
+ const name = ServiceClass.name ?? "<anonymous>";
277
+ throw new Error(`[runtime] service "${name}" is missing \`static key\`. Define it via \`Service.create({ key: "...", ... })\`.`);
278
+ }
279
+ const metaUrl = importMeta?.url;
280
+ const pluginDir = metaUrl ? findOwningPluginDir(metaUrl) : null;
281
+ this.definitions.set(slotKey, ServiceClass);
282
+ const slot = this.slots.get(slotKey);
283
+ if (slot) {
284
+ slot.ServiceClass = ServiceClass;
285
+ slot.pluginDir = pluginDir;
286
+ } else this.slots.set(slotKey, {
287
+ error: null,
288
+ instance: null,
289
+ ServiceClass,
290
+ status: "blocked",
291
+ pluginDir
292
+ });
293
+ this.rebuildDependentsIndex();
294
+ hot?.accept();
295
+ const token = Symbol(slotKey);
296
+ this.registrationTokens.set(slotKey, token);
297
+ hot?.prune?.(() => {
298
+ this.unregister(slotKey, token);
299
+ });
300
+ this.scheduleReconcile([slotKey]);
301
+ }
302
+ getAllKeys() {
303
+ return [...this.slots.keys()];
304
+ }
305
+ getSlot(key) {
306
+ return this.slots.get(key);
307
+ }
308
+ async whenIdle() {
309
+ while (this.draining) await this.draining;
310
+ if (this.drainError) {
311
+ const error = this.drainError;
312
+ this.drainError = null;
313
+ throw error;
314
+ }
315
+ }
316
+ async reloadAll() {
317
+ const keys = [...this.slots.keys()];
318
+ if (keys.length === 0) return;
319
+ await this.scheduleReconcile(keys);
320
+ await this.whenIdle();
321
+ }
322
+ /**
323
+ * Reload a single service by key. No-op if the key is not registered.
324
+ * Used by infrastructure that watches resources outside dynohot's
325
+ * import-graph (e.g. the migrations directory watcher in DbService) and
326
+ * needs to nudge a specific service to re-evaluate without leaning on
327
+ * the devtools-only `__zenbu_dev__.reloadService` hook.
328
+ */
329
+ async reload(key) {
330
+ if (!this.slots.has(key)) return;
331
+ await this.scheduleReconcile([key]);
332
+ await this.whenIdle();
333
+ }
334
+ async shutdown() {
335
+ try {
336
+ await this.whenIdle();
337
+ } catch (error) {
338
+ console.error("[hot] runtime idle wait failed during shutdown:", error);
339
+ }
340
+ const keys = [...this.slots.keys()].reverse();
341
+ for (const key of keys) await this.teardownService(key, { removeSlot: true });
342
+ this.definitions.clear();
343
+ this.dependentsIndex.clear();
344
+ this.dirtyKeys.clear();
345
+ this.registrationTokens.clear();
346
+ }
347
+ /**
348
+ * Subscribe to a service. Behavior-subject style: the callback fires
349
+ * synchronously once with the current value (the live instance if ready,
350
+ * `undefined` otherwise), then again on every reconcile of that service —
351
+ * so you see the new instance after each HMR. Pass through `undefined`
352
+ * when the service tears down or unregisters.
353
+ *
354
+ * Returns an unsubscribe function. Always call it from a `setup()` cleanup
355
+ * (or wherever you'd otherwise leak callbacks across reloads).
356
+ */
357
+ get(ref, cb) {
358
+ const key = ref.key;
359
+ let subs = this.subscribers.get(key);
360
+ if (!subs) {
361
+ subs = /* @__PURE__ */ new Set();
362
+ this.subscribers.set(key, subs);
363
+ }
364
+ const wrapped = cb;
365
+ subs.add(wrapped);
366
+ const slot = this.slots.get(key);
367
+ const current = slot?.status === "ready" && slot.instance ? slot.instance : void 0;
368
+ try {
369
+ cb(current);
370
+ } catch (e) {
371
+ console.error(`[hot] runtime.get subscriber for "${key}" threw:`, e);
372
+ }
373
+ return () => {
374
+ const set = this.subscribers.get(key);
375
+ if (!set) return;
376
+ set.delete(wrapped);
377
+ if (set.size === 0) this.subscribers.delete(key);
378
+ };
379
+ }
380
+ fireSubscribers(key) {
381
+ const subs = this.subscribers.get(key);
382
+ if (!subs || subs.size === 0) return;
383
+ const slot = this.slots.get(key);
384
+ const instance = slot?.status === "ready" && slot.instance ? slot.instance : void 0;
385
+ for (const cb of [...subs]) try {
386
+ cb(instance);
387
+ } catch (e) {
388
+ console.error(`[hot] runtime.get subscriber for "${key}" threw:`, e);
389
+ }
390
+ }
391
+ /**
392
+ * Build a 3-level router keyed by plugin name → service key → method.
393
+ * Mirrors how the database is sectioned (`root.<plugin>.<field>`) so a
394
+ * service's RPC surface lives under its owning plugin's namespace
395
+ * instead of polluting the top level. Core services bucket under
396
+ * `"core"` because the core package is registered as a synthetic
397
+ * plugin (see `getPluginRegistry`).
398
+ */
399
+ buildRouter() {
400
+ const router = {};
401
+ for (const [slotKey, slot] of this.slots) {
402
+ if (slot.status !== "ready" || !slot.instance) continue;
403
+ const proto = Object.getPrototypeOf(slot.instance);
404
+ const methods = {};
405
+ for (const name of Object.getOwnPropertyNames(proto)) {
406
+ if (SERVICE_BASE_METHODS.has(name)) continue;
407
+ if (name.startsWith("_")) continue;
408
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
409
+ if (!desc || typeof desc.value !== "function") continue;
410
+ const instance = slot.instance;
411
+ methods[name] = (...args) => instance[name](...args);
412
+ }
413
+ if (Object.keys(methods).length === 0) continue;
414
+ const pluginName = pluginNameForDir(slot.pluginDir);
415
+ const bucket = router[pluginName] ??= {};
416
+ bucket[slotKey] = methods;
417
+ }
418
+ return router;
419
+ }
420
+ onReconciled(cb) {
421
+ this.onReconciledCallbacks.push(cb);
422
+ return () => {
423
+ const idx = this.onReconciledCallbacks.indexOf(cb);
424
+ if (idx >= 0) this.onReconciledCallbacks.splice(idx, 1);
425
+ };
426
+ }
427
+ resolveDepSlot(depKey) {
428
+ return this.slots.get(depKey);
429
+ }
430
+ injectCtx(instance, ServiceClass) {
431
+ const deps = ServiceClass.deps ?? {};
432
+ const ctx = {};
433
+ for (const [name, entry] of Object.entries(deps)) {
434
+ const key = resolveDepKey(entry);
435
+ const slot = this.resolveDepSlot(key);
436
+ if (!slot || slot.status !== "ready" || !slot.instance) throw new Error(`Dependency "${key}" not ready for "${ServiceClass.key}"`);
437
+ ctx[name] = slot.instance;
438
+ }
439
+ instance.ctx = ctx;
440
+ }
441
+ rebuildDependentsIndex() {
442
+ const next = /* @__PURE__ */ new Map();
443
+ for (const [slotKey, ServiceClass] of this.definitions) {
444
+ const deps = ServiceClass.deps ?? {};
445
+ for (const entry of Object.values(deps)) {
446
+ const depKey = resolveDepKey(entry);
447
+ const dependents = next.get(depKey) ?? /* @__PURE__ */ new Set();
448
+ dependents.add(slotKey);
449
+ next.set(depKey, dependents);
450
+ }
451
+ }
452
+ this.dependentsIndex = next;
453
+ }
454
+ getAffectedKeys(changedKeys) {
455
+ const affected = /* @__PURE__ */ new Set();
456
+ const visit = (key) => {
457
+ if (affected.has(key)) return;
458
+ affected.add(key);
459
+ for (const dependent of this.dependentsIndex.get(key) ?? []) visit(dependent);
460
+ };
461
+ for (const key of changedKeys) visit(key);
462
+ return [...affected].filter((key) => this.definitions.has(key));
463
+ }
464
+ listMissingDeps(ServiceClass) {
465
+ const deps = ServiceClass.deps ?? {};
466
+ const missing = [];
467
+ for (const entry of Object.values(deps)) {
468
+ const key = resolveDepKey(entry);
469
+ const slot = this.resolveDepSlot(key);
470
+ if (!slot || slot.status !== "ready" || !slot.instance) missing.push(key);
471
+ }
472
+ return missing;
473
+ }
474
+ ensureSlot(key, ServiceClass) {
475
+ const existing = this.slots.get(key);
476
+ if (existing) return existing;
477
+ const slot = {
478
+ error: null,
479
+ instance: null,
480
+ ServiceClass,
481
+ status: "blocked",
482
+ pluginDir: null
483
+ };
484
+ this.slots.set(key, slot);
485
+ return slot;
486
+ }
487
+ async teardownService(key, options = {}) {
488
+ const slot = this.slots.get(key);
489
+ if (!slot) return;
490
+ const instance = slot.instance;
491
+ slot.instance = null;
492
+ slot.status = "blocked";
493
+ slot.error = null;
494
+ if (instance) {
495
+ const reason = options.reason ?? "shutdown";
496
+ const timeoutMs = readShutdownTimeoutMs();
497
+ const cleanup = instance.__cleanupAllSetups(reason);
498
+ let timer = null;
499
+ const timeout = new Promise((resolve) => {
500
+ timer = setTimeout(() => {
501
+ console.error(`[hot] ${key} ${reason} cleanup timed out after ${timeoutMs}ms; forcing teardown`);
502
+ resolve();
503
+ }, timeoutMs);
504
+ timer.unref?.();
505
+ });
506
+ try {
507
+ await Promise.race([cleanup, timeout]);
508
+ } finally {
509
+ if (timer) clearTimeout(timer);
510
+ }
511
+ }
512
+ if (options.removeSlot) this.slots.delete(key);
513
+ }
514
+ async unregister(key, token) {
515
+ if (this.registrationTokens.get(key) !== token) return;
516
+ this.registrationTokens.delete(key);
517
+ this.definitions.delete(key);
518
+ await this.teardownService(key, { removeSlot: true });
519
+ this.rebuildDependentsIndex();
520
+ this.fireSubscribers(key);
521
+ await this.scheduleReconcile([key]);
522
+ }
523
+ async scheduleReconcile(keys) {
524
+ for (const key of keys) this.dirtyKeys.add(key);
525
+ if (this.draining) return this.draining;
526
+ this.draining = (async () => {
527
+ try {
528
+ while (this.dirtyKeys.size > 0) {
529
+ const batch = [...this.dirtyKeys];
530
+ this.dirtyKeys.clear();
531
+ await this.reconcileBatch(batch);
532
+ }
533
+ } catch (error) {
534
+ this.drainError = error;
535
+ console.error("[hot] runtime reconcile failed:", error);
536
+ } finally {
537
+ this.draining = null;
538
+ if (this.dirtyKeys.size > 0) this.scheduleReconcile([]);
539
+ }
540
+ })();
541
+ return this.draining;
542
+ }
543
+ async reconcileBatch(changedKeys) {
544
+ const affectedKeys = this.getAffectedKeys(changedKeys);
545
+ if (affectedKeys.length === 0) return;
546
+ const affected = /* @__PURE__ */ new Map();
547
+ for (const key of affectedKeys) {
548
+ const ServiceClass = this.definitions.get(key);
549
+ if (ServiceClass) affected.set(key, ServiceClass);
550
+ }
551
+ const levels = this.topologicalLevels(affected);
552
+ const wasReady = /* @__PURE__ */ new Set();
553
+ for (const key of affectedKeys) if (this.slots.get(key)?.status === "ready") wasReady.add(key);
554
+ for (const level of [...levels].reverse()) await Promise.all(level.map((key) => this.teardownService(key, { reason: "reload" })));
555
+ for (const level of levels) await Promise.all(level.map((key) => this.reconcileKey(key, wasReady)));
556
+ for (const key of affectedKeys) this.fireSubscribers(key);
557
+ if (affectedKeys.length > 0) for (const cb of this.onReconciledCallbacks) try {
558
+ cb(affectedKeys);
559
+ } catch (e) {
560
+ console.error("[hot] onReconciled callback failed:", e);
561
+ }
562
+ }
563
+ async reconcileKey(key, wasReady) {
564
+ const ServiceClass = this.definitions.get(key);
565
+ if (!ServiceClass) return;
566
+ const slot = this.ensureSlot(key, ServiceClass);
567
+ slot.ServiceClass = ServiceClass;
568
+ const missingDeps = this.listMissingDeps(ServiceClass);
569
+ if (missingDeps.length > 0) {
570
+ await this.teardownService(key, { reason: "reload" });
571
+ if (wasReady.has(key)) console.log(`[hot] ${key} waiting on: ${missingDeps.join(", ")}`);
572
+ return;
573
+ }
574
+ const instance = new ServiceClass();
575
+ slot.instance = instance;
576
+ slot.status = "evaluating";
577
+ slot.error = null;
578
+ try {
579
+ this.injectCtx(instance, ServiceClass);
580
+ await instance.evaluate();
581
+ slot.status = "ready";
582
+ } catch (e) {
583
+ slot.status = "failed";
584
+ slot.error = e;
585
+ console.error(`[hot] ${key} failed to evaluate:`, e);
586
+ }
587
+ }
588
+ topologicalLevels(services) {
589
+ const keys = new Set(services.keys());
590
+ const inDegree = /* @__PURE__ */ new Map();
591
+ const dependents = /* @__PURE__ */ new Map();
592
+ for (const key of keys) {
593
+ inDegree.set(key, 0);
594
+ dependents.set(key, []);
595
+ }
596
+ for (const [slotKey, ServiceClass] of services) {
597
+ const deps = ServiceClass.deps ?? {};
598
+ let degree = 0;
599
+ for (const entry of Object.values(deps)) {
600
+ const depKey = resolveDepKey(entry);
601
+ if (keys.has(depKey)) {
602
+ degree++;
603
+ dependents.get(depKey).push(slotKey);
604
+ }
605
+ }
606
+ inDegree.set(slotKey, degree);
607
+ }
608
+ const levels = [];
609
+ let queue = [...keys].filter((k) => inDegree.get(k) === 0);
610
+ while (queue.length > 0) {
611
+ levels.push(queue);
612
+ const next = [];
613
+ for (const key of queue) for (const dep of dependents.get(key)) {
614
+ const d = inDegree.get(dep) - 1;
615
+ inDegree.set(dep, d);
616
+ if (d === 0) next.push(dep);
617
+ }
618
+ queue = next;
619
+ }
620
+ const resolved = levels.flat();
621
+ if (resolved.length !== keys.size) {
622
+ const missing = [...keys].filter((k) => !resolved.includes(k));
623
+ throw new Error(`Circular dependency detected involving: ${missing.join(", ")}`);
624
+ }
625
+ return levels;
626
+ }
627
+ };
628
+ /**
629
+ * fixme: i don't think these commments make sense
630
+ */
631
+ /**
632
+ * Devtools-only handles, attached to the global so they aren't part of the
633
+ * public `ServiceRuntime` autocomplete surface. Same shape React DevTools
634
+ * uses (`__REACT_DEVTOOLS_GLOBAL_HOOK__`): user code reading
635
+ * `runtime.<...>` never lands on these methods; you only find them if you
636
+ * specifically type `globalThis.__zenbu_dev__`. Kept here (next to the
637
+ * runtime that owns the state) because the hook reaches into private
638
+ * implementation details — `scheduleReconcile` stays `private` on the
639
+ * class; the cast lives inside the encapsulation boundary.
640
+ */
641
+ function installDevHook(rt) {
642
+ const internals = rt;
643
+ globalThis.__zenbu_dev__ = { reloadService: async (key) => {
644
+ if (!internals.definitions.has(key)) throw new Error(`No service registered for key "${key}"`);
645
+ await internals.scheduleReconcile([key]);
646
+ await rt.whenIdle();
647
+ } };
648
+ }
649
+ const runtime = (() => {
650
+ const existing = globalThis.__zenbu_service_runtime__;
651
+ if (existing) {
652
+ Object.setPrototypeOf(existing, ServiceRuntime.prototype);
653
+ installDevHook(existing);
654
+ return existing;
655
+ }
656
+ const fresh = new ServiceRuntime();
657
+ globalThis.__zenbu_service_runtime__ = fresh;
658
+ installDevHook(fresh);
659
+ return fresh;
660
+ })();
661
+ function getPluginRegistry() {
662
+ const slot = globalThis;
663
+ if (!slot.__zenbu_plugin_registry__) {
664
+ slot.__zenbu_plugin_registry__ = {
665
+ plugins: /* @__PURE__ */ new Map(),
666
+ appEntrypoint: null,
667
+ splashPath: null,
668
+ subscribers: /* @__PURE__ */ new Set()
669
+ };
670
+ seedCorePlugin(slot.__zenbu_plugin_registry__);
671
+ } else if (!slot.__zenbu_plugin_registry__.subscribers) slot.__zenbu_plugin_registry__.subscribers = /* @__PURE__ */ new Set();
672
+ return slot.__zenbu_plugin_registry__;
673
+ }
674
+ /**
675
+ * Walk up from this module's location until we hit the `@zenbujs/core`
676
+ * package.json. Cached at first call. Same trick as `findCorePackageRoot`
677
+ * in `services/db.ts`; consolidated here so the runtime can register
678
+ * itself without round-tripping through a service file.
679
+ */
680
+ let cachedCorePluginDir = null;
681
+ function getCorePluginDir() {
682
+ if (cachedCorePluginDir) return cachedCorePluginDir;
683
+ const here = path.dirname(fileURLToPath(import.meta.url));
684
+ let dir = here;
685
+ while (dir !== path.dirname(dir)) {
686
+ const pkgPath = path.join(dir, "package.json");
687
+ try {
688
+ if (JSON.parse(fs.readFileSync(pkgPath, "utf8"))?.name === "@zenbujs/core") {
689
+ cachedCorePluginDir = dir;
690
+ return dir;
691
+ }
692
+ } catch {}
693
+ dir = path.dirname(dir);
694
+ }
695
+ cachedCorePluginDir = path.resolve(here, "..");
696
+ return cachedCorePluginDir;
697
+ }
698
+ function seedCorePlugin(reg) {
699
+ if (reg.plugins.has("core")) return;
700
+ reg.plugins.set(CORE_PLUGIN_NAME, {
701
+ name: CORE_PLUGIN_NAME,
702
+ dir: getCorePluginDir(),
703
+ services: []
704
+ });
705
+ }
706
+ /**
707
+ * Resolve the plugin namespace that owns a given service slot's
708
+ * `pluginDir`. Core services (and anything not declared inside a plugin)
709
+ * fall back to `"core"`. Used by `buildRouter` to nest the runtime
710
+ * router by plugin name.
711
+ */
712
+ function pluginNameForDir(dir) {
713
+ if (!dir) return CORE_PLUGIN_NAME;
714
+ for (const plugin of getPluginRegistry().plugins.values()) if (plugin.dir === dir) return plugin.name;
715
+ return CORE_PLUGIN_NAME;
716
+ }
717
+ function snapshotConfig(reg) {
718
+ return {
719
+ plugins: [...reg.plugins.values()],
720
+ appEntrypoint: reg.appEntrypoint,
721
+ splashPath: reg.splashPath
722
+ };
723
+ }
724
+ function notifySubscribers(reg) {
725
+ if (reg.subscribers.size === 0) return;
726
+ const snapshot = snapshotConfig(reg);
727
+ for (const cb of reg.subscribers) try {
728
+ cb(snapshot);
729
+ } catch (err) {
730
+ console.error("[zenbu config subscriber] threw:", err);
731
+ }
732
+ }
733
+ /**
734
+ * Register a plugin's resolved manifest. Idempotent — replaces any existing
735
+ * entry for the same `name`. Called by the loader-emitted barrel; user code
736
+ * normally does not call this directly.
737
+ */
738
+ function registerPlugin(record) {
739
+ const reg = getPluginRegistry();
740
+ if (record.name === "core") {
741
+ console.warn(`[zenbu] plugin name "${CORE_PLUGIN_NAME}" is reserved for the core package; ignoring registerPlugin`);
742
+ return;
743
+ }
744
+ reg.plugins.set(record.name, record);
745
+ notifySubscribers(reg);
746
+ }
747
+ /**
748
+ * Drop a plugin from the registry. Used when the loader regenerates the
749
+ * barrel and needs to clear stale entries.
750
+ */
751
+ function unregisterPlugin(name) {
752
+ const reg = getPluginRegistry();
753
+ if (!reg.plugins.delete(name)) return;
754
+ notifySubscribers(reg);
755
+ }
756
+ /**
757
+ * Replace the entire plugin set in one shot. The loader uses this on every
758
+ * barrel regeneration so removed plugins disappear cleanly. The synthetic
759
+ * `core` plugin is always reseeded after the clear so user records cannot
760
+ * accidentally evict it (and a user record named `"core"` is rejected so
761
+ * the namespace stays unambiguous).
762
+ */
763
+ function replacePlugins(records) {
764
+ const reg = getPluginRegistry();
765
+ reg.plugins.clear();
766
+ seedCorePlugin(reg);
767
+ for (const record of records) {
768
+ if (record.name === "core") {
769
+ console.warn(`[zenbu] plugin name "${CORE_PLUGIN_NAME}" is reserved for the core package; ignoring user plugin record`);
770
+ continue;
771
+ }
772
+ reg.plugins.set(record.name, record);
773
+ }
774
+ notifySubscribers(reg);
775
+ }
776
+ function getPlugins() {
777
+ return [...getPluginRegistry().plugins.values()];
778
+ }
779
+ /**
780
+ * Find the plugin whose `dir` contains the file at `metaUrl`. The plugin
781
+ * registry (populated by the loader-emitted barrel before any service
782
+ * file imports) is the source of truth — no filesystem walking. Returns
783
+ * the plugin's absolute directory, or `null` if the file lives outside
784
+ * every registered plugin (the framework's own `@zenbujs/core/services/*`
785
+ * files take this branch and end up with `slot.pluginDir = null`).
786
+ *
787
+ * Picks the deepest matching plugin so a nested workspace plugin wins
788
+ * over its parent.
789
+ */
790
+ function findOwningPluginDir(metaUrl) {
791
+ let file;
792
+ try {
793
+ file = fileURLToPath(metaUrl);
794
+ } catch {
795
+ return null;
796
+ }
797
+ let bestMatch = null;
798
+ for (const plugin of getPluginRegistry().plugins.values()) {
799
+ const rel = path.relative(plugin.dir, file);
800
+ if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
801
+ const depth = plugin.dir.split(path.sep).length;
802
+ if (!bestMatch || depth > bestMatch.depth) bestMatch = {
803
+ dir: plugin.dir,
804
+ depth
805
+ };
806
+ }
807
+ return bestMatch?.dir ?? null;
808
+ }
809
+ function getPlugin(name) {
810
+ return getPluginRegistry().plugins.get(name);
811
+ }
812
+ /**
813
+ * Set the renderer entrypoint directory + the absolute path to splash.html
814
+ * inside it. Called once by the loader-emitted barrel. Consumers
815
+ * (`view-registry`, `vite-plugins`, `setup-gate`'s splash window) read
816
+ * via `getAppEntrypoint()` / `getSplashPath()`.
817
+ */
818
+ function registerAppEntrypoint(rendererDir, splashPath) {
819
+ const reg = getPluginRegistry();
820
+ reg.appEntrypoint = rendererDir;
821
+ reg.splashPath = splashPath;
822
+ notifySubscribers(reg);
823
+ }
824
+ function getAppEntrypoint() {
825
+ return getPluginRegistry().appEntrypoint;
826
+ }
827
+ function getSplashPath() {
828
+ return getPluginRegistry().splashPath;
829
+ }
830
+ /**
831
+ * Snapshot of the current resolved config — plugins + entrypoints. Cheap;
832
+ * just walks the in-memory registry. The returned object is a fresh copy,
833
+ * safe to retain or pass through serialization boundaries.
834
+ */
835
+ function getConfig() {
836
+ return snapshotConfig(getPluginRegistry());
837
+ }
838
+ /**
839
+ * Subscribe to config changes. The callback fires:
840
+ * - immediately on subscription (with the current snapshot),
841
+ * - after each `replacePlugins(...)` / `registerPlugin(...)` /
842
+ * `registerAppEntrypoint(...)` call — i.e. every time the loader
843
+ * regenerates the plugin barrel after a `zenbu.config.ts` edit.
844
+ *
845
+ * Callback exceptions are logged and swallowed so one buggy subscriber
846
+ * can't break others. The returned function unsubscribes.
847
+ */
848
+ function subscribeConfig(callback) {
849
+ const reg = getPluginRegistry();
850
+ reg.subscribers.add(callback);
851
+ try {
852
+ callback(snapshotConfig(reg));
853
+ } catch (err) {
854
+ console.error("[zenbu config subscriber] initial fire threw:", err);
855
+ }
856
+ return () => {
857
+ reg.subscribers.delete(callback);
858
+ };
859
+ }
860
+ //#endregion
861
+ export { getAllAdviceTypes as _, getConfig as a, getContentScripts as b, getSplashPath as c, replacePlugins as d, runtime as f, getAdvice as g, advice_config_exports as h, getAppEntrypoint as i, registerAppEntrypoint as l, unregisterPlugin as m, Service as n, getPlugin as o, subscribeConfig as p, ServiceRuntime as r, getPlugins as s, CORE_PLUGIN_NAME as t, registerPlugin as u, getAllContentScriptPaths as v, getAllTypes as y };