@chromahq/core 1.0.55 → 1.0.57

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,4 +1,124 @@
1
1
  import { Container } from '@inversifyjs/container';
2
+ import { injectable } from '@inversifyjs/core';
3
+
4
+ const SUBSCRIBE_METADATA_KEY = "chroma:subscribe";
5
+ function Subscribe(eventName) {
6
+ return function(target, propertyKey, _descriptor) {
7
+ const key = "chroma:subscribe";
8
+ const constructor = target.constructor;
9
+ const existing = Reflect.getMetadata(key, constructor) || [];
10
+ existing.push({ eventName, methodName: propertyKey });
11
+ Reflect.defineMetadata(key, existing, constructor);
12
+ };
13
+ }
14
+ function getSubscribeMetadata(constructor) {
15
+ return Reflect.getMetadata("chroma:subscribe", constructor) || [];
16
+ }
17
+
18
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
19
+ var __decorateClass = (decorators, target, key, kind) => {
20
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
21
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
22
+ if (decorator = decorators[i])
23
+ result = (decorator(result)) || result;
24
+ return result;
25
+ };
26
+ const EventBusToken = /* @__PURE__ */ Symbol.for("EventBus");
27
+ let AppEventBus = class {
28
+ constructor() {
29
+ /** Registry of all active subscriptions */
30
+ this.subscriptions = [];
31
+ /** Auto-incrementing ID counter */
32
+ this.nextId = 0;
33
+ }
34
+ // ─────────────────────────────────────────────────────────────────────────
35
+ // Subscribe
36
+ // ─────────────────────────────────────────────────────────────────────────
37
+ /**
38
+ * Subscribe to a named event.
39
+ *
40
+ * @param eventName - the event to listen for
41
+ * @param handler - callback invoked with the event payload
42
+ * @param handlerName - human-readable name for logging
43
+ * @returns an unsubscribe function
44
+ */
45
+ on(eventName, handler, handlerName) {
46
+ const id = ++this.nextId;
47
+ this.subscriptions.push({
48
+ id,
49
+ eventName,
50
+ handler,
51
+ handlerName
52
+ });
53
+ return () => {
54
+ this.subscriptions = this.subscriptions.filter((s) => s.id !== id);
55
+ };
56
+ }
57
+ // ─────────────────────────────────────────────────────────────────────────
58
+ // Emit
59
+ // ─────────────────────────────────────────────────────────────────────────
60
+ /**
61
+ * Emit a named event to all matching subscribers.
62
+ *
63
+ * Handlers execute in parallel. Individual handler failures are caught
64
+ * and logged — they do not propagate or block other handlers.
65
+ *
66
+ * @param eventName - the event to emit
67
+ * @param payload - optional data passed to every handler
68
+ */
69
+ async emit(eventName, payload) {
70
+ const matching = this.subscriptions.filter((s) => s.eventName === eventName);
71
+ if (matching.length === 0) {
72
+ return;
73
+ }
74
+ const results = await Promise.allSettled(
75
+ matching.map(async (sub) => {
76
+ try {
77
+ await sub.handler(payload);
78
+ } catch (error) {
79
+ console.error(
80
+ `[AppEventBus] Handler "${sub.handlerName}" failed for event "${eventName}":`,
81
+ error
82
+ );
83
+ throw error;
84
+ }
85
+ })
86
+ );
87
+ const failed = results.filter((r) => r.status === "rejected").length;
88
+ if (failed > 0) {
89
+ console.warn(
90
+ `[AppEventBus] ${failed}/${matching.length} handler(s) failed for event "${eventName}"`
91
+ );
92
+ }
93
+ }
94
+ // ─────────────────────────────────────────────────────────────────────────
95
+ // Utilities
96
+ // ─────────────────────────────────────────────────────────────────────────
97
+ /**
98
+ * Get the total number of active subscriptions.
99
+ * Useful for debugging and testing.
100
+ */
101
+ getSubscriptionCount() {
102
+ return this.subscriptions.length;
103
+ }
104
+ /**
105
+ * Get the number of subscriptions for a specific event.
106
+ *
107
+ * @param eventName - the event to count subscriptions for
108
+ */
109
+ getSubscriptionCountForEvent(eventName) {
110
+ return this.subscriptions.filter((s) => s.eventName === eventName).length;
111
+ }
112
+ /**
113
+ * Remove all subscriptions. Primarily for testing.
114
+ */
115
+ clearAllSubscriptions() {
116
+ this.subscriptions = [];
117
+ }
118
+ };
119
+ AppEventBus = __decorateClass([
120
+ injectable()
121
+ ], AppEventBus);
2
122
 
