mycelia-kernel-plugin 1.2.0 → 1.4.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.
@@ -9,20 +9,35 @@ import {
9
9
  executeHooksAndCreateFacets,
10
10
  validateHookDependencies,
11
11
  } from './hook-processor.js';
12
+ import { instrumentBuildPhase } from '../utils/instrumentation.js';
12
13
 
13
14
  // Re-export deepMerge for backward compatibility
14
15
  export { deepMerge } from './context-resolver.js';
15
16
 
16
17
  /**
17
- * VERIFY (pure):
18
- * - resolve ctx (pure)
19
- * - collect hooks (defaults + user)
20
- * - instantiate facets
21
- * - validate + topo-sort (with caching)
18
+ * VERIFY (pure, side-effect-free):
19
+ *
20
+ * This is the first phase of the two-phase build process. It:
21
+ * 1. Resolves context (pure operation)
22
+ * 2. Collects hooks (defaults + user)
23
+ * 3. Orders hooks by dependencies (topological sort)
24
+ * 4. Executes hooks to create facets (temporary, for dependency lookups)
25
+ * 5. Validates facets against contracts
26
+ * 6. Builds dependency graph and sorts facets (with caching)
27
+ *
28
+ * Key property: This phase is pure - it doesn't mutate the subsystem state.
29
+ * Facets are temporarily added to api.__facets for dependency lookups only.
30
+ *
31
+ * @param {BaseSubsystem} subsystem - Subsystem to verify
32
+ * @param {Object} ctx - Context to merge
33
+ * @param {DependencyGraphCache} graphCache - Optional cache for dependency graphs
34
+ * @returns {Object} Build plan with { resolvedCtx, orderedKinds, facetsByKind, graphCache }
22
35
  */
