@cordy/electro 1.2.3 → 1.2.5

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.d.mts CHANGED
@@ -16,90 +16,9 @@ declare enum FeatureStatus {
16
16
  ERROR = "error"
17
17
  }
18
18
  //#endregion
19
- //#region src/core/view/types.d.ts
20
- type ViewId = string;
21
- /** Runtime view registry entry — injected by CLI via __ELECTRO_VIEW_REGISTRY__. */
22
- interface ViewRegistryEntry {
23
- id: ViewId;
24
- hasRenderer: boolean;
25
- webPreferences?: Record<string, unknown>;
26
- }
27
- /** A WebContentsView augmented with `load()` for renderer-linked views. */
28
- type ElectroView = WebContentsView & {
29
- load(): Promise<void>;
30
- };
31
- /** Public interface — hides the View class. */
32
- interface ViewInstance {
33
- readonly id: ViewId;
34
- /** Create the WebContentsView. Idempotent: returns existing if alive. */
35
- create(): ElectroView;
36
- /** Return the live ElectroView, or null if not yet created / destroyed. */
37
- view(): ElectroView | null;
38
- /** Destroy the WebContentsView and clear refs. */
39
- destroy(): void;
40
- }
41
- //#endregion
42
- //#region src/core/view/manager.d.ts
43
- /**
44
- * ViewManager — global registry for view instances.
45
- *
46
- * Views are registered at runtime startup and accessible from any feature.
47
- * Views are idempotent: `get(id).create()` returns the same instance if alive.
48
- */
49
- declare class ViewManager {
50
- private readonly instances;
51
- register(view: ViewInstance): void;
52
- get(id: ViewId): ViewInstance | null;
53
- destroyAll(): void;
54
- }
55
- //#endregion
56
- //#region src/core/window/types.d.ts
57
- type WindowId = string;
58
- interface WindowConfig<TApi = void> {
59
- id: WindowId;
60
- options?: BaseWindowConstructorOptions;
61
- api?: (window: BaseWindow) => TApi;
62
- }
63
- /** Base window interface (without API methods). */
64
- interface WindowBase {
65
- readonly id: WindowId;
66
- /** Create the BaseWindow. Idempotent: returns existing if alive. */
67
- create(): BaseWindow;
68
- /** The live BaseWindow, or null if not yet created / destroyed. */
69
- readonly window: BaseWindow | null;
70
- /** Destroy the BaseWindow and clear refs. */
71
- destroy(): void;
72
- }
73
- /**
74
- * Public interface — API methods are mixed directly onto the object.
75
- * Access API methods directly: `window.show()` instead of `window.api?.show()`.
76
- */
77
- type CreatedWindow<TApi = void> = WindowBase & (TApi extends void ? {} : TApi) & {
78
- /** @internal Phantom type for API type inference. */readonly __apiType?: TApi;
79
- };
80
- /** Type-erased interface for managers. */
81
- interface WindowInstance {
82
- readonly id: WindowId;
83
- create(): BaseWindow;
84
- readonly window: BaseWindow | null;
85
- destroy(): void;
86
- }
87
- //#endregion
88
- //#region src/core/window/manager.d.ts
89
- /**
90
- * WindowManager — global registry for window instances.
91
- *
92
- * Windows are registered at runtime startup and accessible from any feature.
93
- */
94
- declare class WindowManager {
95
- private readonly instances;
96
- register(window: WindowInstance): void;
97
- get(id: WindowId): WindowInstance | null;
98
- destroyAll(): void;
99
- }
100
- //#endregion
101
19
  //#region src/core/event-bus/types.d.ts
102
20
  type EventHandler = (payload: unknown) => void;