3
123
  const NONCE_STORE_STORAGE_KEY = "__CHROMA_NONCE_STORE__";
4
124
  class NonceService {
@@ -2200,6 +2320,7 @@ class ApplicationBootstrap {
2200
2320
  constructor() {
2201
2321
  this.serviceDependencies = /* @__PURE__ */ new Map();
2202
2322
  this.serviceRegistry = /* @__PURE__ */ new Map();
2323
+ this.jobRegistry = /* @__PURE__ */ new Map();
2203
2324
  this.logger = new BootstrapLogger();
2204
2325
  this.storeDefinitions = [];
2205
2326
  }
@@ -2230,7 +2351,8 @@ class ApplicationBootstrap {
2230
2351
  }) {
2231
2352
  try {
2232
2353
  this.logger = new BootstrapLogger(enableLogs);
2233
- this.logger.info("\u{1F680} Starting Chroma application bootstrap...");
2354
+ this.logger.info("Starting Chroma application bootstrap...");
2355
+ this.initializeEventBus();
2234
2356
  await this.discoverAndInitializeStores();
2235
2357
  await this.discoverServices();
2236
2358
  const store = this.storeDefinitions[0].store;
@@ -2250,10 +2372,11 @@ class ApplicationBootstrap {
2250
2372
  if (!disableBootMethods) {
2251
2373
  await this.bootMessages();
2252
2374
  await this.bootServices();
2375
+ this.wireEventSubscriptions();
2253
2376
  }
2254
- this.logger.success("\u{1F389} Chroma application initialization complete!");
2377
+ this.logger.success("Chroma application initialization complete");
2255
2378
  } catch (error) {
2256
- this.logger.error("\u{1F4A5} Application bootstrap failed:", error);
2379
+ this.logger.error("Application bootstrap failed:", error);
2257
2380
  throw error;
2258
2381
  }
2259
2382
  }
@@ -2261,7 +2384,7 @@ class ApplicationBootstrap {
2261
2384
  * Boot all registered services by calling their onBoot method if present
2262
2385
  */
2263
2386
  async bootServices() {
2264
- this.logger.info("\u{1F680} Booting services...");
2387
+ this.logger.info("Booting services...");
2265
2388
  const bootPromises = Array.from(this.serviceRegistry.entries()).map(
2266
2389
  async ([serviceName, ServiceClass]) => {
2267
2390
  try {
@@ -2280,11 +2403,67 @@ class ApplicationBootstrap {
2280
2403
  );
2281
2404
  await Promise.all(bootPromises);
2282
2405
  }
2406
+ /**
2407
+ * Create and bind the global AppEventBus singleton to the DI container.
2408
+ * Called early in bootstrap so any service can inject it.
2409
+ */
2410
+ initializeEventBus() {
2411
+ if (!container.isBound(AppEventBus)) {
2412
+ container.bind(AppEventBus).toSelf().inSingletonScope();
2413
+ }
2414
+ if (!container.isBound(EventBusToken)) {
2415
+ container.bind(EventBusToken).toDynamicValue(() => container.get(AppEventBus)).inSingletonScope();
2416
+ }
2417
+ this.logger.debug("AppEventBus bound to DI container");
2418
+ }
2419
+ /**
2420
+ * Scan all registered services and jobs for @Subscribe metadata and
2421
+ * wire the decorated methods to the AppEventBus.
2422
+ *
2423
+ * This runs after bootServices so every singleton is already instantiated.
2424
+ */
2425
+ wireEventSubscriptions() {
2426
+ this.logger.info("Wiring @Subscribe event subscriptions...");
2427
+ const bus = container.get(AppEventBus);
2428
+ let wiredCount = 0;
2429
+ const scan = (name, Constructor) => {
2430
+ const metadata = getSubscribeMetadata(Constructor);
2431
+ if (metadata.length === 0) {
2432
+ return;
2433
+ }
2434
+ let instance;
2435
+ try {
2436
+ instance = container.get(Constructor);
2437
+ } catch {
2438
+ this.logger.warn(`Could not resolve instance for ${name}, skipping @Subscribe wiring`);
2439
+ return;
2440
+ }
2441
+ for (const { eventName, methodName } of metadata) {
2442
+ const method = instance[methodName];
2443
+ if (typeof method !== "function") {
2444
+ this.logger.warn(
2445
+ `@Subscribe('${eventName}') on ${name}.${methodName} is not a function, skipping`
2446
+ );
2447
+ continue;
2448
+ }
2449
+ bus.on(eventName, method.bind(instance), `${name}.${methodName}`);
2450
+ wiredCount++;
2451
+ this.logger.debug(`Wired @Subscribe('${eventName}') \u2192 ${name}.${methodName}`);
2452
+ }
2453
+ };
2454
+ for (const [name, Constructor] of this.serviceRegistry) {
2455
+ scan(name, Constructor);
2456
+ }
2457
+ for (const [name, Constructor] of this.jobRegistry) {
2458
+ scan(name, Constructor);
2459
+ }
2460
+ this.logger.success(`Wired ${wiredCount} @Subscribe handler(s) to AppEventBus`);
2461
+ }
2283
2462
  /**
2284
2463
  * Discover all services in the application directory
2285
2464
  */
2286
2465
  async discoverServices() {
2287
- this.logger.info("\u{1F50D} Discovering services...");
2466
+ this.logger.info("Discovering services...");
2288
2467
  const serviceModules = import.meta.glob(
2289
2468
  "/src/app/services/**/*.service.{ts,js}",
2290
2469
  { eager: true }
@@ -2299,7 +2478,7 @@ class ApplicationBootstrap {
2299
2478
  }
2300
2479
  const circularDepsResult = this.detectCircularDependencies(serviceClasses);
2301
2480
  if (circularDepsResult.hasCircularDependencies) {
2302
- this.logger.error("\u{1F4A5} Circular dependencies detected!");
2481
+ this.logger.error("Circular dependencies detected!");
2303
2482
  circularDepsResult.cycles.forEach((cycle, index) => {
2304
2483
  this.logger.error(`Cycle ${index + 1}: ${cycle.cycle.join(" \u2192 ")} \u2192 ${cycle.cycle[0]}`);
2305
2484
  });
@@ -2307,10 +2486,10 @@ class ApplicationBootstrap {
2307
2486
  }
2308
2487
  for (const ServiceClass of serviceClasses) {
2309
2488
  container.bind(ServiceClass).toSelf().inSingletonScope();
2310
- this.logger.debug(`\u{1F4E6} Discovered service: ${ServiceClass.name}`);
2489
+ this.logger.debug(`Discovered service: ${ServiceClass.name}`);
2311
2490
  }
2312
2491
  this.logger.success(
2313
- `\u2705 Registered ${serviceClasses.length} services without circular dependencies`
2492
+ `Registered ${serviceClasses.length} services without circular dependencies`
2314
2493
  );
2315
2494
  }
2316
2495
  /**
@@ -2442,17 +2621,17 @@ class ApplicationBootstrap {
2442
2621
  analyzeDependencies() {
2443
2622
  const serviceClasses = Array.from(this.serviceRegistry.values());
2444
2623
  const result = this.detectCircularDependencies(serviceClasses);
2445
- this.logger.info("\u{1F4CA} Dependency Analysis Report:");
2624
+ this.logger.info("Dependency Analysis Report:");
2446
2625
  this.logger.info(`Total Services: ${result.dependencyGraph.size}`);
2447
2626
  if (result.hasCircularDependencies) {
2448
- this.logger.error(`\u{1F504} Circular Dependencies Found: ${result.cycles.length}`);
2627
+ this.logger.error(`Circular Dependencies Found: ${result.cycles.length}`);
2449
2628
  result.cycles.forEach((cycle, index) => {
2450
2629
  this.logger.error(` Cycle ${index + 1}: ${cycle.cycle.join(" \u2192 ")} \u2192 ${cycle.cycle[0]}`);
2451
2630
  });
2452
2631
  } else {
2453
- this.logger.success("\u2705 No circular dependencies detected");
2632
+ this.logger.success("No circular dependencies detected");
2454
2633
  }
2455
- this.logger.info("\u{1F333} Service Dependency Tree:");
2634
+ this.logger.info("Service Dependency Tree:");
2456
2635
  for (const [serviceName, node] of result.dependencyGraph) {
2457
2636
  if (node.dependencies.length > 0) {
2458
2637
  this.logger.info(` ${serviceName} depends on:`);
@@ -2470,7 +2649,7 @@ class ApplicationBootstrap {
2470
2649
  async discoverAndInitializeStores() {
2471
2650
  try {
2472
2651
  if (this.storeDefinitions.length === 0) {
2473
- this.logger.debug("\u{1F4ED} No store definitions provided");
2652
+ this.logger.debug("No store definitions provided");
2474
2653
  return;
2475
2654
  }
2476
2655
  this.logger.info(`Initializing ${this.storeDefinitions.length} store(s)...`);
@@ -2481,7 +2660,7 @@ class ApplicationBootstrap {
2481
2660
  const classes = store.classes;
2482
2661
  container.bind(diKey).toConstantValue(storeInstance);
2483
2662
  if (isFirstStore) {
2484
- container.bind(Symbol.for("Store")).toConstantValue(storeInstance);
2663
+ container.bind(/* @__PURE__ */ Symbol.for("Store")).toConstantValue(storeInstance);
2485
2664
  isFirstStore = false;
2486
2665
  }
2487
2666
  await this.registerMessageClass(
@@ -2498,18 +2677,18 @@ class ApplicationBootstrap {
2498
2677
  `store:${store.def.name}:reset`
2499
2678
  );
2500
2679
  }
2501
- this.logger.debug(`\u2705 Initialized store: ${store.def.name}`);
2680
+ this.logger.debug(`Initialized store: ${store.def.name}`);
2502
2681
  }
2503
- this.logger.success(`\u2705 Initialized ${this.storeDefinitions.length} store(s)`);
2682
+ this.logger.success(`Initialized ${this.storeDefinitions.length} store(s)`);
2504
2683
  } catch (error) {
2505
- this.logger.error("\u274C Failed to initialize stores:", error);
2684
+ this.logger.error("Failed to initialize stores:", error);
2506
2685
  }
2507
2686
  }
2508
2687
  /**
2509
2688
  * Register message handlers
2510
2689
  */
2511
2690
  async registerMessages() {
2512
- this.logger.info("\u{1F4E8} Registering messages...");
2691
+ this.logger.info("Registering messages...");
2513
2692
  const messageModules = import.meta.glob(
2514
2693
  "/src/app/messages/**/*.message.{ts,js}",
2515
2694
  { eager: true }
@@ -2522,7 +2701,7 @@ class ApplicationBootstrap {
2522
2701
  if (messageClasses.length > 0) {
2523
2702
  const circularDepsResult = this.detectCircularDependencies(messageClasses);
2524
2703
  if (circularDepsResult.hasCircularDependencies) {
2525
- this.logger.error("\u{1F4A5} Circular dependencies detected in messages!");
2704
+ this.logger.error("Circular dependencies detected in messages!");
2526
2705
  circularDepsResult.cycles.forEach((cycle, index) => {
2527
2706
  this.logger.error(
2528
2707
  `Message Cycle ${index + 1}: ${cycle.cycle.join(" \u2192 ")} \u2192 ${cycle.cycle[0]}`
@@ -2537,30 +2716,30 @@ class ApplicationBootstrap {
2537
2716
  try {
2538
2717
  for (const [name, ServiceClass] of this.serviceRegistry) {
2539
2718
  if (!ServiceClass) {
2540
- this.logger.warn(`\u26A0\uFE0F Service not found in registry: ${name}`);
2719
+ this.logger.warn(`Service not found in registry: ${name}`);
2541
2720
  }
2542
2721
  if (!container.isBound(ServiceClass)) {
2543
- this.logger.warn(`\u26A0\uFE0F Service not bound in container: ${name}`);
2722
+ this.logger.warn(`Service not bound in container: ${name}`);
2544
2723
  }
2545
2724
  }
2546
2725
  const messageMetadata = Reflect.getMetadata("name", MessageClass);
2547
2726
  const messageName = messageMetadata || MessageClass.name;
2548
2727
  container.bind(messageName).to(MessageClass).inSingletonScope();
2549
- this.logger.success(`\u2705 Registered message: ${messageName}`);
2728
+ this.logger.success(`Registered message: ${messageName}`);
2550
2729
  } catch (error) {
2551
- this.logger.error(`\u274C Failed to register message ${MessageClass.name}:`, error);
2730
+ this.logger.error(`Failed to register message ${MessageClass.name}:`, error);
2552
2731
  }
2553
2732
  }
2554
2733
  }
2555
2734
  async registerMessageClass(MessageClass, name) {
2556
2735
  container.bind(name).to(MessageClass).inSingletonScope();
2557
- this.logger.success(`\u2705 Registered message: ${name}`);
2736
+ this.logger.success(`Registered message: ${name}`);
2558
2737
  }
2559
2738
  /**
2560
2739
  * Boot all registered messages
2561
2740
  */
2562
2741
  async bootMessages() {
2563
- this.logger.info("\u{1F680} Booting messages...");
2742
+ this.logger.info("Booting messages...");
2564
2743
  const messageModules = import.meta.glob(
2565
2744
  "/src/app/messages/**/*.message.{ts,js}",
2566
2745
  { eager: true }
@@ -2575,10 +2754,10 @@ class ApplicationBootstrap {
2575
2754
  const messageName = messageMetadata || MessageClass.name;
2576
2755
  const messageInstance = container.get(messageName);
2577
2756
  await messageInstance.boot();
2578
- this.logger.success(`\u2705 Booted message: ${messageName}`);
2757
+ this.logger.success(`Booted message: ${messageName}`);
2579
2758
  return { messageName, success: true };
2580
2759
  } catch (error) {
2581
- this.logger.error(`\u274C Failed to boot message ${MessageClass.name}:`, error);
2760
+ this.logger.error(`Failed to boot message ${MessageClass.name}:`, error);
2582
2761
  return { messageName: MessageClass.name, success: false, error };
2583
2762
  }
2584
2763
  });
@@ -2588,7 +2767,7 @@ class ApplicationBootstrap {
2588
2767
  * Register jobs for scheduled execution
2589
2768
  */
2590
2769
  async registerJobs() {
2591
- this.logger.info("\u{1F552} Registering jobs...");
2770
+ this.logger.info("Registering jobs...");
2592
2771
  const jobModules = import.meta.glob(
2593
2772
  "/src/app/jobs/**/*.job.{ts,js}",
2594
2773
  { eager: true }
@@ -2599,10 +2778,10 @@ class ApplicationBootstrap {
2599
2778
  this.logger.debug("container isBound(Scheduler)", { isBound: container.isBound(Scheduler) });
2600
2779
  for (const [name, ServiceClass] of this.serviceRegistry) {
2601
2780
  if (!ServiceClass) {
2602
- this.logger.warn(`\u26A0\uFE0F Service not found in registry: ${name}`);
2781
+ this.logger.warn(`Service not found in registry: ${name}`);
2603
2782
  }
2604
2783
  if (!container.isBound(ServiceClass)) {
2605
- this.logger.warn(`\u26A0\uFE0F Service not bound in container: ${name}`);
2784
+ this.logger.warn(`Service not bound in container: ${name}`);
2606
2785
  } else {
2607
2786
  container.get(ServiceClass);
2608
2787
  }
@@ -2622,23 +2801,43 @@ class ApplicationBootstrap {
2622
2801
  container.bind(id).to(JobClass).inSingletonScope();
2623
2802
  const options = Reflect.getMetadata("job:options", JobClass) || {};
2624
2803
  jobEntries.push({ JobClass, jobName, id, options });
2625
- this.logger.debug(`\u{1F4E6} Bound job: ${jobName}`);
2804
+ this.jobRegistry.set(jobName, JobClass);
2805
+ this.logger.debug(`Bound job: ${jobName}`);
2626
2806
  } catch (error) {
2627
- this.logger.error(`\u274C Failed to bind job ${JobClass.name}:`, error);
2807
+ this.logger.error(`Failed to bind job ${JobClass.name}:`, error);
2628
2808
  }
2629
2809
  }
2630
2810
  for (const { JobClass, jobName, id, options } of jobEntries) {
2631
2811
  try {
2632
2812
  const instance = container.get(JobClass);
2633
2813
  JobRegistry.instance.register(id, instance, options);
2814
+ if (typeof instance.onBoot === "function") {
2815
+ this.logger.info(`Executing onBoot for job: ${jobName}`);
2816
+ try {
2817
+ if (options.requiresPopup) {
2818
+ const isPopupVisible = PopupVisibilityService.instance.isPopupVisible();
2819
+ if (!isPopupVisible) {
2820
+ this.logger.debug(`Skipping onBoot for job ${jobName} - popup not visible`);
2821
+ } else {
2822
+ await instance.onBoot();
2823
+ this.logger.debug(`Executed onBoot for job: ${jobName}`);
2824
+ }
2825
+ } else {
2826
+ await instance.onBoot();
2827
+ this.logger.debug(`Executed onBoot for job: ${jobName}`);
2828
+ }
2829
+ } catch (error) {
2830
+ this.logger.error(`Failed to execute onBoot for job ${jobName}:`, error);
2831
+ }
2832
+ }
2634
2833
  if (!options.startPaused) {
2635
2834
  this.scheduler.schedule(id, options);
2636
2835
  } else {
2637
- this.logger.info(`\u23F8\uFE0F Job ${jobName} registered but paused (startPaused: true)`);
2836
+ this.logger.info(`Job ${jobName} registered but paused (startPaused: true)`);
2638
2837
  }
2639
- this.logger.success(`\u2705 Registered job: ${jobName}`);
2838
+ this.logger.success(`Registered job: ${jobName}`);
2640
2839
  } catch (error) {
2641
- this.logger.error(`\u274C Failed to register job ${JobClass.name}:`, error);
2840
+ this.logger.error(`Failed to register job ${JobClass.name}:`, error);
2642
2841
  }
2643
2842
  }
2644
2843
  }
@@ -2723,5 +2922,5 @@ class BootstrapBuilder {
2723
2922
  }
2724
2923
  }
2725
2924
 
2726
- export { JobRegistry as J, NonceService as N, PopupVisibilityService as P, Scheduler as S, getPopupVisibilityService as a, arePortsClaimed as b, claimEarlyPorts as c, container as d, create as e, bootstrap as f, getNonceService as g, JobState as h, isEarlyListenerSetup as i, setupEarlyListener as s };
2727
- //# sourceMappingURL=boot-Bty0U5M4.js.map
2925
+ export { AppEventBus as A, EventBusToken as E, JobRegistry as J, NonceService as N, PopupVisibilityService as P, Subscribe as S, SUBSCRIBE_METADATA_KEY as a, getNonceService as b, getPopupVisibilityService as c, claimEarlyPorts as d, arePortsClaimed as e, container as f, getSubscribeMetadata as g, create as h, isEarlyListenerSetup as i, bootstrap as j, Scheduler as k, JobState as l, setupEarlyListener as s };
2926
+ //# sourceMappingURL=boot-C2Rq9czO.js.map