@chromahq/core 1.0.56 → 1.0.58

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.
@@ -1,6 +1,126 @@
1
1
  'use strict';
2
2
 
3
3
  var container$1 = require('@inversifyjs/container');
4
+ var core = require('@inversifyjs/core');
5
+
6
+ const SUBSCRIBE_METADATA_KEY = "chroma:subscribe";
7
+ function Subscribe(eventName) {
8
+ return function(target, propertyKey, _descriptor) {
9
+ const key = "chroma:subscribe";
10
+ const constructor = target.constructor;
11
+ const existing = Reflect.getMetadata(key, constructor) || [];
12
+ existing.push({ eventName, methodName: propertyKey });
13
+ Reflect.defineMetadata(key, existing, constructor);
14
+ };
15
+ }
16
+ function getSubscribeMetadata(constructor) {
17
+ return Reflect.getMetadata("chroma:subscribe", constructor) || [];
18
+ }
19
+
20
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
21
+ var __decorateClass = (decorators, target, key, kind) => {
22
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
23
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
24
+ if (decorator = decorators[i])
25
+ result = (decorator(result)) || result;
26
+ return result;
27
+ };
28
+ const EventBusToken = /* @__PURE__ */ Symbol.for("EventBus");
29
+ exports.AppEventBus = class AppEventBus {
30
+ constructor() {
31
+ /** Registry of all active subscriptions */
32
+ this.subscriptions = [];
33
+ /** Auto-incrementing ID counter */
34
+ this.nextId = 0;
35
+ }
36
+ // ─────────────────────────────────────────────────────────────────────────
37
+ // Subscribe
38
+ // ─────────────────────────────────────────────────────────────────────────
39
+ /**
40
+ * Subscribe to a named event.
41
+ *
42
+ * @param eventName - the event to listen for
43
+ * @param handler - callback invoked with the event payload
44
+ * @param handlerName - human-readable name for logging
45
+ * @returns an unsubscribe function
46
+ */
47
+ on(eventName, handler, handlerName) {
48
+ const id = ++this.nextId;
49
+ this.subscriptions.push({
50
+ id,
51
+ eventName,
52
+ handler,
53
+ handlerName
54
+ });
55
+ return () => {
56
+ this.subscriptions = this.subscriptions.filter((s) => s.id !== id);
57
+ };
58
+ }
59
+ // ─────────────────────────────────────────────────────────────────────────
60
+ // Emit
61
+ // ─────────────────────────────────────────────────────────────────────────
62
+ /**
63
+ * Emit a named event to all matching subscribers.
64
+ *
65
+ * Handlers execute in parallel. Individual handler failures are caught
66
+ * and logged — they do not propagate or block other handlers.
67
+ *
68
+ * @param eventName - the event to emit
69
+ * @param payload - optional data passed to every handler
70
+ */
71
+ async emit(eventName, payload) {
72
+ const matching = this.subscriptions.filter((s) => s.eventName === eventName);
73
+ if (matching.length === 0) {
74
+ return;
75
+ }
76
+ const results = await Promise.allSettled(
77
+ matching.map(async (sub) => {
78
+ try {
79
+ await sub.handler(payload);
80
+ } catch (error) {
81
+ console.error(
82
+ `[AppEventBus] Handler "${sub.handlerName}" failed for event "${eventName}":`,
83
+ error
84
+ );
85
+ throw error;
86
+ }
87
+ })
88
+ );
89
+ const failed = results.filter((r) => r.status === "rejected").length;
90
+ if (failed > 0) {
91
+ console.warn(
92
+ `[AppEventBus] ${failed}/${matching.length} handler(s) failed for event "${eventName}"`
93
+ );
94
+ }
95
+ }
96
+ // ─────────────────────────────────────────────────────────────────────────
97
+ // Utilities
98
+ // ─────────────────────────────────────────────────────────────────────────
99
+ /**
100
+ * Get the total number of active subscriptions.
101
+ * Useful for debugging and testing.
102
+ */
103
+ getSubscriptionCount() {
104
+ return this.subscriptions.length;
105
+ }
106
+ /**
107
+ * Get the number of subscriptions for a specific event.
108
+ *
109
+ * @param eventName - the event to count subscriptions for
110
+ */
111
+ getSubscriptionCountForEvent(eventName) {
112
+ return this.subscriptions.filter((s) => s.eventName === eventName).length;
113
+ }
114
+ /**
115
+ * Remove all subscriptions. Primarily for testing.
116
+ */
117
+ clearAllSubscriptions() {
118
+ this.subscriptions = [];
119
+ }
120
+ };
121
+ exports.AppEventBus = __decorateClass([
122
+ core.injectable()
123
+ ], exports.AppEventBus);
4
124
 