21
+ type EventInterceptor = (channel: string, payload: unknown) => void;
103
22
  type EventSubscription = {
104
23
  channel: string;
105
24
  handler: EventHandler;
@@ -129,7 +48,10 @@ interface CreatedEvent<T = void> extends EventInstance {
129
48
  //#region src/core/event-bus/event-bus.d.ts
130
49
  declare class EventBus {
131
50
  private readonly subscriptions;
51
+ private readonly interceptors;
132
52
  publish(channel: string, payload?: unknown): void;
53
+ /** Register an interceptor that receives every published event. Returns an unsubscribe function. */
54
+ addInterceptor(fn: EventInterceptor): () => void;
133
55
  subscribe(channel: string, handler: EventHandler, ownerId: string): () => void;
134
56
  removeByOwner(ownerId: string): void;
135
57
  }
@@ -376,6 +298,89 @@ declare class StateMachine<TState extends string> {
376
298
  onTransition(cb: TransitionListener<TState>): () => void;
377
299
  }
378
300
  //#endregion
301
+ //#region src/core/view/types.d.ts
302
+ type ViewId = string;
303
+ /** Runtime view registry entry — injected by CLI via __ELECTRO_VIEW_REGISTRY__. */
304
+ interface ViewRegistryEntry {
305
+ id: ViewId;
306
+ hasRenderer: boolean;
307
+ features?: readonly string[];
308
+ webPreferences?: Record<string, unknown>;
309
+ }
310
+ /** A WebContentsView augmented with `load()` for renderer-linked views. */
311
+ type ElectroView = WebContentsView & {
312
+ load(): Promise<void>;
313
+ };
314
+ /** Public interface — hides the View class. */
315
+ interface ViewInstance {
316
+ readonly id: ViewId;
317
+ /** Create the WebContentsView. Idempotent: returns existing if alive. */
318
+ create(): ElectroView;
319
+ /** Return the live ElectroView, or null if not yet created / destroyed. */
320
+ view(): ElectroView | null;
321
+ /** Destroy the WebContentsView and clear refs. */
322
+ destroy(): void;
323
+ }
324
+ //#endregion
325
+ //#region src/core/view/manager.d.ts
326
+ /**
327
+ * ViewManager — global registry for view instances.
328
+ *
329
+ * Views are registered at runtime startup and accessible from any feature.
330
+ * Views are idempotent: `get(id).create()` returns the same instance if alive.
331
+ */
332
+ declare class ViewManager {
333
+ private readonly instances;
334
+ register(view: ViewInstance): void;
335
+ get(id: ViewId): ViewInstance | null;
336
+ destroyAll(): void;
337
+ }
338
+ //#endregion
339
+ //#region src/core/window/types.d.ts
340
+ type WindowId = string;
341
+ interface WindowConfig<TApi = void> {
342
+ id: WindowId;
343
+ options?: BaseWindowConstructorOptions;
344
+ api?: (window: BaseWindow) => TApi;
345
+ }
346
+ /** Base window interface (without API methods). */
347
+ interface WindowBase {
348
+ readonly id: WindowId;
349
+ /** Create the BaseWindow. Idempotent: returns existing if alive. */
350
+ create(): BaseWindow;
351
+ /** The live BaseWindow, or null if not yet created / destroyed. */
352
+ readonly window: BaseWindow | null;
353
+ /** Destroy the BaseWindow and clear refs. */
354
+ destroy(): void;
355
+ }
356
+ /**
357
+ * Public interface — API methods are mixed directly onto the object.
358
+ * Access API methods directly: `window.show()` instead of `window.api?.show()`.
359
+ */
360
+ type CreatedWindow<TApi = void> = WindowBase & (TApi extends void ? {} : TApi) & {
361
+ /** @internal Phantom type for API type inference. */readonly __apiType?: TApi;
362
+ };
363
+ /** Type-erased interface for managers. */
364
+ interface WindowInstance {
365
+ readonly id: WindowId;
366
+ create(): BaseWindow;
367
+ readonly window: BaseWindow | null;
368
+ destroy(): void;
369
+ }
370
+ //#endregion
371
+ //#region src/core/window/manager.d.ts
372
+ /**
373
+ * WindowManager — global registry for window instances.
374
+ *
375
+ * Windows are registered at runtime startup and accessible from any feature.
376
+ */
377
+ declare class WindowManager {
378
+ private readonly instances;
379
+ register(window: WindowInstance): void;
380
+ get(id: WindowId): WindowInstance | null;
381
+ destroyAll(): void;
382
+ }
383
+ //#endregion
379
384
  //#region src/core/feature/manager.d.ts
380
385
  declare class FeatureManager {
381
386
  private logger;
@@ -679,6 +684,28 @@ declare class EventAccessor {
679
684
  on(event: string, handler: EventHandler): () => void;
680
685
  }
681
686
  //#endregion
687
+ //#region src/core/event-bus/bridge.d.ts
688
+ /**
689
+ * Forwards EventBus publishes to renderer views via IPC.
690
+ *
691
+ * Respects deny-by-default policy: only views whose `features` array
692
+ * includes the publishing feature's ID receive the event.
693
+ *
694
+ * IPC channel format: `electro:event:{featureId}:{eventName}`
695
+ */
696
+ declare class EventBridge {
697
+ private readonly eventBus;
698
+ private readonly viewManager;
699
+ private readonly viewFeatures;
700
+ private removeInterceptor;
701
+ constructor(eventBus: EventBus, viewManager: ViewManager, viewRegistry: readonly ViewRegistryEntry[]);
702
+ /** Start forwarding events to eligible views. */
703
+ start(): void;
704
+ /** Stop forwarding and clean up. */
705
+ stop(): void;
706
+ private forward;
707
+ }
708
+ //#endregion
682
709
  //#region src/core/event-bus/helpers.d.ts
683
710
  /**
684
711
  * Creates a typed event definition.
@@ -746,6 +773,7 @@ declare class Runtime {
746
773
  private readonly eventBus;
747
774
  private readonly windowManager;
748
775
  private readonly viewManager;
776
+ private readonly eventBridge;
749
777
  constructor(config?: RuntimeConfig);
750
778
  register(features: FeatureConfig<any> | FeatureConfig<any>[]): void;
751
779
  start(): Promise<void>;
@@ -820,4 +848,4 @@ declare class PolicyEngine {
820
848
  getViewNames(): string[];
821
849
  }
822
850
  //#endregion
823
- export { type BaseContext, type CreatedEvent, type CreatedService, type CreatedTask, type CreatedWindow, type DefineConfigInput, type DefineRuntimeInput, type DefineViewInput, type ElectroConfig, type ElectroView, EventAccessor, type EventHandler, type EventId, type EventInstance, type EventSubscription, type FeatureConfig, type FeatureContext, type FeatureHandle, type FeatureId, type FeatureMap, FeatureStatus, type LogEntry, type LogHandler, type LoggerContext, PolicyDecision, PolicyEngine, type PolicyResult, Runtime, type RuntimeConfig, type RuntimeDefinition, RuntimeState, type ServiceConfig, type ServiceId, type ServiceInfo, type ServiceOwnerMap, ServiceScope, ServiceStatus, type StopMode, type TaskConfig, type TaskExecutionContext, type TaskHandle, type TaskId, TaskOverlapStrategy, type TaskOwnerMap, TaskRetryStrategy, TaskStatus, type TaskStatusInfo, type TypedContext, type ViewDefinition, type ViewId, type ViewInstance, type ViewMap, type ViewRegistryEntry, type WindowApiMap, type WindowConfig, type WindowId, type WindowInstance, createEvent, createFeature, createRuntime, createService, createTask, createWindow, defineConfig, defineRuntime, defineView };
851
+ export { type BaseContext, type CreatedEvent, type CreatedService, type CreatedTask, type CreatedWindow, type DefineConfigInput, type DefineRuntimeInput, type DefineViewInput, type ElectroConfig, type ElectroView, EventAccessor, EventBridge, type EventHandler, type EventId, type EventInstance, type EventInterceptor, type EventSubscription, type FeatureConfig, type FeatureContext, type FeatureHandle, type FeatureId, type FeatureMap, FeatureStatus, type LogEntry, type LogHandler, type LoggerContext, PolicyDecision, PolicyEngine, type PolicyResult, Runtime, type RuntimeConfig, type RuntimeDefinition, RuntimeState, type ServiceConfig, type ServiceId, type ServiceInfo, type ServiceOwnerMap, ServiceScope, ServiceStatus, type StopMode, type TaskConfig, type TaskExecutionContext, type TaskHandle, type TaskId, TaskOverlapStrategy, type TaskOwnerMap, TaskRetryStrategy, TaskStatus, type TaskStatusInfo, type TypedContext, type ViewDefinition, type ViewId, type ViewInstance, type ViewMap, type ViewRegistryEntry, type WindowApiMap, type WindowConfig, type WindowId, type WindowInstance, createEvent, createFeature, createRuntime, createService, createTask, createWindow, defineConfig, defineRuntime, defineView };
package/dist/index.mjs CHANGED
@@ -96,6 +96,51 @@ var EventAccessor = class {
96
96
  }
97
97
  };
98
98
 
99
+ //#endregion
100
+ //#region src/core/event-bus/bridge.ts
101
+ /**
102
+ * Forwards EventBus publishes to renderer views via IPC.
103
+ *
104
+ * Respects deny-by-default policy: only views whose `features` array
105
+ * includes the publishing feature's ID receive the event.
106
+ *
107
+ * IPC channel format: `electro:event:{featureId}:{eventName}`
108
+ */
109
+ var EventBridge = class {
110
+ viewFeatures;
111
+ removeInterceptor = null;
112
+ constructor(eventBus, viewManager, viewRegistry) {
113
+ this.eventBus = eventBus;
114
+ this.viewManager = viewManager;
115
+ this.viewFeatures = /* @__PURE__ */ new Map();
116
+ for (const entry of viewRegistry) if (entry.features && entry.features.length > 0) this.viewFeatures.set(entry.id, new Set(entry.features));
117
+ }
118
+ /** Start forwarding events to eligible views. */
119
+ start() {
120
+ if (this.removeInterceptor) return;
121
+ this.removeInterceptor = this.eventBus.addInterceptor((channel, payload) => this.forward(channel, payload));
122
+ }
123
+ /** Stop forwarding and clean up. */
124
+ stop() {
125
+ this.removeInterceptor?.();
126
+ this.removeInterceptor = null;
127
+ }
128
+ forward(channel, payload) {
129
+ const colonIdx = channel.indexOf(":");
130
+ if (colonIdx === -1) return;
131
+ const featureId = channel.slice(0, colonIdx);
132
+ const ipcChannel = `electro:event:${channel}`;
133
+ for (const [viewId, allowed] of this.viewFeatures) {
134
+ if (!allowed.has(featureId)) continue;
135
+ const view = this.viewManager.get(viewId)?.view();
136
+ if (!view || view.webContents.isDestroyed()) continue;
137
+ try {
138
+ view.webContents.send(ipcChannel, payload);
139
+ } catch {}
140
+ }
141
+ }
142
+ };
143
+
99
144
  //#endregion
100
145
  //#region src/core/event-bus/helpers.ts
101
146
  function createEvent(id, defaults) {
@@ -154,10 +199,18 @@ let RuntimeState = /* @__PURE__ */ function(RuntimeState) {
154
199
  //#region src/core/event-bus/event-bus.ts
155
200
  var EventBus = class {
156
201
  subscriptions = /* @__PURE__ */ new Map();
202
+ interceptors = /* @__PURE__ */ new Set();
157
203
  publish(channel, payload) {
158
204
  const subs = this.subscriptions.get(channel);
159
- if (!subs) return;
160
- for (const sub of subs) sub.handler(payload);
205
+ if (subs) for (const sub of subs) sub.handler(payload);
206
+ for (const interceptor of this.interceptors) interceptor(channel, payload);
207
+ }
208
+ /** Register an interceptor that receives every published event. Returns an unsubscribe function. */
209
+ addInterceptor(fn) {
210
+ this.interceptors.add(fn);
211
+ return () => {
212
+ this.interceptors.delete(fn);
213
+ };
161
214
  }
162
215
  subscribe(channel, handler, ownerId) {
163
216
  const sub = {
@@ -511,32 +564,33 @@ var Feature = class {
511
564
  initial: FeatureStatus.NONE,
512
565
  name: `feature "${config.id}"`
513
566
  });
567
+ const fid = config.id;
514
568
  this.context = {
515
569
  getService: () => {
516
- throw new Error("Services not yet initialized");
570
+ throw new Error(`[${fid}] ctx.getService() is not available — feature has not been initialized yet`);
517
571
  },
518
572
  getTask: () => {
519
- throw new Error("Tasks not yet initialized");
573
+ throw new Error(`[${fid}] ctx.getTask() is not available — feature has not been initialized yet`);
520
574
  },
521
575
  getFeature: () => {
522
- throw new Error("Features not yet initialized");
576
+ throw new Error(`[${fid}] ctx.getFeature() is not available — feature has not been initialized yet`);
523
577
  },
524
578
  events: {
525
579
  publish: () => {
526
- throw new Error("Events not yet initialized");
580
+ throw new Error(`[${fid}] ctx.events.publish() is not available — feature has not been initialized yet`);
527
581
  },
528
582
  on: () => {
529
- throw new Error("Events not yet initialized");
583
+ throw new Error(`[${fid}] ctx.events.on() is not available — feature has not been initialized yet`);
530
584
  }
531
585
  },
532
586
  getWindow: () => {
533
- throw new Error("Window manager not available");
587
+ throw new Error(`[${fid}] ctx.getWindow() is not available — feature has not been initialized yet`);
534
588
  },
535
589
  createView: () => {
536
- throw new Error("View manager not available");
590
+ throw new Error(`[${fid}] ctx.createView() is not available — feature has not been initialized yet`);
537
591
  },
538
592
  getView: () => {
539
- throw new Error("View manager not available");
593
+ throw new Error(`[${fid}] ctx.getView() is not available — feature has not been initialized yet`);
540
594
  },
541
595
  logger: this.logger,
542
596
  signal: this.controller.signal
@@ -581,12 +635,6 @@ var Feature = class {
581
635
  for (const dep of features) if (dep.serviceManager) deps.set(dep.id, dep.serviceManager);
582
636
  const accessor = new ServiceAccessor(this.serviceManager, deps);
583
637
  this.context.getService = ((name) => accessor.get(name));
584
- this.serviceManager.startup();
585
- this.taskManager = new TaskManager(this.context);
586
- for (const task of this.config.tasks ?? []) this.taskManager.register(task);
587
- this.context.getTask = ((name) => {
588
- return new TaskHandle(this.taskManager.getTaskInstance(name), this.context);
589
- });
590
638
  const declaredDeps = new Set(this.config.dependencies ?? []);
591
639
  this.context.getFeature = ((name) => {
592
640
  if (!declaredDeps.has(name)) throw new Error(`Feature "${name}" is not a declared dependency of "${this.id}"`);
@@ -624,6 +672,12 @@ var Feature = class {
624
672
  return inst.view();
625
673
  };
626
674
  }
675
+ this.serviceManager.startup();
676
+ this.taskManager = new TaskManager(this.context);
677
+ for (const task of this.config.tasks ?? []) this.taskManager.register(task);
678
+ this.context.getTask = ((name) => {
679
+ return new TaskHandle(this.taskManager.getTaskInstance(name), this.context);
680
+ });
627
681
  }
628
682
  };
629
683
 
@@ -712,8 +766,9 @@ var FeatureManager = class {
712
766
  feature.transition(FeatureStatus.READY);
713
767
  } catch (err) {
714
768
  feature.transition(FeatureStatus.ERROR);
715
- this.logger.error(id, `initialize failed`, { error: err instanceof Error ? err.message : String(err) });
716
- if (feature.config.critical) throw new Error(`Critical feature "${id}" failed to initialize`);
769
+ const message = err instanceof Error ? err.message : String(err);
770
+ this.logger.error(id, `initialize failed: ${message}`);
771
+ if (feature.config.critical) throw new Error(`Critical feature "${id}" failed to initialize: ${message}`, { cause: err });
717
772
  }
718
773
  }
719
774
  /**
@@ -738,8 +793,9 @@ var FeatureManager = class {
738
793
  feature.transition(FeatureStatus.ACTIVATED);
739
794
  } catch (err) {
740
795
  feature.transition(FeatureStatus.ERROR);
741
- this.logger.error(id, `activate failed`, { error: err instanceof Error ? err.message : String(err) });
742
- if (feature.config.critical) throw new Error(`Critical feature "${id}" failed to activate`);
796
+ const message = err instanceof Error ? err.message : String(err);
797
+ this.logger.error(id, `activate failed: ${message}`);
798
+ if (feature.config.critical) throw new Error(`Critical feature "${id}" failed to activate: ${message}`, { cause: err });
743
799
  }
744
800
  }
745
801
  /**
@@ -755,7 +811,8 @@ var FeatureManager = class {
755
811
  feature.transition(FeatureStatus.DEACTIVATED);
756
812
  } catch (err) {
757
813
  feature.transition(FeatureStatus.ERROR);
758
- this.logger.error(id, `deactivate failed`, { error: err instanceof Error ? err.message : String(err) });
814
+ const message = err instanceof Error ? err.message : String(err);
815
+ this.logger.error(id, `deactivate failed: ${message}`);
759
816
  }
760
817
  }
761
818
  async destroy(id) {
@@ -767,7 +824,8 @@ var FeatureManager = class {
767
824
  feature.transition(FeatureStatus.DESTROYED);
768
825
  } catch (err) {
769
826
  feature.transition(FeatureStatus.ERROR);
770
- this.logger.error(id, `destroy failed`, { error: err instanceof Error ? err.message : String(err) });
827
+ const message = err instanceof Error ? err.message : String(err);
828
+ this.logger.error(id, `destroy failed: ${message}`);
771
829
  }
772
830
  }
773
831
  /** Public: re-activate a DEACTIVATED or ERROR feature. */
@@ -986,6 +1044,7 @@ var Runtime = class {
986
1044
  eventBus;
987
1045
  windowManager;
988
1046
  viewManager;
1047
+ eventBridge = null;
989
1048
  constructor(config) {
990
1049
  this.state = new StateMachine({
991
1050
  transitions: RUNTIME_TRANSITIONS,
@@ -1002,6 +1061,7 @@ var Runtime = class {
1002
1061
  this.viewManager = new ViewManager();
1003
1062
  const viewRegistry = typeof __ELECTRO_VIEW_REGISTRY__ !== "undefined" ? __ELECTRO_VIEW_REGISTRY__ : [];
1004
1063
  for (const entry of viewRegistry) this.viewManager.register(new View(entry));
1064
+ if (viewRegistry.some((v) => v.features && v.features.length > 0)) this.eventBridge = new EventBridge(this.eventBus, this.viewManager, viewRegistry);
1005
1065
  this.featureManager.setWindowManager(this.windowManager);
1006
1066
  this.featureManager.setViewManager(this.viewManager);
1007
1067
  if (config?.features) this.featureManager.register(config.features);
@@ -1013,9 +1073,11 @@ var Runtime = class {
1013
1073
  async start() {
1014
1074
  this.state.transition(RuntimeState.STARTING);
1015
1075
  try {
1076
+ this.eventBridge?.start();
1016
1077
  await this.featureManager.bootstrap();
1017
1078
  this.state.transition(RuntimeState.RUNNING);
1018
1079
  } catch (err) {
1080
+ this.eventBridge?.stop();
1019
1081
  this.state.transition(RuntimeState.FAILED);
1020
1082
  throw err;
1021
1083
  }
@@ -1023,6 +1085,7 @@ var Runtime = class {
1023
1085
  async shutdown() {
1024
1086
  this.state.assertState(RuntimeState.RUNNING);
1025
1087
  this.state.transition(RuntimeState.STOPPING);
1088
+ this.eventBridge?.stop();
1026
1089
  await this.featureManager.shutdown();
1027
1090
  this.viewManager.destroyAll();
1028
1091
  this.windowManager.destroyAll();
@@ -1619,4 +1682,4 @@ var PolicyEngine = class {
1619
1682
  };
1620
1683
 
1621
1684
  //#endregion
1622
- export { EventAccessor, FeatureStatus, PolicyDecision, PolicyEngine, Runtime, RuntimeState, ServiceScope, ServiceStatus, TaskOverlapStrategy, TaskRetryStrategy, TaskStatus, createEvent, createFeature, createRuntime, createService, createTask, createWindow, defineConfig, defineRuntime, defineView };
1685
+ export { EventAccessor, EventBridge, FeatureStatus, PolicyDecision, PolicyEngine, Runtime, RuntimeState, ServiceScope, ServiceStatus, TaskOverlapStrategy, TaskRetryStrategy, TaskStatus, createEvent, createFeature, createRuntime, createService, createTask, createWindow, defineConfig, defineRuntime, defineView };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cordy/electro",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Electron framework — feature-first runtime, codegen, and Vite-based build system",
5
5
  "license": "MIT",
6
6
  "type": "module",