23
36
  export function verifySubsystemBuild(subsystem, ctx = {}, graphCache = null) {
37
+ // Step 1: Resolve context (deep merge with subsystem.ctx)
24
38
  const resolvedCtx = resolveCtx(subsystem, ctx);
25
39
 
40
+ // Step 2: Collect all hooks (defaults + user)
26
41
  // Check whether the defaults are defined as a DefaultHooks instance or an array of hooks.
27
42
  // If it's a DefaultHooks instance, use the list() method to get the array of hooks.
28
43
  const defaults = Array.isArray(subsystem.defaultHooks)
@@ -32,22 +47,28 @@ export function verifySubsystemBuild(subsystem, ctx = {}, graphCache = null) {
32
47
  const user = Array.isArray(subsystem.hooks) ? subsystem.hooks : [];
33
48
  const hooks = [...defaults, ...user];
34
49
 
35
- // Extract hook metadata
50
+ // Step 3: Extract hook metadata (kind, required, overwrite, etc.)
36
51
  const hooksByKind = extractHookMetadata(hooks);
37
52
 
38
- // Order hooks based on dependencies
53
+ // Step 4: Order hooks based on their dependencies (topological sort)
54
+ // This ensures hooks are executed in the correct order
39
55
  const orderedHooks = orderHooksByDependencies(hooks);
40
56
 
41
- // Execute hooks and create facets
57
+ // Step 5: Execute hooks and create facets
58
+ // Facets are created here temporarily so later hooks can access them via subsystem.find()
59
+ // They will be properly initialized/attached in the execute phase
42
60
  const { facetsByKind } = executeHooksAndCreateFacets(orderedHooks, resolvedCtx, subsystem, hooksByKind);
43
61
 
44
- // Validate facets against their contracts (before dependency graph building)
62
+ // Step 6: Validate facets against their contracts (before dependency graph building)
63
+ // This ensures all facets satisfy their declared contracts
45
64
  validateFacets(facetsByKind, resolvedCtx, subsystem, defaultContractRegistry);
46
65
 
47
- // Validate hook.required dependencies exist
66
+ // Step 7: Validate hook.required dependencies exist
67
+ // Double-check that all declared dependencies are satisfied
48
68
  validateHookDependencies(hooksByKind, facetsByKind, subsystem);
49
69
 
50
- // Create cache key from sorted facet kinds
70
+ // Step 8: Create cache key from sorted facet kinds
71
+ // This key is used to cache the dependency graph computation
51
72
  const kinds = Object.keys(facetsByKind);
52
73
  const cacheKey = graphCache ? createCacheKey(kinds) : null;
53
74
 
@@ -56,21 +77,25 @@ export function verifySubsystemBuild(subsystem, ctx = {}, graphCache = null) {
56
77
  resolvedCtx.graphCache = graphCache;
57
78
  }
58
79
 
59
- // Check cache before building graph
80
+ // Step 9: Check cache before building graph
81
+ // If we've computed this dependency graph before, reuse the result
60
82
  if (graphCache && cacheKey) {
61
83
  const cached = graphCache.get(cacheKey);
62
84
  if (cached) {
63
85
  if (cached.valid) {
64
86
  // Return cached result (skip graph building and sorting)
87
+ // This significantly speeds up repeated builds with the same dependency structure
65
88
  return { resolvedCtx, orderedKinds: cached.orderedKinds, facetsByKind, graphCache };
66
89
  } else {
67
- // Throw cached error
90
+ // Throw cached error (don't recompute known-invalid graph)
68
91
  throw new Error(cached.error || 'Cached dependency graph error');
69
92
  }
70
93
  }
71
94
  }
72
95
 
73
- // Build graph and sort (will cache result in topoSort)
96
+ // Step 10: Build dependency graph and sort facets
97
+ // This computes the initialization order based on facet dependencies
98
+ // The result will be cached in topoSort for future use
74
99
  const graph = buildDepGraph(hooksByKind, facetsByKind, subsystem);
75
100
  const orderedKinds = topoSort(graph, graphCache, cacheKey);
76
101
 
@@ -79,17 +104,35 @@ export function verifySubsystemBuild(subsystem, ctx = {}, graphCache = null) {
79
104
 
80
105
  /**
81
106
  * EXECUTE (transactional):
82
- * - assign resolved ctx
83
- * - add/init/attach facets via FacetManager.addMany
84
- * - build children
107
+ *
108
+ * This is the second phase of the two-phase build process. It:
109
+ * 1. Assigns resolved context to subsystem
110
+ * 2. Separates facets into new vs overwrite
111
+ * 3. Removes overwritten facets
112
+ * 4. Adds/initializes/attaches facets via FacetManager.addMany (transactional)
113
+ * 5. Builds child subsystems recursively
114
+ *
115
+ * Key property: This phase is transactional - if any step fails, all changes are rolled back.
116
+ * Facets are properly initialized (onInit callbacks) and attached to the subsystem.
117
+ *
118
+ * @param {BaseSubsystem} subsystem - Subsystem to build
119
+ * @param {Object} plan - Build plan from verifySubsystemBuild
85
120
  */
86
121
  export async function buildSubsystem(subsystem, plan) {
122
+ // Instrument the build phase if enabled
123
+ return instrumentBuildPhase(subsystem, async () => {
124
+ return buildSubsystemInternal(subsystem, plan);
125
+ });
126
+ }
127
+
128
+ async function buildSubsystemInternal(subsystem, plan) {
87
129
  if (!plan) throw new Error('buildSubsystem: invalid plan');
88
130
  const { resolvedCtx, orderedKinds, facetsByKind } = plan;
89
131
  if (!Array.isArray(orderedKinds)) throw new Error('buildSubsystem: invalid plan');
90
132
  if (!facetsByKind || typeof facetsByKind !== 'object' || Array.isArray(facetsByKind)) throw new Error('buildSubsystem: invalid plan');
91
133
 
92
134
  // Validate consistency: if one is non-empty, the other must match
135
+ // This ensures the plan is internally consistent
93
136
  const hasOrderedKinds = orderedKinds.length > 0;
94
137
  const hasFacetsByKind = Object.keys(facetsByKind).length > 0;
95
138
 
@@ -99,14 +142,17 @@ export async function buildSubsystem(subsystem, plan) {
99
142
  if (hasOrderedKinds && !hasFacetsByKind) throw new Error('buildSubsystem: invalid plan');
100
143
  // Both empty is valid (no facets to add)
101
144
 
145
+ // Step 1: Assign resolved context to subsystem
102
146
  subsystem.ctx = resolvedCtx;
103
147
 
104
- // Separate facets into new and overwrite
148
+ // Step 2: Separate facets into new and overwrite categories
149
+ // This allows us to handle overwrites correctly (remove old, add new)
105
150
  const facetsToAdd = {};
106
151
  const kindsToAdd = [];
107
152
  const facetsToOverwrite = {};
108
153
  const kindsToOverwrite = [];
109
154
 
155
+ // Process facets in dependency order (from topological sort)
110
156
  for (const kind of orderedKinds) {
111
157
  const facet = facetsByKind[kind];
112
158
  const existingFacet = subsystem.api.__facets.find(kind);
@@ -118,13 +164,14 @@ export async function buildSubsystem(subsystem, plan) {
118
164
  } else if (existingFacet === facet) {
119
165
  // Same facet instance - this was added during verify phase for dependency lookups
120
166
  // It needs to be properly initialized/attached, so add it
167
+ // This handles the case where facets are temporarily added in verify phase
121
168
  facetsToAdd[kind] = facet;
122
169
  kindsToAdd.push(kind);
123
170
  } else {
124
171
  // Different facet instance - check if we can overwrite
125
172
  const canOverwrite = facet.shouldOverwrite?.() === true;
126
173
  if (canOverwrite) {
127
- // Remove old facet first, then add new one
174
+ // Mark for overwrite: remove old facet first, then add new one
128
175
  facetsToOverwrite[kind] = facet;
129
176
  kindsToOverwrite.push(kind);
130
177
  } else {
@@ -134,7 +181,8 @@ export async function buildSubsystem(subsystem, plan) {
134
181
  }
135
182
  }
136
183
 
137
- // First, remove overwritten facets
184
+ // Step 3: Remove overwritten facets first (before adding new ones)
185
+ // This ensures clean state before adding replacements
138
186
  for (const kind of kindsToOverwrite) {
139
187
  subsystem.api.__facets.remove(kind);
140
188
  // Also remove from subsystem property if it exists
@@ -142,16 +190,22 @@ export async function buildSubsystem(subsystem, plan) {
142
190
  try {
143
191
  delete subsystem[kind];
144
192
  } catch {
145
- // Best-effort cleanup
193
+ // Best-effort cleanup (property might be non-configurable)
146
194
  }
147
195
  }
148
196
  }
149
197
 
150
- // Then add all facets (new + overwritten)
198
+ // Step 4: Add all facets (new + overwritten) in a single transactional operation
199
+ // This ensures atomicity - if any facet fails to initialize, all are rolled back
151
200
  const allFacets = { ...facetsToAdd, ...facetsToOverwrite };
152
201
  const allKinds = [...kindsToAdd, ...kindsToOverwrite];
153
202
 
154
203
  if (allKinds.length > 0) {
204
+ // addMany is transactional - it will:
205
+ // 1. Register all facets
206
+ // 2. Initialize them (call onInit callbacks) in parallel within dependency levels
207
+ // 3. Attach them to the subsystem
208
+ // 4. Roll back everything if any initialization fails
155
209
  await subsystem.api.__facets.addMany(allKinds, allFacets, {
156
210
  init: true,
157
211
  attach: true,
@@ -160,6 +214,8 @@ export async function buildSubsystem(subsystem, plan) {
160
214
  });
161
215
  }
162
216
 
217
+ // Step 5: Build child subsystems recursively
218
+ // This allows nested plugin systems with their own dependency graphs
163
219
  await buildChildren(subsystem);
164
220
  }
165
221
 
package/src/core/facet.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getDefaultVersion, isValidSemver } from '../utils/semver.js';
2
+ import { instrumentFacetInit, instrumentDisposeCallback } from '../utils/instrumentation.js';
2
3
 
3
4
  export class Facet {
4
5
  #kind;
@@ -121,15 +122,27 @@ export class Facet {
121
122
 
122
123
  async init(ctx, api, subsystem) {
123
124
  if (this.#isInit) return;
124
- if (this.#initCallback) {
125
+
126
+ // Use instrumentation if available and enabled, otherwise call callback directly
127
+ if (subsystem && typeof instrumentFacetInit === 'function') {
128
+ // Instrumentation will handle timing and call the callback
129
+ await instrumentFacetInit(this, ctx, api, subsystem, this.#initCallback);
130
+ } else if (this.#initCallback) {
131
+ // No instrumentation - call callback directly
125
132
  await this.#initCallback({ ctx, api, subsystem, facet: this });
126
133
  }
134
+
127
135
  this.#isInit = true;
128
136
  Object.freeze(this);
129
137
  }
130
138
 
131
- async dispose() {
132
- if (this.#disposeCallback) {
139
+ async dispose(subsystem) {
140
+ // Use instrumentation if available and enabled, otherwise call callback directly
141
+ if (subsystem && this.#disposeCallback && typeof instrumentDisposeCallback === 'function') {
142
+ // Instrumentation will handle timing and call the callback
143
+ await instrumentDisposeCallback(this, subsystem, this.#disposeCallback);
144
+ } else if (this.#disposeCallback) {
145
+ // No instrumentation - call callback directly
133
146
  await this.#disposeCallback(this);
134
147
  }
135
148
  }
package/src/index.js CHANGED
@@ -34,11 +34,29 @@ export { useListeners } from './hooks/listeners/use-listeners.js';
34
34
  export { useQueue } from './hooks/queue/use-queue.js';
35
35
  export { useSpeak } from './hooks/speak/use-speak.js';
36
36
 
37
+ // Framework bindings are available via subpath exports:
38
+ // - 'mycelia-kernel-plugin/react' for React bindings
39
+ // - 'mycelia-kernel-plugin/vue' for Vue bindings
40
+ // - 'mycelia-kernel-plugin/svelte' for Svelte bindings
41
+ // - 'mycelia-kernel-plugin/angular' for Angular bindings
42
+ // - 'mycelia-kernel-plugin/qwik' for Qwik bindings
43
+ // - 'mycelia-kernel-plugin/solid' for Solid.js bindings
44
+ //
45
+ // They are not re-exported from the main entry point to avoid
46
+ // requiring framework dependencies when using the core system.
47
+
37
48
  // Utility exports
38
49
  export { createLogger, createSubsystemLogger } from './utils/logger.js';
39
50
  export { getDebugFlag } from './utils/debug-flag.js';
40
51
  export { findFacet } from './utils/find-facet.js';
41
52
  export { useBase } from './utils/use-base.js';
53
+ export {
54
+ isInstrumentationEnabled,
55
+ instrumentHookExecution,
56
+ instrumentFacetInit,
57
+ instrumentDisposeCallback,
58
+ instrumentBuildPhase
59
+ } from './utils/instrumentation.js';
42
60
  export {
43
61
  parseVersion,
44
62
  isValidSemver,
@@ -1,5 +1,6 @@
1
1
  import { FacetManagerTransaction } from './facet-manager-transaction.js';
2
2
  import { createSubsystemLogger } from '../utils/logger.js';
3
+ import { instrumentFacetInit, instrumentDisposeCallback } from '../utils/instrumentation.js';
3
4
 
4
5
  export class FacetManager {
5
6
  #facets = new Map(); // Map<kind, Array<facet>> - stores arrays of facets per kind, sorted by orderIndex
@@ -80,6 +81,7 @@ export class FacetManager {
80
81
  // 2) Init now
81
82
  try {
82
83
  if (opts.init && typeof facet.init === 'function') {
84
+ // facet.init() will handle instrumentation internally
83
85
  await facet.init(opts.ctx, opts.api, this.#subsystem);
84
86
  }
85
87
  } catch (err) {
@@ -546,14 +548,20 @@ export class FacetManager {
546
548
  if (Array.isArray(facets)) {
547
549
  for (const facet of facets) {
548
550
  if (typeof facet.dispose === 'function') {
549
- try { await facet.dispose(subsystem); }
551
+ try {
552
+ // facet.dispose() will handle instrumentation internally
553
+ await facet.dispose(subsystem);
554
+ }
550
555
  catch (e) { errors.push({ kind, error: e }); }
551
556
  }
552
557
  }
553
558
  } else {
554
559
  // Legacy: single facet
555
560
  if (typeof facets.dispose === 'function') {
556
- try { await facets.dispose(subsystem); }
561
+ try {
562
+ // facet.dispose() will handle instrumentation internally
563
+ await facets.dispose(subsystem);
564
+ }
557
565
  catch (e) { errors.push({ kind, error: e }); }
558
566
  }
559
567
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Qwik Builder Helpers
3
+ */
4
+
5
+ /**
6
+ * createQwikSystemBuilder - Create a reusable system builder function for Qwik
7
+ *
8
+ * @param {string} name - System name
9
+ * @param {Function} configure - Configuration function: (builder) => builder
10
+ * @returns {Function} Build function: () => Promise<System>
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { useBase } from 'mycelia-kernel-plugin';
15
+ *
16
+ * const buildTodoSystem = createQwikSystemBuilder('todo-app', (b) =>
17
+ * b
18
+ * .config('database', { host: 'localhost' })
19
+ * .use(useDatabase)
20
+ * .use(useListeners)
21
+ * );
22
+ *
23
+ * // Then use in Provider
24
+ * <MyceliaProvider build={buildTodoSystem}>
25
+ * <App />
26
+ * </MyceliaProvider>
27
+ * ```
28
+ */
29
+ export function createQwikSystemBuilder(name, configure) {
30
+ return async function build() {
31
+ // Import useBase - users should have it available
32
+ const { useBase } = await import('../utils/use-base.js');
33
+ let builder = useBase(name);
34
+ builder = configure(builder);
35
+ return builder.build();
36
+ };
37
+ }
38
+
39
+
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Mycelia Plugin System - Qwik Bindings
3
+ *
4
+ * Qwik utilities that make the Mycelia Plugin System feel natural
5
+ * inside Qwik applications using signals and context.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { MyceliaProvider, useFacet, useListener } from 'mycelia-kernel-plugin/qwik';
10
+ *
11
+ * const buildSystem = () => useBase('app')
12
+ * .use(useDatabase)
13
+ * .build();
14
+ *
15
+ * export default component$(() => {
16
+ * return (
17
+ * <MyceliaProvider build={buildSystem}>
18
+ * <MyComponent />
19
+ * </MyceliaProvider>
20
+ * );
21
+ * });
22
+ *
23
+ * export const MyComponent = component$(() => {
24
+ * const db = useFacet('database');
25
+ * useListener('user:created', (msg) => console.log(msg));
26
+ * // ...
27
+ * });
28
+ * ```
29
+ */
30
+
31
+ import { createContextId, useContextProvider, useContext, useSignal, useStore, useTask$, component$, Slot } from '@builder.io/qwik';
32
+
33
+ // ============================================================================
34
+ // Core Bindings: Provider + Basic Hooks
35
+ // ============================================================================
36
+
37
+ const MyceliaContextId = createContextId('mycelia');
38
+
39
+ /**
40
+ * MyceliaProvider - Provides Mycelia system to Qwik component tree
41
+ *
42
+ * @param {Object} props
43
+ * @param {Function} props.build - Async function that returns a built system
44
+ * @param {import('@builder.io/qwik').QRL} props.children - Child components
45
+ * @param {import('@builder.io/qwik').QRL} [props.fallback] - Optional loading/fallback component
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * export default component$(() => {
50
+ * return (
51
+ * <MyceliaProvider build={buildSystem}>
52
+ * <App />
53
+ * </MyceliaProvider>
54
+ * );
55
+ * });
56
+ * ```
57
+ */
58
+ export const MyceliaProvider = component$(({ build, fallback = null }) => {
59
+ const system = useSignal(null);
60
+ const error = useSignal(null);
61
+ const loading = useSignal(true);
62
+
63
+ useTask$(async () => {
64
+ try {
65
+ const builtSystem = await build();
66
+ system.value = builtSystem;
67
+ error.value = null;
68
+ } catch (err) {
69
+ error.value = err;
70
+ system.value = null;
71
+ } finally {
72
+ loading.value = false;
73
+ }
74
+ });
75
+
76
+ const context = {
77
+ system,
78
+ error,
79
+ loading
80
+ };
81
+
82
+ useContextProvider(MyceliaContextId, context);
83
+
84
+ if (error.value) {
85
+ throw error.value; // Let error boundaries handle it
86
+ }
87
+
88
+ if (loading.value) {
89
+ return fallback || <div>Loading...</div>;
90
+ }
91
+
92
+ return <Slot />;
93
+ });
94
+
95
+ /**
96
+ * useMycelia - Get the Mycelia system from context
97
+ *
98
+ * @returns {Object} The Mycelia system instance
99
+ * @throws {Error} If used outside MyceliaProvider
100
+ *
101
+ * @example
102
+ * ```tsx
103
+ * export const MyComponent = component$(() => {
104
+ * const system = useMycelia();
105
+ * // Use system.find(), system.listeners, etc.
106
+ * });
107
+ * ```
108
+ */
109
+ export function useMycelia() {
110
+ const context = useContext(MyceliaContextId);
111
+ if (!context) {
112
+ throw new Error('useMycelia must be used within a MyceliaProvider');
113
+ }
114
+ return context.system.value;
115
+ }
116
+
117
+ /**
118
+ * useMyceliaContext - Get the full Mycelia context (system, loading, error)
119
+ *
120
+ * @returns {Object} Context object with system, loading, and error signals
121
+ * @throws {Error} If used outside MyceliaProvider
122
+ *
123
+ * @example
124
+ * ```tsx
125
+ * export const MyComponent = component$(() => {
126
+ * const { system, loading, error } = useMyceliaContext();
127
+ * if (loading.value) return <div>Loading...</div>;
128
+ * if (error.value) return <div>Error: {error.value.message}</div>;
129
+ * return <div>System: {system.value.name}</div>;
130
+ * });
131
+ * ```
132
+ */
133
+ export function useMyceliaContext() {
134
+ const context = useContext(MyceliaContextId);
135
+ if (!context) {
136
+ throw new Error('useMyceliaContext must be used within a MyceliaProvider');
137
+ }
138
+ return context;
139
+ }
140
+
141
+ /**
142
+ * useFacet - Get a facet by kind from the system (reactive signal)
143
+ *
144
+ * @param {string} kind - Facet kind identifier
145
+ * @returns {import('@builder.io/qwik').Signal} Signal containing the facet instance, or null if not found
146
+ *
147
+ * @example
148
+ * ```tsx
149
+ * export const UserList = component$(() => {
150
+ * const db = useFacet('database');
151
+ * // Use db.value.query(), etc.
152
+ * });
153
+ * ```
154
+ */
155
+ export function useFacet(kind) {
156
+ const context = useMyceliaContext();
157
+ const facet = useSignal(null);
158
+
159
+ useTask$(({ track }) => {
160
+ const system = track(() => context.system.value);
161
+ facet.value = system?.find?.(kind) ?? null;
162
+ });
163
+
164
+ return facet;
165
+ }
166
+
167
+ // Re-export listener helpers
168
+ export { useListener, useEventStream } from './listeners.js';
169
+
170
+ // Re-export queue helpers
171
+ export { useQueueStatus, useQueueDrain } from './queues.js';
172
+
173
+ // Re-export builder helpers
174
+ export { createQwikSystemBuilder } from './builders.js';
175
+
176
+ // Re-export facet signal generator
177
+ export { createFacetSignal } from './signals.js';
178
+
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Qwik Listener Helpers
3
+ */
4
+
5
+ import { useTask$, useSignal } from '@builder.io/qwik';
6
+ import { useMycelia } from './index.js';
7
+
8
+ // Note: Qwik's useTask$ requires proper serialization
9
+ // Event handlers should be QRL-wrapped for proper serialization
10
+
11
+ /**
12
+ * useListener - Register an event listener with automatic cleanup
13
+ *
14
+ * @param {string} eventName - Event name/path to listen for
15
+ * @param {Function} handler - Handler function: (message) => void
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * export const AuditLog = component$(() => {
20
+ * useListener('user:created', (msg) => {
21
+ * console.log('User created:', msg.body);
22
+ * });
23
+ * // ...
24
+ * });
25
+ * ```
26
+ */
27
+ export function useListener(eventName, handler) {
28
+ const system = useMycelia();
29
+ const listeners = system.listeners; // useListeners facet
30
+
31
+ useTask$(({ cleanup }) => {
32
+ if (!listeners || !listeners.hasListeners?.()) {
33
+ return;
34
+ }
35
+
36
+ const wrappedHandler = (msg) => {
37
+ handler(msg);
38
+ };
39
+
40
+ listeners.on(eventName, wrappedHandler);
41
+
42
+ cleanup(() => {
43
+ listeners.off?.(eventName, wrappedHandler);
44
+ });
45
+ });
46
+ }
47
+
48
+ /**
49
+ * useEventStream - Subscribe to events and keep them in Qwik signal
50
+ *
51
+ * @param {string} eventName - Event name/path to listen for
52
+ * @param {Object} [options={}] - Options
53
+ * @param {boolean} [options.accumulate=false] - If true, accumulate events in array
54
+ * @returns {import('@builder.io/qwik').Signal} Signal containing latest event value, array of events, or null
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * export const EventList = component$(() => {
59
+ * const events = useEventStream('todo:created', { accumulate: true });
60
+ * return (
61
+ * <ul>
62
+ * {events.value?.map(e => <li key={e.id}>{e.text}</li>)}
63
+ * </ul>
64
+ * );
65
+ * });
66
+ * ```
67
+ */
68
+ export function useEventStream(eventName, options = {}) {
69
+ const { accumulate = false } = options;
70
+ const value = useSignal(accumulate ? [] : null);
71
+ const system = useMycelia();
72
+ const listeners = system.listeners;
73
+
74
+ useTask$(({ cleanup }) => {
75
+ if (!listeners || !listeners.hasListeners?.()) {
76
+ return;
77
+ }
78
+
79
+ const handler = (msg) => {
80
+ if (accumulate) {
81
+ value.value = [...value.value, msg.body];
82
+ } else {
83
+ value.value = msg.body;
84
+ }
85
+ };
86
+
87
+ listeners.on(eventName, handler);
88
+
89
+ cleanup(() => {
90
+ listeners.off?.(eventName, handler);
91
+ });
92
+ });
93
+
94
+ return value;
95
+ }
96
+