@cordy/electro 1.0.0

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/index.mjs ADDED
@@ -0,0 +1,1592 @@
1
+ import { createRequire } from "node:module";
2
+ import { join } from "node:path";
3
+ import { Cron } from "croner";
4
+
5
+ //#region \0rolldown/runtime.js
6
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
7
+
8
+ //#endregion
9
+ //#region src/config/define-config.ts
10
+ function defineConfig(input) {
11
+ const windows = input.windows ?? [];
12
+ const seen = /* @__PURE__ */ new Set();
13
+ for (const win of windows) {
14
+ if (seen.has(win.name)) throw new Error(`[electro] defineConfig: duplicate window name "${win.name}"`);
15
+ seen.add(win.name);
16
+ }
17
+ return {
18
+ runtime: input.runtime,
19
+ windows
20
+ };
21
+ }
22
+
23
+ //#endregion
24
+ //#region src/config/caller.ts
25
+ /** Resolve the file path of the caller (skipping electro config internals). */
26
+ function getCallerPath() {
27
+ const stack = (/* @__PURE__ */ new Error()).stack;
28
+ if (!stack) return void 0;
29
+ const lines = stack.split("\n");
30
+ for (const line of lines) {
31
+ const match = line.match(/(?:at\s+.*\()?((?:file:\/\/)?[^\s)]+):\d+:\d+\)?/);
32
+ if (!match) continue;
33
+ const rawPath = match[1];
34
+ if (rawPath.includes("/src/config/") || rawPath.includes("/dist/config") || rawPath.includes("@cordy/electro") || rawPath.includes("/packages/electro/dist/")) continue;
35
+ if (rawPath.startsWith("file://")) try {
36
+ return decodeURIComponent(new URL(rawPath).pathname);
37
+ } catch {
38
+ return rawPath;
39
+ }
40
+ return rawPath;
41
+ }
42
+ }
43
+
44
+ //#endregion
45
+ //#region src/config/define-runtime.ts
46
+ function defineRuntime(input) {
47
+ if (!input.entry || input.entry.trim().length === 0) throw new Error("[electro] defineRuntime: entry must be a non-empty string");
48
+ return {
49
+ entry: input.entry,
50
+ vite: input.vite,
51
+ __source: getCallerPath() ?? ""
52
+ };
53
+ }
54
+
55
+ //#endregion
56
+ //#region src/config/define-window.ts
57
+ const WINDOW_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
58
+ function defineWindow(input) {
59
+ if (!input.name || input.name.trim().length === 0) throw new Error("[electro] defineWindow: name must be a non-empty string");
60
+ if (!WINDOW_NAME_PATTERN.test(input.name)) throw new Error(`[electro] defineWindow: name "${input.name}" is invalid. Must match ${WINDOW_NAME_PATTERN.toString()}`);
61
+ if (!input.entry || input.entry.trim().length === 0) throw new Error("[electro] defineWindow: entry must be a non-empty string");
62
+ const lifecycle = input.lifecycle ?? "singleton";
63
+ const close = input.behavior?.close ?? (lifecycle === "multi" ? "destroy" : "hide");
64
+ if (lifecycle === "multi" && close === "hide") throw new Error("[electro] defineWindow: behavior.close \"hide\" is only allowed with lifecycle \"singleton\". Multi-instance windows must use \"destroy\".");
65
+ return {
66
+ name: input.name,
67
+ entry: input.entry,
68
+ type: input.type,
69
+ features: input.features,
70
+ vite: input.vite,
71
+ preload: input.preload,
72
+ lifecycle,
73
+ autoShow: input.autoShow ?? false,
74
+ behavior: { close },
75
+ window: input.window,
76
+ __source: getCallerPath() ?? ""
77
+ };
78
+ }
79
+
80
+ //#endregion
81
+ //#region src/core/event-bus/accessor.ts
82
+ /**
83
+ * Scoped event access per feature.
84
+ *
85
+ * - `publish("name", payload)` -> publishes as `"ownerId:name"`
86
+ * - `on("dep:name", handler)` -> validates `dep` is a declared dependency
87
+ * - `on("name", handler)` -> subscribes to own `"ownerId:name"`
88
+ */
89
+ var EventAccessor = class {
90
+ constructor(bus, ownerId, declaredDeps) {
91
+ this.bus = bus;
92
+ this.ownerId = ownerId;
93
+ this.declaredDeps = declaredDeps;
94
+ }
95
+ publish(event, payload) {
96
+ this.bus.publish(`${this.ownerId}:${event}`, payload);
97
+ }
98
+ on(event, handler) {
99
+ const colonIdx = event.indexOf(":");
100
+ if (colonIdx === -1) return this.bus.subscribe(`${this.ownerId}:${event}`, handler, this.ownerId);
101
+ const depId = event.slice(0, colonIdx);
102
+ if (!this.declaredDeps.has(depId)) throw new Error(`Feature "${this.ownerId}" cannot subscribe to "${event}": "${depId}" is not a declared dependency`);
103
+ return this.bus.subscribe(event, handler, this.ownerId);
104
+ }
105
+ };
106
+
107
+ //#endregion
108
+ //#region src/core/event-bus/helpers.ts
109
+ function createEvent(id, defaults) {
110
+ if (!id || id.trim().length === 0) throw new Error("createEvent: id is required");
111
+ return {
112
+ id,
113
+ defaults,
114
+ payload() {
115
+ throw new Error("phantom method — not callable");
116
+ }
117
+ };
118
+ }
119
+
120
+ //#endregion
121
+ //#region src/core/feature/enums.ts
122
+ let FeatureStatus = /* @__PURE__ */ function(FeatureStatus) {
123
+ FeatureStatus["NONE"] = "none";
124
+ FeatureStatus["REGISTERED"] = "registered";
125
+ FeatureStatus["INITIALIZING"] = "initializing";
126
+ FeatureStatus["READY"] = "ready";
127
+ FeatureStatus["ACTIVATING"] = "activating";
128
+ FeatureStatus["ACTIVATED"] = "activated";
129
+ FeatureStatus["DEACTIVATING"] = "deactivating";
130
+ FeatureStatus["DEACTIVATED"] = "deactivated";
131
+ FeatureStatus["DESTROYING"] = "destroying";
132
+ FeatureStatus["DESTROYED"] = "destroyed";
133
+ FeatureStatus["ERROR"] = "error";
134
+ return FeatureStatus;
135
+ }({});
136
+
137
+ //#endregion
138
+ //#region src/core/feature/helpers.ts
139
+ /**
140
+ * Method for creating a feature
141
+ * @param config - The configuration object for the feature
142
+ * @returns The feature configuration object
143
+ */
144
+ function createFeature(config) {
145
+ if (!config.id) throw new Error("Feature must have an id");
146
+ return config;
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/core/runtime/enums.ts
151
+ let RuntimeState = /* @__PURE__ */ function(RuntimeState) {
152
+ RuntimeState["CREATED"] = "created";
153
+ RuntimeState["STARTING"] = "starting";
154
+ RuntimeState["RUNNING"] = "running";
155
+ RuntimeState["STOPPING"] = "stopping";
156
+ RuntimeState["STOPPED"] = "stopped";
157
+ RuntimeState["FAILED"] = "failed";
158
+ return RuntimeState;
159
+ }({});
160
+
161
+ //#endregion
162
+ //#region src/window/default-factory.ts
163
+ /**
164
+ * Default WindowFactory — creates Electron BrowserWindow or BaseWindow
165
+ * from a WindowDefinition at runtime.
166
+ */
167
+ function createDefaultWindowFactory() {
168
+ return { create(definition) {
169
+ if (definition.type === "browser-window") {
170
+ const { BrowserWindow } = __require("electron");
171
+ return new BrowserWindow({
172
+ show: definition.autoShow ?? false,
173
+ ...definition.window ?? {}
174
+ });
175
+ }
176
+ const { BaseWindow: BW } = __require("electron");
177
+ return new BW({
178
+ show: definition.autoShow ?? false,
179
+ ...definition.window ?? {}
180
+ });
181
+ } };
182
+ }
183
+
184
+ //#endregion
185
+ //#region src/window/manager.ts
186
+ /**
187
+ * WindowManager — registry, creation, and lifecycle coordinator for windows.
188
+ *
189
+ * Manages window definitions and their instances through a WindowFactory
190
+ * abstraction. Enforces singleton/multi lifecycle semantics and provides
191
+ * querying and bulk destruction capabilities.
192
+ */
193
+ var WindowManager = class {
194
+ factory;
195
+ definitions = /* @__PURE__ */ new Map();
196
+ instances = /* @__PURE__ */ new Map();
197
+ constructor(factory) {
198
+ this.factory = factory;
199
+ }
200
+ /** Register a window definition. Throws on duplicate name. */
201
+ registerDefinition(definition) {
202
+ if (this.definitions.has(definition.name)) throw new Error(`Window definition duplicate: "${definition.name}" is already registered`);
203
+ this.definitions.set(definition.name, definition);
204
+ }
205
+ /** Check whether a definition is registered for the given name. */
206
+ hasDefinition(name) {
207
+ return this.definitions.has(name);
208
+ }
209
+ /**
210
+ * Create a window from a registered definition.
211
+ *
212
+ * - Throws if no definition exists for `name`.
213
+ * - For singleton lifecycle (the default), throws if a live instance already exists.
214
+ * - For multi lifecycle, always creates a new instance.
215
+ */
216
+ createWindow(name) {
217
+ const definition = this.definitions.get(name);
218
+ if (!definition) throw new Error(`Cannot create window: no definition found for "${name}"`);
219
+ if ((definition.lifecycle ?? "singleton") === "singleton") {
220
+ if (this.getAliveInstances(name).length > 0) throw new Error(`Cannot create window "${name}": singleton instance already exists`);
221
+ }
222
+ const window = this.factory.create(definition);
223
+ const electrified = window;
224
+ electrified.load = async () => {
225
+ if (typeof window.loadURL !== "function") throw new Error(`Cannot load window "${name}": BaseWindow does not support loadURL/loadFile. Set type: "browser-window" in your window definition.`);
226
+ const bw = window;
227
+ const devUrl = process.env[`ELECTRO_DEV_URL_${name}`];
228
+ if (devUrl) {
229
+ await bw.loadURL(devUrl);
230
+ return;
231
+ }
232
+ const { app } = await import("electron");
233
+ await bw.loadFile(join(app.getAppPath(), ".electro", "out", "renderer", name, "index.html"));
234
+ };
235
+ const list = this.instances.get(name);
236
+ if (list) list.push(electrified);
237
+ else this.instances.set(name, [electrified]);
238
+ return electrified;
239
+ }
240
+ /**
241
+ * Get the first alive window for the given name, or null.
242
+ * Cleans up destroyed instances as a side effect.
243
+ */
244
+ getWindow(name) {
245
+ const list = this.instances.get(name);
246
+ if (!list) return null;
247
+ const alive = list.filter((w) => !w.isDestroyed());
248
+ this.instances.set(name, alive);
249
+ return alive.length > 0 ? alive[0] : null;
250
+ }
251
+ /** Destroy all instances for the given name. No-op if name is unknown. */
252
+ destroyWindow(name) {
253
+ const list = this.instances.get(name);
254
+ if (!list) return;
255
+ for (const window of list) if (!window.isDestroyed()) window.destroy();
256
+ this.instances.set(name, []);
257
+ }
258
+ /** Destroy all tracked windows across all names. */
259
+ destroyAll() {
260
+ for (const [name] of this.instances) this.destroyWindow(name);
261
+ }
262
+ /** Returns a snapshot of all tracked windows (including destroyed ones). */
263
+ list() {
264
+ const result = [];
265
+ for (const [name, windows] of this.instances) for (const window of windows) result.push({
266
+ name,
267
+ windowId: window.id,
268
+ destroyed: window.isDestroyed()
269
+ });
270
+ return result;
271
+ }
272
+ /** Return alive (non-destroyed) instances for a given name. */
273
+ getAliveInstances(name) {
274
+ const list = this.instances.get(name);
275
+ if (!list) return [];
276
+ return list.filter((w) => !w.isDestroyed());
277
+ }
278
+ };
279
+
280
+ //#endregion
281
+ //#region src/core/event-bus/event-bus.ts
282
+ var EventBus = class {
283
+ subscriptions = /* @__PURE__ */ new Map();
284
+ publish(channel, payload) {
285
+ const subs = this.subscriptions.get(channel);
286
+ if (!subs) return;
287
+ for (const sub of subs) sub.handler(payload);
288
+ }
289
+ subscribe(channel, handler, ownerId) {
290
+ const sub = {
291
+ channel,
292
+ handler,
293
+ ownerId
294
+ };
295
+ let channelSubs = this.subscriptions.get(channel);
296
+ if (!channelSubs) {
297
+ channelSubs = /* @__PURE__ */ new Set();
298
+ this.subscriptions.set(channel, channelSubs);
299
+ }
300
+ channelSubs.add(sub);
301
+ return () => {
302
+ channelSubs.delete(sub);
303
+ if (channelSubs.size === 0) this.subscriptions.delete(channel);
304
+ };
305
+ }
306
+ removeByOwner(ownerId) {
307
+ for (const [channel, subs] of this.subscriptions) {
308
+ for (const sub of subs) if (sub.ownerId === ownerId) subs.delete(sub);
309
+ if (subs.size === 0) this.subscriptions.delete(channel);
310
+ }
311
+ }
312
+ };
313
+
314
+ //#endregion
315
+ //#region src/window/accessor.ts
316
+ /**
317
+ * Thin accessor bound to FeatureContext.
318
+ *
319
+ * Provides `createWindow(name)` and `getWindow(name)` for features.
320
+ * Delegates all work to WindowManager.
321
+ */
322
+ var WindowAccessor = class {
323
+ constructor(manager) {
324
+ this.manager = manager;
325
+ }
326
+ createWindow(name) {
327
+ return this.manager.createWindow(name);
328
+ }
329
+ getWindow(name) {
330
+ return this.manager.getWindow(name);
331
+ }
332
+ };
333
+
334
+ //#endregion
335
+ //#region src/core/service/enums.ts
336
+ let ServiceScope = /* @__PURE__ */ function(ServiceScope) {
337
+ ServiceScope["PRIVATE"] = "private";
338
+ ServiceScope["INTERNAL"] = "internal";
339
+ ServiceScope["EXPOSED"] = "exposed";
340
+ return ServiceScope;
341
+ }({});
342
+ let ServiceStatus = /* @__PURE__ */ function(ServiceStatus) {
343
+ ServiceStatus["REGISTERED"] = "registered";
344
+ ServiceStatus["ACTIVE"] = "active";
345
+ ServiceStatus["DESTROYED"] = "destroyed";
346
+ return ServiceStatus;
347
+ }({});
348
+
349
+ //#endregion
350
+ //#region src/core/service/accessor.ts
351
+ /**
352
+ * Implements `ctx.getService(name)` with scope-aware access control.
353
+ *
354
+ * - `"serviceId"` — own-feature lookup, all scopes visible, returns API directly.
355
+ * - `"featureId:serviceId"` — cross-feature lookup, only INTERNAL + EXPOSED visible.
356
+ */
357
+ var ServiceAccessor = class {
358
+ constructor(own, deps) {
359
+ this.own = own;
360
+ this.deps = deps;
361
+ }
362
+ /**
363
+ * Resolve a service by name.
364
+ *
365
+ * @param name Either `"serviceId"` (own feature) or `"featureId:serviceId"` (cross-feature).
366
+ * @returns The service API directly.
367
+ * @throws If the service or feature is not found, or scope is not accessible.
368
+ */
369
+ get(name) {
370
+ const colonIdx = name.indexOf(":");
371
+ if (colonIdx === -1) return this.resolveOwn(name);
372
+ const featureId = name.slice(0, colonIdx);
373
+ const serviceId = name.slice(colonIdx + 1);
374
+ return this.resolveDep(featureId, serviceId);
375
+ }
376
+ resolveOwn(serviceId) {
377
+ const result = this.own.get(serviceId);
378
+ if (!result) throw new Error(`Service "${serviceId}" not found in own feature`);
379
+ return result.api;
380
+ }
381
+ resolveDep(featureId, serviceId) {
382
+ const manager = this.deps.get(featureId);
383
+ if (!manager) throw new Error(`Feature "${featureId}" is not a declared dependency`);
384
+ const result = manager.get(serviceId);
385
+ if (!result) throw new Error(`Service "${serviceId}" not found in feature "${featureId}"`);
386
+ if (result.scope === ServiceScope.PRIVATE) throw new Error(`Service "${serviceId}" in feature "${featureId}" is private and not accessible cross-feature`);
387
+ return result.api;
388
+ }
389
+ };
390
+
391
+ //#endregion
392
+ //#region src/core/service/manager.ts
393
+ /**
394
+ * Per-feature registry and lifecycle coordinator for {@link Service} instances.
395
+ *
396
+ * One service per id — `get()` returns `{ api, scope }` directly.
397
+ */
398
+ var ServiceManager = class {
399
+ /** Primary storage: `id` → Service */
400
+ services = /* @__PURE__ */ new Map();
401
+ _isShutdown = false;
402
+ constructor(ctx) {
403
+ this.ctx = ctx;
404
+ }
405
+ /** Add a service to the registry. Throws on duplicate id. */
406
+ register(service) {
407
+ if (this.services.has(service.id)) throw new Error(`Duplicate service: "${service.id}"`);
408
+ this.services.set(service.id, service);
409
+ }
410
+ /** Remove a service by id. Destroys it before removing. */
411
+ unregister(serviceId) {
412
+ const service = this.services.get(serviceId);
413
+ if (!service) return;
414
+ service.destroy();
415
+ this.services.delete(serviceId);
416
+ }
417
+ /** Build all registered services. No-op after shutdown. */
418
+ startup() {
419
+ if (this._isShutdown) return;
420
+ for (const service of this.services.values()) service.build(this.ctx);
421
+ }
422
+ /** Destroy all registered services. Marks manager as shut down. */
423
+ shutdown() {
424
+ this._isShutdown = true;
425
+ for (const service of this.services.values()) service.destroy();
426
+ }
427
+ /**
428
+ * Get a service by id, returning its API and scope.
429
+ *
430
+ * @returns `{ api, scope }` or `null` if service id is unknown or not yet built
431
+ */
432
+ get(serviceId) {
433
+ const service = this.services.get(serviceId);
434
+ if (!service) return null;
435
+ const api = service.api();
436
+ if (api == null) return null;
437
+ return {
438
+ api,
439
+ scope: service.scope
440
+ };
441
+ }
442
+ /** Return status for a given service id. Throws if unknown. */
443
+ status(serviceId) {
444
+ const service = this.services.get(serviceId);
445
+ if (!service) throw new Error(`Service "${serviceId}" not found`);
446
+ return service.status();
447
+ }
448
+ /** Return status for all registered services. */
449
+ list() {
450
+ return Array.from(this.services.values()).map((s) => s.status());
451
+ }
452
+ };
453
+
454
+ //#endregion
455
+ //#region src/core/state-machine/state-machine.ts
456
+ var StateMachine = class {
457
+ _current;
458
+ _transitions;
459
+ _name;
460
+ _listeners = /* @__PURE__ */ new Set();
461
+ constructor(config) {
462
+ this._current = config.initial;
463
+ this._transitions = config.transitions;
464
+ this._name = config.name ?? "StateMachine";
465
+ }
466
+ get current() {
467
+ return this._current;
468
+ }
469
+ transition(target) {
470
+ const allowed = this._transitions[this._current];
471
+ if (!allowed || !allowed.includes(target)) throw new Error(`Illegal transition: "${this._current}" \u2192 "${target}" for "${this._name}"`);
472
+ const from = this._current;
473
+ this._current = target;
474
+ for (const listener of this._listeners) listener(from, target);
475
+ }
476
+ canTransition(target) {
477
+ const allowed = this._transitions[this._current];
478
+ return !!allowed && allowed.includes(target);
479
+ }
480
+ assertState(...allowed) {
481
+ if (!allowed.includes(this._current)) {
482
+ const list = allowed.map((s) => `"${s}"`).join(", ");
483
+ throw new Error(`"${this._name}" expected state ${list}, but current is "${this._current}"`);
484
+ }
485
+ }
486
+ onTransition(cb) {
487
+ this._listeners.add(cb);
488
+ return () => {
489
+ this._listeners.delete(cb);
490
+ };
491
+ }
492
+ };
493
+
494
+ //#endregion
495
+ //#region src/core/task/handle.ts
496
+ /**
497
+ * Ergonomic handle for a single {@link Task}.
498
+ *
499
+ * Provides per-task control: start, queue, stop, enable, disable, clear, status.
500
+ * Created by Feature and bound to `ctx.getTask(name)`.
501
+ */
502
+ var TaskHandle = class {
503
+ constructor(task, ctx = void 0) {
504
+ this.task = task;
505
+ this.ctx = ctx;
506
+ }
507
+ /** Execute immediately (respects overlap policy). */
508
+ async start(payload) {
509
+ await this.task.start(payload);
510
+ }
511
+ /** Push payload to FIFO queue, processes sequentially. */
512
+ queue(payload) {
513
+ this.task.queue(payload);
514
+ }
515
+ /** Abort current execution. Queue continues processing. */
516
+ stop() {
517
+ this.task.stop();
518
+ }
519
+ /** Re-enable the task (cron, ready for start/queue). */
520
+ enable() {
521
+ if (this.ctx) this.task.enable(this.ctx);
522
+ }
523
+ /** Abort current + clear queue + stop cron. */
524
+ disable(mode) {
525
+ this.task.disable(mode);
526
+ }
527
+ /** Clear the FIFO queue without stopping current execution. */
528
+ clear() {
529
+ this.task.clear();
530
+ }
531
+ /** Snapshot of the task's current state. */
532
+ status() {
533
+ return this.task.status();
534
+ }
535
+ };
536
+
537
+ //#endregion
538
+ //#region src/core/task/manager.ts
539
+ /**
540
+ * Registry and lifecycle coordinator for {@link Task} instances.
541
+ *
542
+ * Manages a flat collection of tasks: registration, bulk startup/shutdown,
543
+ * individual enable/disable, manual execution, and status queries.
544
+ *
545
+ * Does **not** own scheduling or retry — those belong to {@link Task}.
546
+ */
547
+ var TaskManager = class {
548
+ tasks = /* @__PURE__ */ new Map();
549
+ _isShutdown = false;
550
+ constructor(ctx) {
551
+ this.ctx = ctx;
552
+ }
553
+ /** Add a task to the registry. Throws on duplicate `task.id`. */
554
+ register(task) {
555
+ if (this.tasks.has(task.id)) throw new Error(`Duplicate task id: "${task.id}"`);
556
+ this.tasks.set(task.id, task);
557
+ }
558
+ /** Remove a task, force-disabling it first. No-op for unknown ids. */
559
+ unregister(taskId) {
560
+ const task = this.tasks.get(taskId);
561
+ if (!task) return;
562
+ task.disable("force");
563
+ this.tasks.delete(taskId);
564
+ }
565
+ /** Enable all registered tasks. No-op after {@link shutdown}. */
566
+ startup() {
567
+ if (this._isShutdown) return;
568
+ for (const task of this.tasks.values()) task.enable(this.ctx);
569
+ }
570
+ /** Disable all registered tasks. Marks the manager as shut down (startup becomes a no-op). */
571
+ shutdown(mode = "graceful") {
572
+ this._isShutdown = true;
573
+ for (const task of this.tasks.values()) task.disable(mode);
574
+ }
575
+ /** Enable a single task by id. Throws if the task is not registered. */
576
+ enable(taskId) {
577
+ this.getTaskInstance(taskId).enable(this.ctx);
578
+ }
579
+ /** Disable a single task by id. Throws if the task is not registered. */
580
+ disable(taskId, mode = "graceful") {
581
+ this.getTaskInstance(taskId).disable(mode);
582
+ }
583
+ /** Trigger a manual execution of the task. Throws if the task is not registered. */
584
+ async start(taskId, payload) {
585
+ await this.getTaskInstance(taskId).start(payload);
586
+ }
587
+ /** Return the current status snapshot for a single task. */
588
+ status(taskId) {
589
+ return this.getTaskInstance(taskId).status();
590
+ }
591
+ /** Return status snapshots for all registered tasks. */
592
+ list() {
593
+ return Array.from(this.tasks.values()).map((t) => t.status());
594
+ }
595
+ /** Return the raw Task instance. Throws if not registered. */
596
+ getTaskInstance(taskId) {
597
+ const task = this.tasks.get(taskId);
598
+ if (!task) throw new Error(`Task "${taskId}" not found`);
599
+ return task;
600
+ }
601
+ };
602
+
603
+ //#endregion
604
+ //#region src/core/feature/handle.ts
605
+ /**
606
+ * Ergonomic handle for a declared dependency Feature.
607
+ *
608
+ * Provides per-feature control: status, enable, disable.
609
+ * Created by Feature and bound to `ctx.getFeature(name)`.
610
+ */
611
+ var FeatureHandle = class {
612
+ constructor(feature, manager) {
613
+ this.feature = feature;
614
+ this.manager = manager;
615
+ }
616
+ /** Current lifecycle state. */
617
+ status() {
618
+ return this.feature.status;
619
+ }
620
+ /** Re-activate the feature (from DEACTIVATED or ERROR). */
621
+ async enable() {
622
+ await this.manager.enable(this.feature.id);
623
+ }
624
+ /** Deactivate the feature. */
625
+ async disable() {
626
+ await this.manager.disable(this.feature.id);
627
+ }
628
+ };
629
+
630
+ //#endregion
631
+ //#region src/core/feature/feature.ts
632
+ /** Allowed state transitions for the Feature FSM. */
633
+ const TRANSITIONS = {
634
+ [FeatureStatus.NONE]: [FeatureStatus.REGISTERED],
635
+ [FeatureStatus.REGISTERED]: [FeatureStatus.INITIALIZING],
636
+ [FeatureStatus.INITIALIZING]: [FeatureStatus.READY, FeatureStatus.ERROR],
637
+ [FeatureStatus.READY]: [FeatureStatus.ACTIVATING],
638
+ [FeatureStatus.ACTIVATING]: [FeatureStatus.ACTIVATED, FeatureStatus.ERROR],
639
+ [FeatureStatus.ACTIVATED]: [FeatureStatus.DEACTIVATING],
640
+ [FeatureStatus.DEACTIVATING]: [FeatureStatus.DEACTIVATED, FeatureStatus.ERROR],
641
+ [FeatureStatus.DEACTIVATED]: [FeatureStatus.ACTIVATING, FeatureStatus.DESTROYING],
642
+ [FeatureStatus.DESTROYING]: [FeatureStatus.DESTROYED, FeatureStatus.ERROR],
643
+ [FeatureStatus.DESTROYED]: [],
644
+ [FeatureStatus.ERROR]: [FeatureStatus.ACTIVATING, FeatureStatus.DESTROYING]
645
+ };
646
+ var Feature = class {
647
+ state;
648
+ controller = new AbortController();
649
+ context;
650
+ serviceManager = null;
651
+ taskManager = null;
652
+ eventBus = null;
653
+ constructor(config, logger) {
654
+ this.config = config;
655
+ this.logger = logger;
656
+ this.state = new StateMachine({
657
+ transitions: TRANSITIONS,
658
+ initial: FeatureStatus.NONE,
659
+ name: `feature "${config.id}"`
660
+ });
661
+ this.context = {
662
+ getService: () => {
663
+ throw new Error("Services not yet initialized");
664
+ },
665
+ getTask: () => {
666
+ throw new Error("Tasks not yet initialized");
667
+ },
668
+ getFeature: () => {
669
+ throw new Error("Features not yet initialized");
670
+ },
671
+ events: {
672
+ publish: () => {
673
+ throw new Error("Events not yet initialized");
674
+ },
675
+ on: () => {
676
+ throw new Error("Events not yet initialized");
677
+ }
678
+ },
679
+ createWindow: () => {
680
+ throw new Error("Window manager not available");
681
+ },
682
+ getWindow: () => {
683
+ throw new Error("Window manager not available");
684
+ },
685
+ logger: this.logger,
686
+ signal: this.controller.signal
687
+ };
688
+ }
689
+ get id() {
690
+ return this.config.id;
691
+ }
692
+ get status() {
693
+ return this.state.current;
694
+ }
695
+ /**
696
+ * Validate and apply an FSM transition.
697
+ * @throws If the transition is not allowed from the current state.
698
+ */
699
+ transition(target) {
700
+ this.state.transition(target);
701
+ }
702
+ async initialize(features, manager, eventBus, windowManager) {
703
+ this.buildContext(features, manager, eventBus, windowManager);
704
+ await this.config.onInitialize?.(this.context);
705
+ }
706
+ async activate() {
707
+ await this.config.onActivate?.(this.context);
708
+ this.taskManager?.startup();
709
+ }
710
+ async deactivate() {
711
+ if (this.eventBus) this.eventBus.removeByOwner(this.id);
712
+ this.taskManager?.shutdown();
713
+ if (this.serviceManager) this.serviceManager.shutdown();
714
+ await this.config.onDeactivate?.(this.context);
715
+ }
716
+ async destroy() {
717
+ await this.config.onDestroy?.(this.context);
718
+ }
719
+ buildContext(features, manager, eventBus, windowManager) {
720
+ this.context.signal = this.controller.signal;
721
+ this.context.logger = this.logger;
722
+ this.serviceManager = new ServiceManager(this.context);
723
+ for (const service of this.config.services ?? []) this.serviceManager.register(service);
724
+ this.serviceManager.startup();
725
+ const deps = /* @__PURE__ */ new Map();
726
+ for (const dep of features) if (dep.serviceManager) deps.set(dep.id, dep.serviceManager);
727
+ const accessor = new ServiceAccessor(this.serviceManager, deps);
728
+ this.context.getService = ((name) => accessor.get(name));
729
+ this.taskManager = new TaskManager(this.context);
730
+ for (const task of this.config.tasks ?? []) this.taskManager.register(task);
731
+ this.context.getTask = ((name) => {
732
+ return new TaskHandle(this.taskManager.getTaskInstance(name), this.context);
733
+ });
734
+ const declaredDeps = new Set(this.config.dependencies ?? []);
735
+ this.context.getFeature = ((name) => {
736
+ if (!declaredDeps.has(name)) throw new Error(`Feature "${name}" is not a declared dependency of "${this.id}"`);
737
+ const dep = features.find((f) => f.id === name);
738
+ if (!dep) throw new Error(`Feature "${name}" not found`);
739
+ return new FeatureHandle(dep, manager);
740
+ });
741
+ if (eventBus) {
742
+ this.eventBus = eventBus;
743
+ const eventAccessor = new EventAccessor(eventBus, this.id, declaredDeps);
744
+ const eventDefaults = /* @__PURE__ */ new Map();
745
+ for (const evt of this.config.events ?? []) if (evt.defaults !== void 0) eventDefaults.set(evt.id, evt.defaults);
746
+ this.context.events = {
747
+ publish: (event, payload) => {
748
+ const resolved = payload ?? eventDefaults.get(event);
749
+ eventAccessor.publish(event, resolved);
750
+ },
751
+ on: (event, handler) => {
752
+ return eventAccessor.on(event, handler);
753
+ }
754
+ };
755
+ }
756
+ if (windowManager) {
757
+ const windowAccessor = new WindowAccessor(windowManager);
758
+ this.context.createWindow = (name) => windowAccessor.createWindow(name);
759
+ this.context.getWindow = (name) => windowAccessor.getWindow(name);
760
+ }
761
+ }
762
+ };
763
+
764
+ //#endregion
765
+ //#region src/core/feature/manager.ts
766
+ var FeatureManager = class {
767
+ registry = /* @__PURE__ */ new Map();
768
+ /** Global service ownership: serviceId → featureId. */
769
+ serviceOwners = /* @__PURE__ */ new Map();
770
+ /** Global task ownership: taskId → featureId. */
771
+ taskOwners = /* @__PURE__ */ new Map();
772
+ windowManager = null;
773
+ constructor(logger, eventBus = void 0) {
774
+ this.logger = logger;
775
+ this.eventBus = eventBus;
776
+ }
777
+ /** @internal Set the window manager (called by Electron layer before bootstrap). */
778
+ setWindowManager(windowManager) {
779
+ this.windowManager = windowManager;
780
+ }
781
+ /**
782
+ * Register a feature or a list of features.
783
+ * If a feature with the same ID is already registered, a warning is logged and the feature is skipped.
784
+ * @throws If a service or task ID is already owned by another feature.
785
+ */
786
+ register(features) {
787
+ const list = Array.isArray(features) ? features : [features];
788
+ for (const item of list) {
789
+ if (this.registry.has(item.id)) {
790
+ this.logger.warn("FeatureManager", `Feature "${item.id}" is already registered. Skipping.`);
791
+ continue;
792
+ }
793
+ const seenSvcIds = /* @__PURE__ */ new Set();
794
+ for (const svc of item.services ?? []) {
795
+ if (seenSvcIds.has(svc.id)) throw new Error(`Duplicate service "${svc.id}" within feature "${item.id}" — one service per ID.`);
796
+ seenSvcIds.add(svc.id);
797
+ const owner = this.serviceOwners.get(svc.id);
798
+ if (owner !== void 0) throw new Error(`Service "${svc.id}" is already registered by feature "${owner}". Feature "${item.id}" cannot claim it — service IDs must be globally unique.`);
799
+ }
800
+ for (const task of item.tasks ?? []) {
801
+ const owner = this.taskOwners.get(task.id);
802
+ if (owner !== void 0) throw new Error(`Task "${task.id}" is already registered by feature "${owner}". Feature "${item.id}" cannot claim it — task IDs must be globally unique.`);
803
+ }
804
+ for (const svc of item.services ?? []) this.serviceOwners.set(svc.id, item.id);
805
+ for (const task of item.tasks ?? []) this.taskOwners.set(task.id, item.id);
806
+ const feature = new Feature(item, this.logger);
807
+ this.registry.set(feature.id, feature);
808
+ feature.transition(FeatureStatus.REGISTERED);
809
+ }
810
+ }
811
+ get(id) {
812
+ return this.registry.get(id);
813
+ }
814
+ /** Returns all registered features. */
815
+ list() {
816
+ return [...this.registry.values()];
817
+ }
818
+ /**
819
+ * Bootstrap all features in dependency order.
820
+ * Calls initialize -> activate on each feature.
821
+ * The feature receives a context built externally (by Runtime).
822
+ */
823
+ async bootstrap() {
824
+ const order = this.reorder();
825
+ for (const id of order) await this.initialize(id);
826
+ for (const id of order) await this.activate(id);
827
+ }
828
+ /**
829
+ * Initialize a feature.
830
+ * Initializes the feature and sets its state to "ready" or "error".
831
+ */
832
+ async initialize(id) {
833
+ const feature = this.registry.get(id);
834
+ if (feature?.status !== FeatureStatus.REGISTERED) return;
835
+ feature.transition(FeatureStatus.INITIALIZING);
836
+ try {
837
+ const features = [];
838
+ const deps = feature.config.dependencies ?? [];
839
+ for (const dep of deps) features.push(this.registry.get(dep));
840
+ await feature.initialize(features, this, this.eventBus, this.windowManager ?? void 0);
841
+ feature.transition(FeatureStatus.READY);
842
+ } catch (err) {
843
+ feature.transition(FeatureStatus.ERROR);
844
+ this.logger.error(id, `initialize failed`, { error: err instanceof Error ? err.message : String(err) });
845
+ if (feature.config.critical) throw new Error(`Critical feature "${id}" failed to initialize`);
846
+ }
847
+ }
848
+ /**
849
+ * Activate a feature.
850
+ * @param id The ID of the feature to activate.
851
+ * @param allowRetry When true, ERROR state features can be retried (used by enable()).
852
+ */
853
+ async activate(id, allowRetry = false) {
854
+ const feature = this.registry.get(id);
855
+ if (!(allowRetry ? [
856
+ FeatureStatus.READY,
857
+ FeatureStatus.DEACTIVATED,
858
+ FeatureStatus.ERROR
859
+ ] : [FeatureStatus.READY, FeatureStatus.DEACTIVATED]).includes(feature.status)) return;
860
+ for (const depId of feature.config.dependencies ?? []) if (this.registry.get(depId)?.status === FeatureStatus.ERROR) {
861
+ this.logger.error(id, `cannot activate — dependency "${depId}" is in error state`);
862
+ return;
863
+ }
864
+ feature.transition(FeatureStatus.ACTIVATING);
865
+ try {
866
+ await feature.activate();
867
+ feature.transition(FeatureStatus.ACTIVATED);
868
+ } catch (err) {
869
+ feature.transition(FeatureStatus.ERROR);
870
+ this.logger.error(id, `activate failed`, { error: err instanceof Error ? err.message : String(err) });
871
+ if (feature.config.critical) throw new Error(`Critical feature "${id}" failed to activate`);
872
+ }
873
+ }
874
+ /**
875
+ * Deactivate a feature.
876
+ * @param id The ID of the feature to deactivate.
877
+ */
878
+ async deactivate(id) {
879
+ const feature = this.registry.get(id);
880
+ if (![FeatureStatus.ACTIVATED].includes(feature.status)) return;
881
+ feature.transition(FeatureStatus.DEACTIVATING);
882
+ try {
883
+ await feature.deactivate();
884
+ feature.transition(FeatureStatus.DEACTIVATED);
885
+ } catch (err) {
886
+ feature.transition(FeatureStatus.ERROR);
887
+ this.logger.error(id, `deactivate failed`, { error: err instanceof Error ? err.message : String(err) });
888
+ }
889
+ }
890
+ async destroy(id) {
891
+ const feature = this.registry.get(id);
892
+ if (![FeatureStatus.DEACTIVATED, FeatureStatus.ERROR].includes(feature.status)) return;
893
+ feature.transition(FeatureStatus.DESTROYING);
894
+ try {
895
+ await feature.destroy();
896
+ feature.transition(FeatureStatus.DESTROYED);
897
+ } catch (err) {
898
+ feature.transition(FeatureStatus.ERROR);
899
+ this.logger.error(id, `destroy failed`, { error: err instanceof Error ? err.message : String(err) });
900
+ }
901
+ }
902
+ /** Public: re-activate a DEACTIVATED or ERROR feature. */
903
+ async enable(id) {
904
+ if (!this.registry.get(id)) throw new Error(`Feature "${id}" not found`);
905
+ await this.activate(id, true);
906
+ }
907
+ /** Public: deactivate an ACTIVATED feature. */
908
+ async disable(id) {
909
+ if (!this.registry.get(id)) throw new Error(`Feature "${id}" not found`);
910
+ await this.deactivate(id);
911
+ }
912
+ async shutdown() {
913
+ const order = [...this.reorder()].reverse();
914
+ for (const id of order) await this.deactivate(id);
915
+ for (const id of order) await this.destroy(id);
916
+ }
917
+ reorder() {
918
+ const sorted = [];
919
+ const visited = /* @__PURE__ */ new Set();
920
+ const visiting = /* @__PURE__ */ new Set();
921
+ const visit = (id) => {
922
+ if (visiting.has(id)) throw new Error(`Circular dependency detected: Feature "${id}" depends on itself!`);
923
+ if (visited.has(id)) return;
924
+ visiting.add(id);
925
+ const feature = this.registry.get(id);
926
+ if (!feature) throw new Error(`Missing dependency: Feature "${id}" is not registered.`);
927
+ for (const depId of feature.config.dependencies ?? []) visit(depId);
928
+ visiting.delete(id);
929
+ visited.add(id);
930
+ sorted.push(id);
931
+ };
932
+ for (const id of this.registry.keys()) visit(id);
933
+ return sorted;
934
+ }
935
+ };
936
+
937
+ //#endregion
938
+ //#region src/core/logger/console-handler.ts
939
+ const dim = "\x1B[90m";
940
+ const cyan = "\x1B[36m";
941
+ const green = "\x1B[32m";
942
+ const yellow = "\x1B[33m";
943
+ const magenta = "\x1B[35m";
944
+ const reset = "\x1B[0m";
945
+ function formatTime(ts) {
946
+ const d = new Date(ts);
947
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
948
+ }
949
+ function colorizeValue(value) {
950
+ if (value === null) return `${magenta}null${reset}`;
951
+ if (value === void 0) return `${dim}undefined${reset}`;
952
+ if (typeof value === "string") return `${green}"${value}"${reset}`;
953
+ if (typeof value === "number") return `${yellow}${value}${reset}`;
954
+ if (typeof value === "boolean") return `${yellow}${value}${reset}`;
955
+ if (Array.isArray(value)) {
956
+ if (value.length === 0) return "[]";
957
+ return `[${value.map(colorizeValue).join(`${dim},${reset} `)}]`;
958
+ }
959
+ if (typeof value === "object") {
960
+ const entries = Object.entries(value);
961
+ if (entries.length === 0) return "{}";
962
+ return `${dim}{${reset} ${entries.map(([k, v]) => `${cyan}${k}${reset}${dim}:${reset} ${colorizeValue(v)}`).join(`${dim},${reset} `)} ${dim}}${reset}`;
963
+ }
964
+ return String(value);
965
+ }
966
+ function createConsoleHandler() {
967
+ return (entry) => {
968
+ const time = formatTime(entry.timestamp);
969
+ const tag = entry.level === "debug" ? "electro" : entry.level;
970
+ const detailsPart = entry.details ? ` ${colorizeValue(entry.details)}` : "";
971
+ const line = `${time} [${tag}] ${entry.code} \u2192 ${entry.message}${detailsPart}`;
972
+ if (entry.level === "error") console.error(line);
973
+ else if (entry.level === "warn") console.warn(line);
974
+ else console.log(line);
975
+ };
976
+ }
977
+
978
+ //#endregion
979
+ //#region src/core/logger/logger.ts
980
+ var Logger = class {
981
+ handlers = /* @__PURE__ */ new Set();
982
+ addHandler(handler) {
983
+ this.handlers.add(handler);
984
+ }
985
+ removeHandler(handler) {
986
+ this.handlers.delete(handler);
987
+ }
988
+ debug(code, message, details) {
989
+ this.emit("debug", code, message, details);
990
+ }
991
+ warn(code, message, details) {
992
+ this.emit("warn", code, message, details);
993
+ }
994
+ error(code, message, details) {
995
+ this.emit("error", code, message, details);
996
+ }
997
+ emit(level, code, message, details) {
998
+ const entry = {
999
+ level,
1000
+ code,
1001
+ message,
1002
+ details,
1003
+ timestamp: Date.now()
1004
+ };
1005
+ for (const handler of this.handlers) handler(entry);
1006
+ }
1007
+ };
1008
+
1009
+ //#endregion
1010
+ //#region src/core/runtime/runtime.ts
1011
+ const RUNTIME_TRANSITIONS = {
1012
+ [RuntimeState.CREATED]: [RuntimeState.STARTING],
1013
+ [RuntimeState.STARTING]: [RuntimeState.RUNNING, RuntimeState.FAILED],
1014
+ [RuntimeState.RUNNING]: [RuntimeState.STOPPING],
1015
+ [RuntimeState.STOPPING]: [RuntimeState.STOPPED],
1016
+ [RuntimeState.STOPPED]: [],
1017
+ [RuntimeState.FAILED]: []
1018
+ };
1019
+ var Runtime = class {
1020
+ state;
1021
+ logger;
1022
+ featureManager;
1023
+ eventBus;
1024
+ windowManager = null;
1025
+ constructor(config) {
1026
+ this.state = new StateMachine({
1027
+ transitions: RUNTIME_TRANSITIONS,
1028
+ initial: RuntimeState.CREATED,
1029
+ name: "Runtime"
1030
+ });
1031
+ this.logger = new Logger();
1032
+ this.eventBus = new EventBus();
1033
+ this.featureManager = new FeatureManager(this.logger, this.eventBus);
1034
+ this.logger.addHandler(createConsoleHandler());
1035
+ if (config?.logger?.handlers) for (const handler of config.logger.handlers) this.logger.addHandler(handler);
1036
+ if (config?.features) this.featureManager.register(config.features);
1037
+ }
1038
+ /** @internal Inject window manager (called by Electron layer before start). */
1039
+ _injectWindowManager(windowManager) {
1040
+ this.state.assertState(RuntimeState.CREATED);
1041
+ this.windowManager = windowManager;
1042
+ this.featureManager.setWindowManager(windowManager);
1043
+ }
1044
+ register(features) {
1045
+ this.state.assertState(RuntimeState.CREATED);
1046
+ this.featureManager.register(features);
1047
+ }
1048
+ async start() {
1049
+ if (!this.windowManager && typeof __ELECTRO_WINDOW_DEFINITIONS__ !== "undefined") {
1050
+ const wm = new WindowManager(createDefaultWindowFactory());
1051
+ for (const def of __ELECTRO_WINDOW_DEFINITIONS__) wm.registerDefinition(def);
1052
+ this._injectWindowManager(wm);
1053
+ }
1054
+ this.state.transition(RuntimeState.STARTING);
1055
+ try {
1056
+ await this.featureManager.bootstrap();
1057
+ this.state.transition(RuntimeState.RUNNING);
1058
+ } catch (err) {
1059
+ this.state.transition(RuntimeState.FAILED);
1060
+ throw err;
1061
+ }
1062
+ }
1063
+ async shutdown() {
1064
+ this.state.assertState(RuntimeState.RUNNING);
1065
+ this.state.transition(RuntimeState.STOPPING);
1066
+ await this.featureManager.shutdown();
1067
+ this.windowManager?.destroyAll();
1068
+ this.state.transition(RuntimeState.STOPPED);
1069
+ }
1070
+ async enable(id) {
1071
+ this.state.assertState(RuntimeState.RUNNING);
1072
+ await this.featureManager.enable(id);
1073
+ }
1074
+ async disable(id) {
1075
+ this.state.assertState(RuntimeState.RUNNING);
1076
+ await this.featureManager.disable(id);
1077
+ }
1078
+ isDegraded() {
1079
+ this.state.assertState(RuntimeState.RUNNING);
1080
+ return this.featureManager.list().some((f) => f.status === FeatureStatus.ERROR);
1081
+ }
1082
+ };
1083
+
1084
+ //#endregion
1085
+ //#region src/core/runtime/helpers.ts
1086
+ function createRuntime(config) {
1087
+ return new Runtime(config);
1088
+ }
1089
+
1090
+ //#endregion
1091
+ //#region src/core/service/service.ts
1092
+ /**
1093
+ * Single service unit: one id, one scope, one factory.
1094
+ *
1095
+ * Lifecycle: Registered → Active (after build) → Destroyed (after destroy).
1096
+ * `build()` is idempotent — calling it when already Active is a no-op.
1097
+ */
1098
+ var Service = class {
1099
+ _status = ServiceStatus.REGISTERED;
1100
+ _api = null;
1101
+ constructor(config) {
1102
+ this.config = config;
1103
+ }
1104
+ get id() {
1105
+ return this.config.id;
1106
+ }
1107
+ get scope() {
1108
+ return this.config.scope ?? ServiceScope.PRIVATE;
1109
+ }
1110
+ /** Invoke the factory and store the result. Idempotent — no-op if already Active. */
1111
+ build(ctx) {
1112
+ if (this._status === ServiceStatus.ACTIVE) return;
1113
+ if (this._status === ServiceStatus.DESTROYED) throw new Error(`Service "${this.id}" (${this.scope}) is destroyed and cannot be rebuilt`);
1114
+ this._api = this.config.api(ctx);
1115
+ this._status = ServiceStatus.ACTIVE;
1116
+ }
1117
+ /** Clear the factory result and mark as Destroyed. Idempotent. */
1118
+ destroy() {
1119
+ this._api = null;
1120
+ this._status = ServiceStatus.DESTROYED;
1121
+ }
1122
+ /** Return the factory result, or null if not yet built / destroyed. */
1123
+ api() {
1124
+ return this._api;
1125
+ }
1126
+ /** Snapshot of the service's current state. */
1127
+ status() {
1128
+ return {
1129
+ serviceId: this.id,
1130
+ scope: this.scope,
1131
+ state: this._status
1132
+ };
1133
+ }
1134
+ };
1135
+
1136
+ //#endregion
1137
+ //#region src/core/service/helpers.ts
1138
+ /**
1139
+ * Creates a {@link Service} instance from a configuration object.
1140
+ *
1141
+ * @param config - Service configuration with `id`, `scope`, and `api`.
1142
+ * @returns A new `Service` instance ready for registration.
1143
+ * @throws If `config.id` is empty.
1144
+ */
1145
+ function createService(config) {
1146
+ if (!config.id) throw new Error("Service must have an id");
1147
+ return new Service(config);
1148
+ }
1149
+
1150
+ //#endregion
1151
+ //#region src/core/task/enums.ts
1152
+ let TaskOverlapStrategy = /* @__PURE__ */ function(TaskOverlapStrategy) {
1153
+ TaskOverlapStrategy["Skip"] = "skip";
1154
+ TaskOverlapStrategy["Queue"] = "queue";
1155
+ TaskOverlapStrategy["Parallel"] = "parallel";
1156
+ return TaskOverlapStrategy;
1157
+ }({});
1158
+ let TaskStatus = /* @__PURE__ */ function(TaskStatus) {
1159
+ TaskStatus["Registered"] = "registered";
1160
+ TaskStatus["Scheduled"] = "scheduled";
1161
+ TaskStatus["Running"] = "running";
1162
+ TaskStatus["Stopped"] = "stopped";
1163
+ TaskStatus["Failed"] = "failed";
1164
+ return TaskStatus;
1165
+ }({});
1166
+ let TaskRetryStrategy = /* @__PURE__ */ function(TaskRetryStrategy) {
1167
+ TaskRetryStrategy["Fixed"] = "fixed";
1168
+ TaskRetryStrategy["Exponential"] = "exponential";
1169
+ return TaskRetryStrategy;
1170
+ }({});
1171
+ let TaskTriggerKind = /* @__PURE__ */ function(TaskTriggerKind) {
1172
+ TaskTriggerKind["Cron"] = "cron";
1173
+ TaskTriggerKind["Manual"] = "manual";
1174
+ return TaskTriggerKind;
1175
+ }({});
1176
+
1177
+ //#endregion
1178
+ //#region ../../node_modules/.bun/es-toolkit@github+MyraxByte+es-toolkit+f71db7c/node_modules/es-toolkit/src/error/AbortError.ts
1179
+ /**
1180
+ * An error class representing an aborted operation.
1181
+ * @augments Error
1182
+ */
1183
+ var AbortError = class extends Error {
1184
+ constructor(message = "The operation was aborted") {
1185
+ super(message);
1186
+ this.name = "AbortError";
1187
+ }
1188
+ };
1189
+
1190
+ //#endregion
1191
+ //#region ../../node_modules/.bun/es-toolkit@github+MyraxByte+es-toolkit+f71db7c/node_modules/es-toolkit/src/error/TimeoutError.ts
1192
+ /**
1193
+ * An error class representing an timeout operation.
1194
+ * @augments Error
1195
+ */
1196
+ var TimeoutError = class extends Error {
1197
+ constructor(message = "The operation was timed out") {
1198
+ super(message);
1199
+ this.name = "TimeoutError";
1200
+ }
1201
+ };
1202
+
1203
+ //#endregion
1204
+ //#region ../../node_modules/.bun/es-toolkit@github+MyraxByte+es-toolkit+f71db7c/node_modules/es-toolkit/src/promise/delay.ts
1205
+ /**
1206
+ * Delays the execution of code for a specified number of milliseconds.
1207
+ *
1208
+ * This function returns a Promise that resolves after the specified delay, allowing you to use it
1209
+ * with async/await to pause execution.
1210
+ *
1211
+ * @param {number} ms - The number of milliseconds to delay.
1212
+ * @param {DelayOptions} options - The options object.
1213
+ * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the delay.
1214
+ * @returns {Promise<void>} A Promise that resolves after the specified delay.
1215
+ *
1216
+ * @example
1217
+ * async function foo() {
1218
+ * console.log('Start');
1219
+ * await delay(1000); // Delays execution for 1 second
1220
+ * console.log('End');
1221
+ * }
1222
+ *
1223
+ * foo();
1224
+ *
1225
+ * // With AbortSignal
1226
+ * const controller = new AbortController();
1227
+ * const { signal } = controller;
1228
+ *
1229
+ * setTimeout(() => controller.abort(), 50); // Will cancel the delay after 50ms
1230
+ * try {
1231
+ * await delay(100, { signal });
1232
+ * } catch (error) {
1233
+ * console.error(error); // Will log 'AbortError'
1234
+ * }
1235
+ * }
1236
+ */
1237
+ function delay(ms, { signal } = {}) {
1238
+ return new Promise((resolve, reject) => {
1239
+ const abortError = () => {
1240
+ reject(new AbortError());
1241
+ };
1242
+ const abortHandler = () => {
1243
+ clearTimeout(timeoutId);
1244
+ abortError();
1245
+ };
1246
+ if (signal?.aborted) return abortError();
1247
+ const timeoutId = setTimeout(() => {
1248
+ signal?.removeEventListener("abort", abortHandler);
1249
+ resolve();
1250
+ }, ms);
1251
+ signal?.addEventListener("abort", abortHandler, { once: true });
1252
+ });
1253
+ }
1254
+
1255
+ //#endregion
1256
+ //#region ../../node_modules/.bun/es-toolkit@github+MyraxByte+es-toolkit+f71db7c/node_modules/es-toolkit/src/promise/timeout.ts
1257
+ /**
1258
+ * Returns a promise that rejects with a `TimeoutError` after a specified delay.
1259
+ *
1260
+ * @param {number} ms - The delay duration in milliseconds.
1261
+ * @param {TimeoutOptions} options - The options object.
1262
+ * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the timeout.
1263
+ * @returns {Promise<never>} A promise that rejects with a `TimeoutError` after the specified delay.
1264
+ * @throws {TimeoutError} Throws a `TimeoutError` after the specified delay.
1265
+ *
1266
+ * @example
1267
+ * try {
1268
+ * await timeout(1000); // Timeout exception after 1 second
1269
+ * } catch (error) {
1270
+ * console.error(error); // Will log 'The operation was timed out'
1271
+ * }
1272
+ *
1273
+ * // With AbortSignal
1274
+ * const controller = new AbortController();
1275
+ * const { signal } = controller;
1276
+ * setTimeout(() => controller.abort(), 50);
1277
+ * try {
1278
+ * await timeout(1000, { signal }); // Will be aborted after 50ms
1279
+ * } catch (error) {
1280
+ * console.error(error); // Will log 'The operation was aborted'
1281
+ * }
1282
+ */
1283
+ async function timeout(ms, { signal } = {}) {
1284
+ await delay(ms, { signal });
1285
+ throw new TimeoutError();
1286
+ }
1287
+
1288
+ //#endregion
1289
+ //#region ../../node_modules/.bun/es-toolkit@github+MyraxByte+es-toolkit+f71db7c/node_modules/es-toolkit/src/promise/withTimeout.ts
1290
+ /**
1291
+ * Executes an async function and enforces a timeout.
1292
+ *
1293
+ * If the promise does not resolve within the specified time,
1294
+ * the timeout will trigger and the returned promise will be rejected.
1295
+ *
1296
+ * @template T
1297
+ * @param {() => Promise<T>} run - A function that returns a promise to be executed.
1298
+ * @param {number} ms - The timeout duration in milliseconds.
1299
+ * @param {WithTimeoutOptions} options - The options object.
1300
+ * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the operation.
1301
+ * @returns {Promise<T>} A promise that resolves with the result of the `run` function or rejects if the timeout is reached.
1302
+ *
1303
+ * @example
1304
+ * async function fetchData() {
1305
+ * const response = await fetch('https://example.com/data');
1306
+ * return response.json();
1307
+ * }
1308
+ *
1309
+ * try {
1310
+ * const data = await withTimeout(fetchData, 1000);
1311
+ * console.log(data); // Logs the fetched data if resolved within 1 second.
1312
+ * } catch (error) {
1313
+ * console.error(error); // Will log 'TimeoutError' if not resolved within 1 second.
1314
+ * }
1315
+ *
1316
+ * // With AbortSignal
1317
+ * const controller = new AbortController();
1318
+ * const { signal } = controller;
1319
+ * const data = await withTimeout(async () => {
1320
+ * const response = await fetch('https://example.com/data', { signal });
1321
+ * return response.json();
1322
+ * }, 5000, { signal });
1323
+ */
1324
+ async function withTimeout(run, ms, { signal } = {}) {
1325
+ return Promise.race([run(), timeout(ms, { signal })]);
1326
+ }
1327
+
1328
+ //#endregion
1329
+ //#region src/core/task/task.ts
1330
+ const noop = () => {};
1331
+ /**
1332
+ * Single executable unit within the Electro task system.
1333
+ *
1334
+ * Supports overlap strategies (skip / queue / parallel), per-payload deduplication,
1335
+ * configurable retry with fixed or exponential backoff, timeouts with abort propagation,
1336
+ * and cron-based scheduling via `croner`.
1337
+ *
1338
+ * State machine: `Registered → Scheduled → Running → Scheduled | Failed | Stopped`
1339
+ */
1340
+ var Task = class {
1341
+ _status = TaskStatus.Registered;
1342
+ _runningCount = 0;
1343
+ _lastRunAt = null;
1344
+ _lastSuccessAt = null;
1345
+ _lastErrorAt = null;
1346
+ _lastError = null;
1347
+ _queue = [];
1348
+ _stopping = false;
1349
+ _enabled = false;
1350
+ _abortControllers = /* @__PURE__ */ new Set();
1351
+ _cronJob = null;
1352
+ _ctx = null;
1353
+ _activeDedupeKeys = /* @__PURE__ */ new Set();
1354
+ constructor(config) {
1355
+ this.config = config;
1356
+ }
1357
+ get id() {
1358
+ return this.config.id;
1359
+ }
1360
+ /** Activate the task within a feature context. Idempotent. */
1361
+ enable(ctx) {
1362
+ if (this._enabled) return;
1363
+ this._ctx = ctx;
1364
+ this._enabled = true;
1365
+ this._status = TaskStatus.Scheduled;
1366
+ const autoStart = this.config.autoStart !== false;
1367
+ if (this.config.cron && autoStart) this.scheduleCron();
1368
+ else if (!this.config.cron && autoStart) this.execute(TaskTriggerKind.Manual, void 0).catch(noop);
1369
+ }
1370
+ /** Deactivate the task. `"graceful"` lets running executions finish; `"force"` aborts them. */
1371
+ disable(mode = "graceful") {
1372
+ this._enabled = false;
1373
+ this.clear();
1374
+ if (this._cronJob) {
1375
+ this._cronJob.stop();
1376
+ this._cronJob = null;
1377
+ }
1378
+ if (mode === "force") for (const ac of this._abortControllers) ac.abort();
1379
+ if (this._runningCount === 0) this._status = TaskStatus.Stopped;
1380
+ }
1381
+ /** Trigger a manual execution. No-op when the task is not enabled. */
1382
+ async start(payload) {
1383
+ await this.execute(TaskTriggerKind.Manual, payload);
1384
+ }
1385
+ /** Push payload to FIFO queue. Starts processing if idle. */
1386
+ queue(payload) {
1387
+ if (!this._enabled || !this._ctx) return;
1388
+ if (this._runningCount > 0) {
1389
+ this._queue.push(payload);
1390
+ return;
1391
+ }
1392
+ this.execute(TaskTriggerKind.Manual, payload).catch(noop);
1393
+ }
1394
+ /** Abort current execution. Queue continues processing next item. */
1395
+ stop() {
1396
+ this._stopping = true;
1397
+ for (const ac of this._abortControllers) ac.abort();
1398
+ }
1399
+ /** Clear the FIFO queue without stopping current execution. */
1400
+ clear() {
1401
+ this._queue = [];
1402
+ }
1403
+ /** Snapshot of the task's current state and run history. */
1404
+ status() {
1405
+ return {
1406
+ taskId: this.id,
1407
+ state: this._status,
1408
+ running: this._runningCount > 0,
1409
+ queueSize: this._queue.length,
1410
+ lastRunAt: this._lastRunAt,
1411
+ lastSuccessAt: this._lastSuccessAt,
1412
+ lastErrorAt: this._lastErrorAt,
1413
+ lastError: this._lastError
1414
+ };
1415
+ }
1416
+ async execute(trigger, payload) {
1417
+ if (!this._enabled || !this._ctx) return;
1418
+ const ctx = this._ctx;
1419
+ this._stopping = false;
1420
+ if (this.shouldSkipExecution(payload)) return;
1421
+ const dedupeKey = this.acquireDedupeKey(payload);
1422
+ this._runningCount++;
1423
+ this._status = TaskStatus.Running;
1424
+ let lastError = null;
1425
+ try {
1426
+ lastError = await this.executeWithRetry(ctx, payload);
1427
+ } finally {
1428
+ this._runningCount--;
1429
+ if (dedupeKey) this._activeDedupeKeys.delete(dedupeKey);
1430
+ this.resolveStatus(lastError);
1431
+ }
1432
+ if (lastError) {
1433
+ if (this._stopping) {
1434
+ this._stopping = false;
1435
+ this.drainQueue(trigger);
1436
+ } else {
1437
+ this._queue = [];
1438
+ throw lastError;
1439
+ }
1440
+ return;
1441
+ }
1442
+ this.drainQueue(trigger);
1443
+ }
1444
+ resolveStatus(lastError) {
1445
+ if (this._runningCount > 0) return;
1446
+ if (lastError) this._status = TaskStatus.Failed;
1447
+ else if (this._enabled) this._status = TaskStatus.Scheduled;
1448
+ else this._status = TaskStatus.Stopped;
1449
+ }
1450
+ shouldSkipExecution(payload) {
1451
+ if (this._runningCount === 0) return false;
1452
+ const overlap = this.config.overlap ?? TaskOverlapStrategy.Skip;
1453
+ if (overlap === TaskOverlapStrategy.Skip) return true;
1454
+ if (overlap === TaskOverlapStrategy.Queue) {
1455
+ this._queue.push(payload);
1456
+ return true;
1457
+ }
1458
+ if (this.config.dedupeKey) return this._activeDedupeKeys.has(this.config.dedupeKey(payload));
1459
+ return false;
1460
+ }
1461
+ acquireDedupeKey(payload) {
1462
+ if (!this.config.dedupeKey) return void 0;
1463
+ const key = this.config.dedupeKey(payload);
1464
+ this._activeDedupeKeys.add(key);
1465
+ return key;
1466
+ }
1467
+ async executeWithRetry(ctx, payload) {
1468
+ const retryConfig = this.config.retry;
1469
+ const maxAttempts = retryConfig ? retryConfig.attempts : 1;
1470
+ let lastError = null;
1471
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1472
+ if (!this._enabled) break;
1473
+ const ac = new AbortController();
1474
+ this._abortControllers.add(ac);
1475
+ this._lastRunAt = Date.now();
1476
+ try {
1477
+ const execCtx = {
1478
+ signal: ac.signal,
1479
+ attempt
1480
+ };
1481
+ const promise = Promise.resolve(this.config.execute(ctx, payload, execCtx));
1482
+ if (this.config.timeoutMs && this.config.timeoutMs > 0) await this.executeWithTimeout(promise, this.config.timeoutMs, ac);
1483
+ else await promise;
1484
+ this._lastSuccessAt = Date.now();
1485
+ this._lastError = null;
1486
+ return null;
1487
+ } catch (err) {
1488
+ const error = err instanceof Error ? err : new Error(String(err));
1489
+ lastError = error;
1490
+ this._lastErrorAt = Date.now();
1491
+ this._lastError = error;
1492
+ if (attempt < maxAttempts && retryConfig) await delay(this.computeRetryDelay(retryConfig.strategy, retryConfig.delayMs, attempt));
1493
+ } finally {
1494
+ this._abortControllers.delete(ac);
1495
+ }
1496
+ }
1497
+ return lastError;
1498
+ }
1499
+ drainQueue(trigger) {
1500
+ if (this._queue.length === 0 || !this._enabled) return;
1501
+ const payload = this._queue.shift();
1502
+ this.execute(trigger, payload).catch(noop);
1503
+ }
1504
+ scheduleCron() {
1505
+ this._cronJob = new Cron(this.config.cron, () => {
1506
+ this.execute(TaskTriggerKind.Cron, void 0).catch(noop);
1507
+ });
1508
+ }
1509
+ async executeWithTimeout(promise, timeoutMs, ac) {
1510
+ try {
1511
+ await withTimeout(() => promise, timeoutMs);
1512
+ } catch (err) {
1513
+ if (err instanceof TimeoutError) {
1514
+ ac.abort();
1515
+ throw new Error(`Task "${this.id}" timed out after ${timeoutMs}ms`);
1516
+ }
1517
+ throw err;
1518
+ }
1519
+ }
1520
+ computeRetryDelay(strategy, baseMs, attempt) {
1521
+ if (strategy === TaskRetryStrategy.Exponential) return baseMs * 2 ** (attempt - 1);
1522
+ return baseMs;
1523
+ }
1524
+ };
1525
+
1526
+ //#endregion
1527
+ //#region src/core/task/helpers.ts
1528
+ /** Create a {@link Task} from a config object. Throws if `id` is empty. */
1529
+ function createTask(config) {
1530
+ if (!config.id || config.id.trim().length === 0) throw new Error("createTask: id is required");
1531
+ return new Task(config);
1532
+ }
1533
+
1534
+ //#endregion
1535
+ //#region src/policy/types.ts
1536
+ /** Outcome of a policy check. */
1537
+ let PolicyDecision = /* @__PURE__ */ function(PolicyDecision) {
1538
+ PolicyDecision["ALLOWED"] = "ALLOWED";
1539
+ PolicyDecision["ACCESS_DENIED"] = "ACCESS_DENIED";
1540
+ PolicyDecision["WINDOW_NOT_FOUND"] = "WINDOW_NOT_FOUND";
1541
+ return PolicyDecision;
1542
+ }({});
1543
+
1544
+ //#endregion
1545
+ //#region src/policy/engine.ts
1546
+ /**
1547
+ * Deny-by-default policy engine for window–feature access control.
1548
+ *
1549
+ * A window's renderer can only access exposed services of features
1550
+ * listed in its `features: []` config. Everything else is denied.
1551
+ *
1552
+ * Used at:
1553
+ * - **Build time** (codegen): generate preload stubs only for allowed features
1554
+ * - **Runtime** (IPC routing): gate incoming calls from renderer
1555
+ */
1556
+ var PolicyEngine = class {
1557
+ policies = /* @__PURE__ */ new Map();
1558
+ constructor(windows) {
1559
+ for (const win of windows) this.policies.set(win.name, new Set(win.features ?? []));
1560
+ }
1561
+ /** Full policy check with decision code and context. */
1562
+ check(windowName, featureId) {
1563
+ const allowed = this.policies.get(windowName);
1564
+ if (!allowed) return {
1565
+ decision: PolicyDecision.WINDOW_NOT_FOUND,
1566
+ windowName,
1567
+ featureId
1568
+ };
1569
+ return {
1570
+ decision: allowed.has(featureId) ? PolicyDecision.ALLOWED : PolicyDecision.ACCESS_DENIED,
1571
+ windowName,
1572
+ featureId
1573
+ };
1574
+ }
1575
+ /** Convenience: returns true only when access is ALLOWED. */
1576
+ canAccess(windowName, featureId) {
1577
+ return this.check(windowName, featureId).decision === PolicyDecision.ALLOWED;
1578
+ }
1579
+ /** Returns the allowed feature IDs for a window. Throws if window unknown. */
1580
+ getAllowedFeatures(windowName) {
1581
+ const allowed = this.policies.get(windowName);
1582
+ if (!allowed) throw new Error(`Window "${windowName}" is not registered in the policy engine`);
1583
+ return [...allowed];
1584
+ }
1585
+ /** Returns all registered window names. */
1586
+ getWindowNames() {
1587
+ return [...this.policies.keys()];
1588
+ }
1589
+ };
1590
+
1591
+ //#endregion
1592
+ export { EventAccessor, FeatureStatus, PolicyDecision, PolicyEngine, Runtime, RuntimeState, ServiceScope, ServiceStatus, TaskOverlapStrategy, TaskRetryStrategy, TaskStatus, createEvent, createFeature, createRuntime, createService, createTask, defineConfig, defineRuntime, defineWindow };