@hypen-space/core 0.4.37 → 0.4.38
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 +13 -14
- package/dist/app.js +289 -227
- package/dist/app.js.map +5 -5
- package/dist/components/builtin.js +289 -227
- package/dist/components/builtin.js.map +5 -5
- package/dist/context.js +60 -64
- package/dist/context.js.map +2 -2
- package/dist/datasource.js +80 -0
- package/dist/datasource.js.map +10 -0
- package/dist/disposable.js +60 -63
- package/dist/disposable.js.map +2 -2
- package/dist/events.js +60 -63
- package/dist/events.js.map +2 -2
- package/dist/hypen.js +78 -0
- package/dist/hypen.js.map +10 -0
- package/dist/index.browser.d.ts +2 -1
- package/dist/index.browser.js +303 -244
- package/dist/index.browser.js.map +6 -7
- package/dist/index.d.ts +0 -3
- package/dist/index.js +492 -180
- package/dist/index.js.map +6 -6
- package/dist/logger.js +60 -64
- package/dist/logger.js.map +2 -2
- package/dist/remote/client.js +243 -158
- package/dist/remote/client.js.map +6 -6
- package/dist/remote/index.d.ts +6 -5
- package/dist/remote/index.js +241 -1986
- package/dist/remote/index.js.map +6 -13
- package/dist/remote/session.js +151 -0
- package/dist/remote/session.js.map +10 -0
- package/dist/renderer.js +60 -63
- package/dist/renderer.js.map +2 -2
- package/dist/result.js +220 -0
- package/dist/result.js.map +10 -0
- package/dist/retry.js +329 -0
- package/dist/retry.js.map +11 -0
- package/dist/router.js +62 -70
- package/dist/router.js.map +2 -2
- package/dist/state.js +3 -8
- package/dist/state.js.map +2 -2
- package/package.json +11 -56
- package/src/index.browser.ts +5 -4
- package/src/index.ts +10 -23
- package/src/remote/index.ts +9 -5
- package/dist/discovery.d.ts +0 -90
- package/dist/discovery.js +0 -1334
- package/dist/discovery.js.map +0 -15
- package/dist/engine.browser.d.ts +0 -116
- package/dist/engine.browser.js +0 -479
- package/dist/engine.browser.js.map +0 -12
- package/dist/engine.d.ts +0 -107
- package/dist/engine.js +0 -543
- package/dist/engine.js.map +0 -12
- package/dist/loader.d.ts +0 -51
- package/dist/loader.js +0 -292
- package/dist/loader.js.map +0 -11
- package/dist/plugin.d.ts +0 -39
- package/dist/plugin.js +0 -685
- package/dist/plugin.js.map +0 -12
- package/dist/remote/server.d.ts +0 -188
- package/dist/remote/server.js +0 -2270
- package/dist/remote/server.js.map +0 -19
- package/src/discovery.ts +0 -527
- package/src/engine.browser.ts +0 -302
- package/src/engine.ts +0 -282
- package/src/loader.ts +0 -136
- package/src/plugin.ts +0 -220
- package/src/remote/server.ts +0 -879
- package/wasm-browser/README.md +0 -594
- package/wasm-browser/hypen_engine.d.ts +0 -389
- package/wasm-browser/hypen_engine.js +0 -1070
- package/wasm-browser/hypen_engine_bg.wasm +0 -0
- package/wasm-browser/hypen_engine_bg.wasm.d.ts +0 -37
- package/wasm-browser/package.json +0 -24
- package/wasm-node/README.md +0 -594
- package/wasm-node/hypen_engine.d.ts +0 -327
- package/wasm-node/hypen_engine.js +0 -979
- package/wasm-node/hypen_engine_bg.wasm +0 -0
- package/wasm-node/hypen_engine_bg.wasm.d.ts +0 -37
- package/wasm-node/package.json +0 -22
package/src/engine.browser.ts
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-compatible wrapper around the WASM engine
|
|
3
|
-
* Uses web target for browser environments
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { frameworkLoggers } from "./logger.js";
|
|
7
|
-
import { classifyEngineError } from "./result.js";
|
|
8
|
-
import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
|
|
9
|
-
|
|
10
|
-
// Re-export types so existing consumers of "./engine.browser.js" still work
|
|
11
|
-
export type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
|
|
12
|
-
|
|
13
|
-
const log = frameworkLoggers.engine;
|
|
14
|
-
|
|
15
|
-
// Dynamic import path - will be configured at build time
|
|
16
|
-
// For browser, the WASM needs to be served and initialized explicitly
|
|
17
|
-
let wasmInit: ((path?: string) => Promise<void>) | null = null;
|
|
18
|
-
let WasmEngineClass: any = null;
|
|
19
|
-
|
|
20
|
-
export interface EngineInitOptions {
|
|
21
|
-
/**
|
|
22
|
-
* URL to the WASM binary file.
|
|
23
|
-
* Default: loads from unpkg CDN
|
|
24
|
-
* For production, consider serving from your own domain for better performance.
|
|
25
|
-
*/
|
|
26
|
-
wasmUrl?: string;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* URL to the WASM JS glue code.
|
|
30
|
-
* Default: loads from unpkg CDN
|
|
31
|
-
* For self-hosting, point to your own copy of hypen_engine.js
|
|
32
|
-
*/
|
|
33
|
-
jsUrl?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Recursively convert Maps and nested structures to plain objects
|
|
38
|
-
*/
|
|
39
|
-
function mapToObject(value: any): any {
|
|
40
|
-
if (value instanceof Map) {
|
|
41
|
-
const obj: Record<string, any> = {};
|
|
42
|
-
for (const [key, val] of value.entries()) {
|
|
43
|
-
obj[key] = mapToObject(val);
|
|
44
|
-
}
|
|
45
|
-
return obj;
|
|
46
|
-
} else if (Array.isArray(value)) {
|
|
47
|
-
return value.map(mapToObject);
|
|
48
|
-
} else if (value && typeof value === 'object' && value.constructor === Object) {
|
|
49
|
-
const obj: Record<string, any> = {};
|
|
50
|
-
for (const [key, val] of Object.entries(value)) {
|
|
51
|
-
obj[key] = mapToObject(val);
|
|
52
|
-
}
|
|
53
|
-
return obj;
|
|
54
|
-
}
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Engine wraps the WASM engine and provides a TypeScript-friendly API
|
|
60
|
-
* Browser version with explicit WASM initialization
|
|
61
|
-
*/
|
|
62
|
-
export class Engine {
|
|
63
|
-
private wasmEngine: any = null;
|
|
64
|
-
private initialized = false;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Initialize the WASM module
|
|
68
|
-
* @param options - Initialization options including wasmUrl and jsUrl
|
|
69
|
-
*/
|
|
70
|
-
async init(options: EngineInitOptions = {}): Promise<void> {
|
|
71
|
-
if (this.initialized) return;
|
|
72
|
-
|
|
73
|
-
// Default to CDN for zero-config experience
|
|
74
|
-
const cdnBase = "https://unpkg.com/@hypen-space/core@latest/wasm-browser";
|
|
75
|
-
const jsUrl = options.jsUrl ?? `${cdnBase}/hypen_engine.js`;
|
|
76
|
-
const wasmUrl = options.wasmUrl ?? `${cdnBase}/hypen_engine_bg.wasm`;
|
|
77
|
-
|
|
78
|
-
// Dynamically import the WASM JS glue code from CDN (or custom URL)
|
|
79
|
-
// Using dynamic import with variable URL to avoid bundler resolution
|
|
80
|
-
try {
|
|
81
|
-
const wasmModule: any = await import(/* @vite-ignore */ jsUrl);
|
|
82
|
-
wasmInit = wasmModule.default;
|
|
83
|
-
WasmEngineClass = wasmModule.WasmEngine;
|
|
84
|
-
|
|
85
|
-
// Initialize WASM with explicit path
|
|
86
|
-
await wasmInit!(wasmUrl);
|
|
87
|
-
|
|
88
|
-
this.wasmEngine = new WasmEngineClass();
|
|
89
|
-
this.initialized = true;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
log.error("Failed to initialize WASM engine:", error);
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Ensure the engine is initialized before operations
|
|
98
|
-
*/
|
|
99
|
-
private ensureInitialized(): any {
|
|
100
|
-
if (!this.wasmEngine) {
|
|
101
|
-
throw new Error("Engine not initialized. Call init() first.");
|
|
102
|
-
}
|
|
103
|
-
return this.wasmEngine;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Set the render callback that receives patches
|
|
108
|
-
*/
|
|
109
|
-
setRenderCallback(callback: RenderCallback): void {
|
|
110
|
-
const engine = this.ensureInitialized();
|
|
111
|
-
engine.setRenderCallback((patches: Patch[]) => {
|
|
112
|
-
callback(patches);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Set the component resolver for dynamic component composition
|
|
118
|
-
*/
|
|
119
|
-
setComponentResolver(resolver: ComponentResolver): void {
|
|
120
|
-
const engine = this.ensureInitialized();
|
|
121
|
-
engine.setComponentResolver((componentName: string, contextPath: string | null) => {
|
|
122
|
-
const result = resolver(componentName, contextPath);
|
|
123
|
-
return result;
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Parse and render Hypen DSL source code
|
|
129
|
-
* @throws {ParseError} if the source fails to parse
|
|
130
|
-
* @throws {RenderError} if rendering fails
|
|
131
|
-
*/
|
|
132
|
-
renderSource(source: string): void {
|
|
133
|
-
const engine = this.ensureInitialized();
|
|
134
|
-
try {
|
|
135
|
-
engine.renderSource(source);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
throw classifyEngineError(err);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Render a lazy component (for lazy route loading)
|
|
143
|
-
*/
|
|
144
|
-
renderLazyComponent(source: string): void {
|
|
145
|
-
const engine = this.ensureInitialized();
|
|
146
|
-
engine.renderLazyComponent(source);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Render a component into a specific parent node (subtree rendering)
|
|
151
|
-
* @throws {ParseError} if the source fails to parse
|
|
152
|
-
* @throws {RenderError} if the parent node is not found or rendering fails
|
|
153
|
-
*/
|
|
154
|
-
renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
|
|
155
|
-
const engine = this.ensureInitialized();
|
|
156
|
-
const safeState = JSON.parse(JSON.stringify(state));
|
|
157
|
-
try {
|
|
158
|
-
engine.renderInto(source, parentNodeId, safeState);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
throw classifyEngineError(err);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Notify the engine of state changes using sparse updates
|
|
166
|
-
* @throws {StateError} if the state patch is invalid
|
|
167
|
-
*/
|
|
168
|
-
notifyStateChange(paths: string[], values: Record<string, any>): void {
|
|
169
|
-
const engine = this.ensureInitialized();
|
|
170
|
-
|
|
171
|
-
if (paths.length === 0) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const plainValues = JSON.parse(JSON.stringify(values));
|
|
176
|
-
try {
|
|
177
|
-
engine.updateStateSparse(paths, plainValues);
|
|
178
|
-
} catch (err) {
|
|
179
|
-
throw classifyEngineError(err);
|
|
180
|
-
}
|
|
181
|
-
log.debug("State changed (sparse):", paths);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Notify the engine of state changes using full state replacement
|
|
186
|
-
* @deprecated Use notifyStateChange with sparse values instead
|
|
187
|
-
*/
|
|
188
|
-
notifyStateChangeFull(paths: string[], currentState: Record<string, any>): void {
|
|
189
|
-
const engine = this.ensureInitialized();
|
|
190
|
-
const plainObject = JSON.parse(JSON.stringify(currentState));
|
|
191
|
-
engine.updateState(plainObject);
|
|
192
|
-
|
|
193
|
-
if (paths.length > 0) {
|
|
194
|
-
log.debug("State changed (full):", paths);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Update state (triggers re-render of affected nodes)
|
|
200
|
-
* @deprecated Use notifyStateChange instead
|
|
201
|
-
*/
|
|
202
|
-
updateState(statePatch: Record<string, any>): void {
|
|
203
|
-
const engine = this.ensureInitialized();
|
|
204
|
-
const plainObject = JSON.parse(JSON.stringify(statePatch));
|
|
205
|
-
engine.updateState(plainObject);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Dispatch an action
|
|
210
|
-
* @throws {HypenError} if the action dispatch fails
|
|
211
|
-
*/
|
|
212
|
-
dispatchAction(name: string, payload?: any): void {
|
|
213
|
-
const engine = this.ensureInitialized();
|
|
214
|
-
log.debug(`Action dispatched: ${name}`);
|
|
215
|
-
try {
|
|
216
|
-
engine.dispatchAction(name, payload ?? null);
|
|
217
|
-
} catch (err) {
|
|
218
|
-
throw classifyEngineError(err);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Register an action handler
|
|
224
|
-
*/
|
|
225
|
-
onAction(actionName: string, handler: ActionHandler): void {
|
|
226
|
-
const engine = this.ensureInitialized();
|
|
227
|
-
engine.onAction(actionName, (action: Action) => {
|
|
228
|
-
const normalizedAction: Action = {
|
|
229
|
-
...action,
|
|
230
|
-
payload: action.payload ? mapToObject(action.payload) : action.payload,
|
|
231
|
-
};
|
|
232
|
-
Promise.resolve(handler(normalizedAction)).catch((err) => log.error("Action handler error:", err));
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Initialize a module
|
|
238
|
-
*/
|
|
239
|
-
setModule(
|
|
240
|
-
name: string,
|
|
241
|
-
actions: string[],
|
|
242
|
-
stateKeys: string[],
|
|
243
|
-
initialState: Record<string, any>
|
|
244
|
-
): void {
|
|
245
|
-
const engine = this.ensureInitialized();
|
|
246
|
-
engine.setModule(name, actions, stateKeys, initialState);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Get the current revision number
|
|
251
|
-
*/
|
|
252
|
-
getRevision(): bigint {
|
|
253
|
-
const engine = this.ensureInitialized();
|
|
254
|
-
return engine.getRevision();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Clear the engine tree
|
|
259
|
-
*/
|
|
260
|
-
clearTree(): void {
|
|
261
|
-
const engine = this.ensureInitialized();
|
|
262
|
-
engine.clearTree();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Debug method to inspect parsed components
|
|
267
|
-
*/
|
|
268
|
-
debugParseComponent(source: string): string {
|
|
269
|
-
const engine = this.ensureInitialized();
|
|
270
|
-
return engine.debugParseComponent(source);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ── Data Source Context ────────────────────────────────────────
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Set (or replace) a named data source context.
|
|
277
|
-
*
|
|
278
|
-
* Registers the provider in the dependency graph, stores the data,
|
|
279
|
-
* and re-renders every node bound to `$name.*`.
|
|
280
|
-
*
|
|
281
|
-
* @param name - Provider name (e.g., "spacetime", "firebase")
|
|
282
|
-
* @param data - Full state object for this provider
|
|
283
|
-
*/
|
|
284
|
-
setContext(name: string, data: Record<string, unknown>): void {
|
|
285
|
-
const engine = this.ensureInitialized();
|
|
286
|
-
const plainData = JSON.parse(JSON.stringify(data));
|
|
287
|
-
try {
|
|
288
|
-
engine.setContext(name, plainData);
|
|
289
|
-
} catch (err) {
|
|
290
|
-
throw classifyEngineError(err);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Remove a data source context entirely.
|
|
296
|
-
* Drops the provider's state and re-renders bound nodes (they resolve to null).
|
|
297
|
-
*/
|
|
298
|
-
removeContext(name: string): void {
|
|
299
|
-
const engine = this.ensureInitialized();
|
|
300
|
-
engine.removeContext(name);
|
|
301
|
-
}
|
|
302
|
-
}
|
package/src/engine.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Low-level wrapper around the WASM engine
|
|
3
|
-
* Node.js / Bundler target
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// WASM module types
|
|
7
|
-
import { WasmEngine } from "../wasm-node/hypen_engine.js";
|
|
8
|
-
import { frameworkLoggers } from "./logger.js";
|
|
9
|
-
import { classifyEngineError } from "./result.js";
|
|
10
|
-
import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
|
|
11
|
-
|
|
12
|
-
// Re-export types so existing consumers of "./engine.js" still work
|
|
13
|
-
export type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";
|
|
14
|
-
|
|
15
|
-
const log = frameworkLoggers.engine;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Unwrap proxy objects to plain values for WASM serialization.
|
|
19
|
-
* Uses structuredClone when available (Node 17+, Bun, modern browsers),
|
|
20
|
-
* falls back to JSON round-trip for proxy objects or older environments.
|
|
21
|
-
*/
|
|
22
|
-
function unwrapForWasm<T>(value: T): T {
|
|
23
|
-
// Fast path: primitives don't need cloning
|
|
24
|
-
if (value === null || typeof value !== 'object') {
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Check if the object has a snapshot method (our proxy convention)
|
|
29
|
-
if (typeof (value as any).__getSnapshot === 'function') {
|
|
30
|
-
return (value as any).__getSnapshot() as T;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Try structuredClone first (fastest for plain objects)
|
|
34
|
-
try {
|
|
35
|
-
return structuredClone(value);
|
|
36
|
-
} catch {
|
|
37
|
-
// Fallback for proxy objects or unsupported types
|
|
38
|
-
return JSON.parse(JSON.stringify(value));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Engine wraps the WASM engine and provides a TypeScript-friendly API
|
|
44
|
-
*/
|
|
45
|
-
export class Engine {
|
|
46
|
-
private wasmEngine: WasmEngine | null = null;
|
|
47
|
-
private initialized = false;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Initialize the WASM module
|
|
51
|
-
*/
|
|
52
|
-
async init(): Promise<void> {
|
|
53
|
-
if (this.initialized) return;
|
|
54
|
-
|
|
55
|
-
// For bundler target, WASM is auto-initialized
|
|
56
|
-
this.wasmEngine = new WasmEngine();
|
|
57
|
-
this.initialized = true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Ensure the engine is initialized before operations
|
|
62
|
-
*/
|
|
63
|
-
private ensureInitialized(): WasmEngine {
|
|
64
|
-
if (!this.wasmEngine) {
|
|
65
|
-
throw new Error("Engine not initialized. Call init() first.");
|
|
66
|
-
}
|
|
67
|
-
return this.wasmEngine;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Set the render callback that receives patches
|
|
72
|
-
*/
|
|
73
|
-
setRenderCallback(callback: RenderCallback): void {
|
|
74
|
-
const engine = this.ensureInitialized();
|
|
75
|
-
engine.setRenderCallback((patches: Patch[]) => {
|
|
76
|
-
callback(patches);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Set the component resolver for dynamic component composition
|
|
82
|
-
*/
|
|
83
|
-
setComponentResolver(resolver: ComponentResolver): void {
|
|
84
|
-
const engine = this.ensureInitialized();
|
|
85
|
-
engine.setComponentResolver((componentName: string, contextPath: string | null) => {
|
|
86
|
-
const result = resolver(componentName, contextPath);
|
|
87
|
-
return result;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Parse and render Hypen DSL source code
|
|
93
|
-
* @throws {ParseError} if the source fails to parse
|
|
94
|
-
* @throws {RenderError} if rendering fails
|
|
95
|
-
*/
|
|
96
|
-
renderSource(source: string): void {
|
|
97
|
-
const engine = this.ensureInitialized();
|
|
98
|
-
try {
|
|
99
|
-
engine.renderSource(source);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
throw classifyEngineError(err);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Render a lazy component (for lazy route loading)
|
|
107
|
-
*/
|
|
108
|
-
renderLazyComponent(source: string): void {
|
|
109
|
-
const engine = this.ensureInitialized();
|
|
110
|
-
engine.renderLazyComponent(source);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Render a component into a specific parent node (subtree rendering)
|
|
115
|
-
* @throws {ParseError} if the source fails to parse
|
|
116
|
-
* @throws {RenderError} if the parent node is not found or rendering fails
|
|
117
|
-
*/
|
|
118
|
-
renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
|
|
119
|
-
const engine = this.ensureInitialized();
|
|
120
|
-
try {
|
|
121
|
-
engine.renderInto(source, parentNodeId, unwrapForWasm(state));
|
|
122
|
-
} catch (err) {
|
|
123
|
-
throw classifyEngineError(err);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Notify the engine of state changes using sparse updates
|
|
129
|
-
* @throws {StateError} if the state patch is invalid
|
|
130
|
-
*/
|
|
131
|
-
notifyStateChange(paths: string[], values: Record<string, any>): void {
|
|
132
|
-
const engine = this.ensureInitialized();
|
|
133
|
-
|
|
134
|
-
if (paths.length === 0) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
engine.updateStateSparse(paths, unwrapForWasm(values));
|
|
140
|
-
} catch (err) {
|
|
141
|
-
throw classifyEngineError(err);
|
|
142
|
-
}
|
|
143
|
-
log.debug("State changed (sparse):", paths);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Notify the engine of state changes using full state replacement
|
|
148
|
-
* @deprecated Use notifyStateChange with sparse values instead
|
|
149
|
-
*/
|
|
150
|
-
notifyStateChangeFull(paths: string[], currentState: Record<string, any>): void {
|
|
151
|
-
const engine = this.ensureInitialized();
|
|
152
|
-
engine.updateState(unwrapForWasm(currentState));
|
|
153
|
-
|
|
154
|
-
if (paths.length > 0) {
|
|
155
|
-
log.debug("State changed (full):", paths);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Update state (triggers re-render of affected nodes)
|
|
161
|
-
* @deprecated Use notifyStateChange instead
|
|
162
|
-
*/
|
|
163
|
-
updateState(statePatch: Record<string, any>): void {
|
|
164
|
-
const engine = this.ensureInitialized();
|
|
165
|
-
engine.updateState(unwrapForWasm(statePatch));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Dispatch an action
|
|
170
|
-
* @throws {HypenError} if the action dispatch fails
|
|
171
|
-
*/
|
|
172
|
-
dispatchAction(name: string, payload?: any): void {
|
|
173
|
-
const engine = this.ensureInitialized();
|
|
174
|
-
try {
|
|
175
|
-
engine.dispatchAction(name, payload ?? null);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
throw classifyEngineError(err);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Register an action handler
|
|
183
|
-
*/
|
|
184
|
-
private _onErrorHandler?: (error: Error) => void;
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Set error handler for action errors
|
|
188
|
-
*/
|
|
189
|
-
setOnError(handler: (error: Error) => void): void {
|
|
190
|
-
this._onErrorHandler = handler;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
onAction(actionName: string, handler: ActionHandler): void {
|
|
194
|
-
const engine = this.ensureInitialized();
|
|
195
|
-
const handleError = (err: unknown) => {
|
|
196
|
-
log.error("Action handler error:", err);
|
|
197
|
-
if (this._onErrorHandler) {
|
|
198
|
-
this._onErrorHandler(err instanceof Error ? err : new Error(String(err)));
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
engine.onAction(actionName, (action: Action) => {
|
|
202
|
-
try {
|
|
203
|
-
const result = handler(action);
|
|
204
|
-
// If handler returns a promise, catch async errors too
|
|
205
|
-
if (result && typeof (result as any).catch === "function") {
|
|
206
|
-
(result as any).catch(handleError);
|
|
207
|
-
}
|
|
208
|
-
} catch (err) {
|
|
209
|
-
handleError(err);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Initialize a module
|
|
216
|
-
*/
|
|
217
|
-
setModule(
|
|
218
|
-
name: string,
|
|
219
|
-
actions: string[],
|
|
220
|
-
stateKeys: string[],
|
|
221
|
-
initialState: Record<string, any>
|
|
222
|
-
): void {
|
|
223
|
-
const engine = this.ensureInitialized();
|
|
224
|
-
engine.setModule(name, actions, stateKeys, initialState);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Get the current revision number
|
|
229
|
-
*/
|
|
230
|
-
getRevision(): number {
|
|
231
|
-
const engine = this.ensureInitialized();
|
|
232
|
-
return Number(engine.getRevision());
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Clear the engine tree
|
|
237
|
-
*/
|
|
238
|
-
clearTree(): void {
|
|
239
|
-
const engine = this.ensureInitialized();
|
|
240
|
-
engine.clearTree();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Debug method to inspect parsed components
|
|
245
|
-
*/
|
|
246
|
-
debugParseComponent(source: string): string {
|
|
247
|
-
const engine = this.ensureInitialized();
|
|
248
|
-
return engine.debugParseComponent(source);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ── Data Source Context ────────────────────────────────────────
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Set (or replace) a named data source context.
|
|
255
|
-
*
|
|
256
|
-
* Registers the provider in the dependency graph, stores the data,
|
|
257
|
-
* and re-renders every node bound to `$name.*`.
|
|
258
|
-
* Sparse merging (if needed) should happen at the SDK layer before
|
|
259
|
-
* calling this method with the merged object.
|
|
260
|
-
*
|
|
261
|
-
* @param name - Provider name (e.g., "spacetime", "firebase")
|
|
262
|
-
* @param data - Full state object for this provider
|
|
263
|
-
*/
|
|
264
|
-
setContext(name: string, data: Record<string, unknown>): void {
|
|
265
|
-
const engine = this.ensureInitialized();
|
|
266
|
-
try {
|
|
267
|
-
engine.setContext(name, unwrapForWasm(data));
|
|
268
|
-
} catch (err) {
|
|
269
|
-
throw classifyEngineError(err);
|
|
270
|
-
}
|
|
271
|
-
log.debug(`Data source "${name}" context set`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Remove a data source context entirely.
|
|
276
|
-
* Drops the provider's state and re-renders bound nodes (they resolve to null).
|
|
277
|
-
*/
|
|
278
|
-
removeContext(name: string): void {
|
|
279
|
-
const engine = this.ensureInitialized();
|
|
280
|
-
engine.removeContext(name);
|
|
281
|
-
}
|
|
282
|
-
}
|
package/src/loader.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component Loader
|
|
3
|
-
*
|
|
4
|
-
* Loads and registers Hypen components from file system.
|
|
5
|
-
* Works in both Node.js and Bun environments.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import type { HypenModuleDefinition } from "./app.js";
|
|
11
|
-
import { frameworkLoggers } from "./logger.js";
|
|
12
|
-
|
|
13
|
-
const log = frameworkLoggers.loader;
|
|
14
|
-
|
|
15
|
-
export interface ComponentDefinition {
|
|
16
|
-
name: string;
|
|
17
|
-
module: HypenModuleDefinition<any>;
|
|
18
|
-
template: string; // The .hypen file content
|
|
19
|
-
path: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class ComponentLoader {
|
|
23
|
-
private components = new Map<string, ComponentDefinition>();
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Register a component with its module and template
|
|
27
|
-
*/
|
|
28
|
-
register(
|
|
29
|
-
name: string,
|
|
30
|
-
module: HypenModuleDefinition<any>,
|
|
31
|
-
template: string,
|
|
32
|
-
path?: string
|
|
33
|
-
): void {
|
|
34
|
-
this.components.set(name, {
|
|
35
|
-
name,
|
|
36
|
-
module,
|
|
37
|
-
template,
|
|
38
|
-
path: path || name,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get a registered component by name
|
|
44
|
-
*/
|
|
45
|
-
get(name: string): ComponentDefinition | undefined {
|
|
46
|
-
return this.components.get(name);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Check if a component is registered
|
|
51
|
-
*/
|
|
52
|
-
has(name: string): boolean {
|
|
53
|
-
return this.components.has(name);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get all registered component names
|
|
58
|
-
*/
|
|
59
|
-
getNames(): string[] {
|
|
60
|
-
return Array.from(this.components.keys());
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Get all registered components
|
|
65
|
-
*/
|
|
66
|
-
getAll(): ComponentDefinition[] {
|
|
67
|
-
return Array.from(this.components.values());
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Clear all registered components
|
|
72
|
-
*/
|
|
73
|
-
clear(): void {
|
|
74
|
-
this.components.clear();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Load a component from a directory
|
|
79
|
-
* Expects: component.ts and component.hypen in the same directory
|
|
80
|
-
*/
|
|
81
|
-
async loadFromDirectory(name: string, dirPath: string): Promise<void> {
|
|
82
|
-
try {
|
|
83
|
-
// Dynamic import of the module
|
|
84
|
-
const modulePath = join(dirPath, "component.ts");
|
|
85
|
-
const moduleExport = await import(modulePath);
|
|
86
|
-
const module = moduleExport.default as HypenModuleDefinition<any>;
|
|
87
|
-
|
|
88
|
-
// Read the .hypen template file
|
|
89
|
-
const templatePath = join(dirPath, "component.hypen");
|
|
90
|
-
const template = readFileSync(templatePath, "utf-8");
|
|
91
|
-
|
|
92
|
-
// Register the component
|
|
93
|
-
this.register(name, module, template, dirPath);
|
|
94
|
-
|
|
95
|
-
log.debug(`Loaded component: ${name} from ${dirPath}`);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
log.error(`Failed to load component ${name} from ${dirPath}:`, error);
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Auto-load all components from a directory
|
|
104
|
-
* Scans for subdirectories containing component.ts and component.hypen
|
|
105
|
-
*/
|
|
106
|
-
async loadFromComponentsDir(baseDir: string): Promise<void> {
|
|
107
|
-
try {
|
|
108
|
-
if (!existsSync(baseDir)) {
|
|
109
|
-
log.warn(`Components directory not found: ${baseDir}`);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const entries = readdirSync(baseDir, { withFileTypes: true });
|
|
114
|
-
|
|
115
|
-
for (const entry of entries) {
|
|
116
|
-
if (!entry.isDirectory()) continue;
|
|
117
|
-
|
|
118
|
-
const componentDir = join(baseDir, entry.name);
|
|
119
|
-
const hypenPath = join(componentDir, "component.hypen");
|
|
120
|
-
|
|
121
|
-
// Only load if component.hypen exists
|
|
122
|
-
if (existsSync(hypenPath)) {
|
|
123
|
-
await this.loadFromDirectory(entry.name, componentDir);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
log.debug(`Loaded ${this.components.size} components from ${baseDir}`);
|
|
128
|
-
} catch (error) {
|
|
129
|
-
log.error(`Failed to load components from ${baseDir}:`, error);
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Global component loader instance
|
|
136
|
-
export const componentLoader = new ComponentLoader();
|