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.
- package/README.md +89 -6
- package/bin/cli.js +769 -3
- package/package.json +15 -5
- package/src/angular/builders.js +37 -0
- package/src/angular/helpers.js +102 -0
- package/src/angular/index.js +189 -0
- package/src/angular/services.js +32 -0
- package/src/builder/dependency-graph.js +65 -9
- package/src/builder/hook-processor.js +26 -4
- package/src/builder/utils.js +78 -22
- package/src/core/facet.js +16 -3
- package/src/index.js +18 -0
- package/src/manager/facet-manager.js +10 -2
- package/src/qwik/builders.js +39 -0
- package/src/qwik/index.js +178 -0
- package/src/qwik/listeners.js +96 -0
- package/src/qwik/queues.js +87 -0
- package/src/qwik/signals.js +32 -0
- package/src/react/README.md +3 -0
- package/src/solid/README.md +69 -0
- package/src/solid/index.js +387 -0
- package/src/svelte/builders.js +43 -0
- package/src/svelte/index.js +183 -0
- package/src/svelte/listeners.js +96 -0
- package/src/svelte/queues.js +114 -0
- package/src/svelte/stores.js +36 -0
- package/src/utils/instrumentation.js +204 -0
- package/src/utils/use-base.js +205 -30
- package/src/vue/builders.js +40 -0
- package/src/vue/composables.js +37 -0
- package/src/vue/index.js +252 -0
- package/src/vue/listeners.js +78 -0
- package/src/vue/queues.js +113 -0
package/src/builder/utils.js
CHANGED
|
@@ -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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
+
|