5
125
  const NONCE_STORE_STORAGE_KEY = "__CHROMA_NONCE_STORE__";
6
126
  class NonceService {
@@ -1025,10 +1145,10 @@ const _AlarmAdapter = class _AlarmAdapter {
1025
1145
  /**
1026
1146
  * Initialize the Chrome Alarms listener (once)
1027
1147
  */
1028
- this.initializeAlarmListener = () => {
1148
+ this.initializeAlarmListener = async () => {
1029
1149
  if (this.listenerRegistered) return;
1030
1150
  if (this.isChromeAlarmsAvailable()) {
1031
- this.clearStaleAlarms();
1151
+ await this.clearStaleAlarms();
1032
1152
  chrome.alarms.onAlarm.addListener(this.handleAlarm);
1033
1153
  this.listenerRegistered = true;
1034
1154
  console.log("[AlarmAdapter] \u2705 Chrome Alarms API available and listener registered");
@@ -1039,20 +1159,31 @@ const _AlarmAdapter = class _AlarmAdapter {
1039
1159
  }
1040
1160
  };
1041
1161
  /**
1042
- * Clear any stale chroma alarms from previous SW instances
1162
+ * Clear any stale chroma alarms from previous SW instances.
1163
+ * Returns a promise that resolves once all stale alarms are cleared.
1043
1164
  */
1044
1165
  this.clearStaleAlarms = () => {
1045
- if (!this.isChromeAlarmsAvailable()) return;
1046
- chrome.alarms.getAll((alarms) => {
1047
- const staleAlarms = alarms.filter((a) => a.name.startsWith(_AlarmAdapter.ALARM_PREFIX));
1048
- if (staleAlarms.length > 0) {
1049
- console.log(
1050
- `[AlarmAdapter] \u{1F9F9} Clearing ${staleAlarms.length} stale alarms from previous session`
1051
- );
1052
- staleAlarms.forEach((alarm) => {
1053
- chrome.alarms.clear(alarm.name);
1054
- });
1055
- }
1166
+ if (!this.isChromeAlarmsAvailable()) return Promise.resolve();
1167
+ return new Promise((resolve) => {
1168
+ chrome.alarms.getAll((alarms) => {
1169
+ const staleAlarms = alarms.filter((a) => a.name.startsWith(_AlarmAdapter.ALARM_PREFIX));
1170
+ if (staleAlarms.length > 0) {
1171
+ console.log(
1172
+ `[AlarmAdapter] \u{1F9F9} Clearing ${staleAlarms.length} stale alarms from previous session`
1173
+ );
1174
+ let cleared = 0;
1175
+ staleAlarms.forEach((alarm) => {
1176
+ chrome.alarms.clear(alarm.name, () => {
1177
+ cleared++;
1178
+ if (cleared === staleAlarms.length) {
1179
+ resolve();
1180
+ }
1181
+ });
1182
+ });
1183
+ } else {
1184
+ resolve();
1185
+ }
1186
+ });
1056
1187
  });
1057
1188
  };
1058
1189
  /**
@@ -1157,7 +1288,7 @@ const _AlarmAdapter = class _AlarmAdapter {
1157
1288
  usingChromeApi: this.isChromeAlarmsAvailable()
1158
1289
  };
1159
1290
  };
1160
- this.initializeAlarmListener();
1291
+ this.ready = this.initializeAlarmListener();
1161
1292
  }
1162
1293
  };
1163
1294
  _AlarmAdapter.ALARM_PREFIX = "chroma_job_";
@@ -2064,6 +2195,13 @@ class Scheduler {
2064
2195
  return;
2065
2196
  }
2066
2197
  }
2198
+ this.alarm.ready.then(() => this.scheduleInternal(id, options));
2199
+ }
2200
+ scheduleInternal(id, options) {
2201
+ const context = this.registry.getContext(id);
2202
+ if (!context || context.isStopped() || context.isPaused()) {
2203
+ return;
2204
+ }
2067
2205
  const when = this.getScheduleTime(options);
2068
2206
  const now = Date.now();
2069
2207
  if (when <= now) {
@@ -2202,6 +2340,7 @@ class ApplicationBootstrap {
2202
2340
  constructor() {
2203
2341
  this.serviceDependencies = /* @__PURE__ */ new Map();
2204
2342
  this.serviceRegistry = /* @__PURE__ */ new Map();
2343
+ this.jobRegistry = /* @__PURE__ */ new Map();
2205
2344
  this.logger = new BootstrapLogger();
2206
2345
  this.storeDefinitions = [];
2207
2346
  }
@@ -2233,6 +2372,7 @@ class ApplicationBootstrap {
2233
2372
  try {
2234
2373
  this.logger = new BootstrapLogger(enableLogs);
2235
2374
  this.logger.info("Starting Chroma application bootstrap...");
2375
+ this.initializeEventBus();
2236
2376
  await this.discoverAndInitializeStores();
2237
2377
  await this.discoverServices();
2238
2378
  const store = this.storeDefinitions[0].store;
@@ -2252,6 +2392,7 @@ class ApplicationBootstrap {
2252
2392
  if (!disableBootMethods) {
2253
2393
  await this.bootMessages();
2254
2394
  await this.bootServices();
2395
+ this.wireEventSubscriptions();
2255
2396
  }
2256
2397
  this.logger.success("Chroma application initialization complete");
2257
2398
  } catch (error) {
@@ -2282,6 +2423,62 @@ class ApplicationBootstrap {
2282
2423
  );
2283
2424
  await Promise.all(bootPromises);
2284
2425
  }
2426
+ /**
2427
+ * Create and bind the global AppEventBus singleton to the DI container.
2428
+ * Called early in bootstrap so any service can inject it.
2429
+ */
2430
+ initializeEventBus() {
2431
+ if (!container.isBound(exports.AppEventBus)) {
2432
+ container.bind(exports.AppEventBus).toSelf().inSingletonScope();
2433
+ }
2434
+ if (!container.isBound(EventBusToken)) {
2435
+ container.bind(EventBusToken).toDynamicValue(() => container.get(exports.AppEventBus)).inSingletonScope();
2436
+ }
2437
+ this.logger.debug("AppEventBus bound to DI container");
2438
+ }
2439
+ /**
2440
+ * Scan all registered services and jobs for @Subscribe metadata and
2441
+ * wire the decorated methods to the AppEventBus.
2442
+ *
2443
+ * This runs after bootServices so every singleton is already instantiated.
2444
+ */
2445
+ wireEventSubscriptions() {
2446
+ this.logger.info("Wiring @Subscribe event subscriptions...");
2447
+ const bus = container.get(exports.AppEventBus);
2448
+ let wiredCount = 0;
2449
+ const scan = (name, Constructor) => {
2450
+ const metadata = getSubscribeMetadata(Constructor);
2451
+ if (metadata.length === 0) {
2452
+ return;
2453
+ }
2454
+ let instance;
2455
+ try {
2456
+ instance = container.get(Constructor);
2457
+ } catch {
2458
+ this.logger.warn(`Could not resolve instance for ${name}, skipping @Subscribe wiring`);
2459
+ return;
2460
+ }
2461
+ for (const { eventName, methodName } of metadata) {
2462
+ const method = instance[methodName];
2463
+ if (typeof method !== "function") {
2464
+ this.logger.warn(
2465
+ `@Subscribe('${eventName}') on ${name}.${methodName} is not a function, skipping`
2466
+ );
2467
+ continue;
2468
+ }
2469
+ bus.on(eventName, method.bind(instance), `${name}.${methodName}`);
2470
+ wiredCount++;
2471
+ this.logger.debug(`Wired @Subscribe('${eventName}') \u2192 ${name}.${methodName}`);
2472
+ }
2473
+ };
2474
+ for (const [name, Constructor] of this.serviceRegistry) {
2475
+ scan(name, Constructor);
2476
+ }
2477
+ for (const [name, Constructor] of this.jobRegistry) {
2478
+ scan(name, Constructor);
2479
+ }
2480
+ this.logger.success(`Wired ${wiredCount} @Subscribe handler(s) to AppEventBus`);
2481
+ }
2285
2482
  /**
2286
2483
  * Discover all services in the application directory
2287
2484
  */
@@ -2620,10 +2817,11 @@ class ApplicationBootstrap {
2620
2817
  if (!container.isBound(JobClass)) {
2621
2818
  container.bind(JobClass).toSelf().inSingletonScope();
2622
2819
  }
2623
- const id = `${jobName.toLowerCase()}:${JobClass.name.toLowerCase()} ${Math.random().toString(36).substring(2, 15)}`;
2820
+ const id = `${jobName.toLowerCase()}:${JobClass.name.toLowerCase()}`;
2624
2821
  container.bind(id).to(JobClass).inSingletonScope();
2625
2822
  const options = Reflect.getMetadata("job:options", JobClass) || {};
2626
2823
  jobEntries.push({ JobClass, jobName, id, options });
2824
+ this.jobRegistry.set(jobName, JobClass);
2627
2825
  this.logger.debug(`Bound job: ${jobName}`);
2628
2826
  } catch (error) {
2629
2827
  this.logger.error(`Failed to bind job ${JobClass.name}:`, error);
@@ -2744,11 +2942,14 @@ class BootstrapBuilder {
2744
2942
  }
2745
2943
  }
2746
2944
 
2945
+ exports.EventBusToken = EventBusToken;
2747
2946
  exports.JobRegistry = JobRegistry;
2748
2947
  exports.JobState = JobState;
2749
2948
  exports.NonceService = NonceService;
2750
2949
  exports.PopupVisibilityService = PopupVisibilityService;
2950
+ exports.SUBSCRIBE_METADATA_KEY = SUBSCRIBE_METADATA_KEY;
2751
2951
  exports.Scheduler = Scheduler;
2952
+ exports.Subscribe = Subscribe;
2752
2953
  exports.arePortsClaimed = arePortsClaimed;
2753
2954
  exports.bootstrap = bootstrap;
2754
2955
  exports.claimEarlyPorts = claimEarlyPorts;
@@ -2756,6 +2957,7 @@ exports.container = container;
2756
2957
  exports.create = create;
2757
2958
  exports.getNonceService = getNonceService;
2758
2959
  exports.getPopupVisibilityService = getPopupVisibilityService;
2960
+ exports.getSubscribeMetadata = getSubscribeMetadata;
2759
2961
  exports.isEarlyListenerSetup = isEarlyListenerSetup;
2760
2962
  exports.setupEarlyListener = setupEarlyListener;
2761
- //# sourceMappingURL=boot-CUFlC4bu.js.map
2963
+ //# sourceMappingURL=boot--zb14Gg3.js.map