@hypen-space/core 0.2.1 → 0.2.3

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.
Files changed (40) hide show
  1. package/README.md +199 -154
  2. package/dist/src/app.js +2 -1
  3. package/dist/src/app.js.map +3 -3
  4. package/dist/src/engine.browser.js +2 -2
  5. package/dist/src/engine.browser.js.map +2 -2
  6. package/dist/src/engine.js +4 -3
  7. package/dist/src/engine.js.map +3 -3
  8. package/dist/src/index.js +18 -6
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/plugin.js +126 -0
  11. package/dist/src/plugin.js.map +10 -0
  12. package/dist/src/resolver.js +102 -0
  13. package/dist/src/resolver.js.map +10 -0
  14. package/dist/src/router.js +37 -18
  15. package/dist/src/router.js.map +3 -3
  16. package/dist/src/state.js +9 -1
  17. package/dist/src/state.js.map +3 -3
  18. package/package.json +15 -2
  19. package/src/app.ts +1 -0
  20. package/src/engine.browser.ts +1 -1
  21. package/src/engine.ts +4 -2
  22. package/src/index.ts +21 -1
  23. package/src/plugin.ts +219 -0
  24. package/src/resolver.ts +216 -0
  25. package/src/router.ts +43 -21
  26. package/src/state.ts +20 -0
  27. package/wasm-browser/README.md +425 -0
  28. package/wasm-browser/hypen_engine.d.ts +151 -0
  29. package/wasm-browser/hypen_engine.js +811 -0
  30. package/wasm-browser/hypen_engine_bg.js +736 -0
  31. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  32. package/wasm-browser/hypen_engine_bg.wasm.d.ts +30 -0
  33. package/wasm-browser/package.json +15 -0
  34. package/wasm-node/README.md +425 -0
  35. package/wasm-node/hypen_engine.d.ts +97 -0
  36. package/wasm-node/hypen_engine.js +751 -0
  37. package/wasm-node/hypen_engine_bg.js +736 -0
  38. package/wasm-node/hypen_engine_bg.wasm +0 -0
  39. package/wasm-node/hypen_engine_bg.wasm.d.ts +30 -0
  40. package/wasm-node/package.json +11 -0
package/src/router.ts CHANGED
@@ -27,6 +27,7 @@ export class HypenRouter {
27
27
  private state: RouteState;
28
28
  private subscribers = new Set<RouteChangeCallback>();
29
29
  private isInitialized = false;
30
+ private isUpdating = false;
30
31
 
31
32
  constructor() {
32
33
  // Create observable state for reactivity
@@ -68,6 +69,8 @@ export class HypenRouter {
68
69
 
69
70
  // Listen for hash changes
70
71
  window.addEventListener("hashchange", () => {
72
+ // Don't respond to hashchange events we triggered ourselves
73
+ if (this.isUpdating) return;
71
74
  const newPath = this.getPathFromBrowser();
72
75
  this.updatePath(newPath, false);
73
76
  });
@@ -150,26 +153,37 @@ export class HypenRouter {
150
153
  updateBrowser: boolean,
151
154
  replace: boolean = false
152
155
  ) {
153
- const oldPath = this.state.currentPath;
154
- this.state.previousPath = oldPath;
155
- this.state.currentPath = path;
156
- this.state.query = this.parseQuery();
157
-
158
- // Update browser URL if needed
159
- if (updateBrowser && typeof window !== "undefined") {
160
- const url = "#" + path;
161
- if (replace) {
162
- window.history.replaceState(null, "", url);
163
- } else {
164
- window.history.pushState(null, "", url);
156
+ // Prevent re-entrant updates
157
+ if (this.isUpdating) return;
158
+
159
+ this.isUpdating = true;
160
+ try {
161
+ const oldPath = this.state.currentPath;
162
+ this.state.previousPath = oldPath;
163
+ this.state.currentPath = path;
164
+ this.state.query = this.parseQuery();
165
+
166
+ // Notify subscribers synchronously before any browser events
167
+ this.notifySubscribers();
168
+
169
+ // Update browser URL if needed
170
+ if (updateBrowser && typeof window !== "undefined") {
171
+ const url = "#" + path;
172
+ if (replace) {
173
+ window.history.replaceState(null, "", url);
174
+ } else {
175
+ window.history.pushState(null, "", url);
176
+ }
177
+
178
+ // Manually trigger hashchange event
179
+ const hashChangeEvent = new HashChangeEvent("hashchange", {
180
+ oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
181
+ newURL: window.location.href,
182
+ });
183
+ window.dispatchEvent(hashChangeEvent);
165
184
  }
166
-
167
- // Manually trigger hashchange event
168
- const hashChangeEvent = new HashChangeEvent("hashchange", {
169
- oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
170
- newURL: window.location.href,
171
- });
172
- window.dispatchEvent(hashChangeEvent);
185
+ } finally {
186
+ this.isUpdating = false;
173
187
  }
174
188
  }
175
189
 
@@ -272,7 +286,11 @@ export class HypenRouter {
272
286
  this.subscribers.add(callback);
273
287
 
274
288
  // Call immediately with current state
275
- callback(this.getState());
289
+ try {
290
+ callback(this.getState());
291
+ } catch (error) {
292
+ console.error("[HypenRouter] Error in route subscriber:", error);
293
+ }
276
294
 
277
295
  // Return unsubscribe function
278
296
  return () => {
@@ -286,7 +304,11 @@ export class HypenRouter {
286
304
  private notifySubscribers() {
287
305
  const routeState = this.getState();
288
306
  this.subscribers.forEach((callback) => {
289
- callback(routeState);
307
+ try {
308
+ callback(routeState);
309
+ } catch (error) {
310
+ console.error("[HypenRouter] Error in route subscriber:", error);
311
+ }
290
312
  });
291
313
  }
292
314
 
package/src/state.ts CHANGED
@@ -91,11 +91,20 @@ function deepClone<T>(obj: T): T {
91
91
  // Handle plain objects
92
92
  const objClone: any = {};
93
93
  visited.set(value, objClone);
94
+
95
+ // Clone string keys
94
96
  for (const key in value) {
95
97
  if (value.hasOwnProperty(key)) {
96
98
  objClone[key] = cloneInternal(value[key]);
97
99
  }
98
100
  }
101
+
102
+ // Clone Symbol keys (not enumerable by for...in)
103
+ const symbolKeys = Object.getOwnPropertySymbols(value);
104
+ for (const sym of symbolKeys) {
105
+ objClone[sym] = cloneInternal(value[sym]);
106
+ }
107
+
99
108
  return objClone;
100
109
  }
101
110
 
@@ -191,7 +200,14 @@ export function createObservableState<T extends object>(
191
200
  // Use default options if not provided
192
201
  const opts: StateObserverOptions = options || { onChange: () => {} };
193
202
 
203
+ // Handle null/undefined by using an empty object
204
+ // This allows modules to start with null state
205
+ if (initialState === null || initialState === undefined) {
206
+ initialState = {} as T;
207
+ }
208
+
194
209
  // Detect and reject primitive wrapper objects (Number, String, Boolean)
210
+ // These cannot be properly proxied due to internal slots
195
211
  if (
196
212
  initialState instanceof Number ||
197
213
  initialState instanceof String ||
@@ -203,6 +219,10 @@ export function createObservableState<T extends object>(
203
219
  );
204
220
  }
205
221
 
222
+ // Clone the initial state to ensure each observable has its own copy
223
+ // This prevents multiple modules from sharing the same underlying state object
224
+ initialState = deepClone(initialState);
225
+
206
226
  // Keep a snapshot of the last known state
207
227
  let lastSnapshot = deepClone(initialState);
208
228
  const pathPrefix = opts.pathPrefix || "";
@@ -0,0 +1,425 @@
1
+ # Hypen Engine
2
+
3
+ The core reactive rendering engine for Hypen, written in Rust. Compiles to WASM for web/desktop or native binaries with UniFFI for mobile platforms.
4
+
5
+ ## Overview
6
+
7
+ Hypen Engine is a platform-agnostic UI engine that:
8
+ - **Expands** Hypen DSL components into an intermediate representation (IR)
9
+ - **Tracks** reactive dependencies between state and UI nodes
10
+ - **Reconciles** UI trees efficiently using keyed diffing
11
+ - **Generates** minimal platform-agnostic patches for renderers
12
+ - **Routes** actions and events between UI and application logic
13
+ - **Serializes** for Remote UI scenarios (client-server streaming)
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ ┌─────────────────────────────────────────────────────────┐
19
+ │ Hypen Engine │
20
+ ├─────────────────────────────────────────────────────────┤
21
+ │ Parser → IR → Reactive Graph → Reconciler → Patches │
22
+ │ ↓ ↓ │
23
+ │ Component Registry Platform Renderer│
24
+ │ Dependency Tracking (Web/iOS/Android)│
25
+ │ State Management │
26
+ └─────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ ### Core Systems
30
+
31
+ 1. **IR & Component Expansion** (`src/ir/`)
32
+ - Canonical intermediate representation
33
+ - Component registry and resolution
34
+ - Props/slots expansion with defaults
35
+ - Stable NodeId generation
36
+
37
+ 2. **Reactive System** (`src/reactive/`)
38
+ - Dependency graph tracking `${state.*}` bindings
39
+ - Dirty marking on state changes
40
+ - Scheduling for efficient updates
41
+
42
+ 3. **Reconciliation** (`src/reconcile/`)
43
+ - Virtual instance tree (no platform objects)
44
+ - Keyed children diffing algorithm
45
+ - Minimal patch generation
46
+
47
+ 4. **Patch Types** (Platform-agnostic):
48
+ - `Create(id, type, props)` - Create new node
49
+ - `SetProp(id, name, value)` - Update property
50
+ - `SetText(id, text)` - Update text content
51
+ - `Insert(parent, id, before?)` - Insert into tree
52
+ - `Move(parent, id, before?)` - Reorder node
53
+ - `Remove(id)` - Remove from tree
54
+ - `AttachEvent(id, event)` / `DetachEvent(id, event)`
55
+
56
+ 5. **Action/Event Routing** (`src/dispatch/`)
57
+ - Map `@actions.*` to module handlers
58
+ - Forward UI events (click, input, etc.)
59
+ - Stable dispatch contract for SDKs
60
+
61
+ 6. **Lifecycle Management** (`src/lifecycle/`)
62
+ - Module lifecycle (created/destroyed)
63
+ - Component lifecycle (mount/unmount)
64
+ - Resource cache (images/fonts) with pluggable fetcher
65
+
66
+ 7. **Remote UI Serialization** (`src/serialize/`)
67
+ - Initial tree serialization
68
+ - Incremental patch streaming
69
+ - Revision tracking and optional integrity hashes
70
+ - JSON/CBOR format support
71
+
72
+ ## Usage
73
+
74
+ ### Basic Example
75
+
76
+ ```rust
77
+ use hypen_engine::{Engine, ir::{Element, Value, Component}};
78
+ use serde_json::json;
79
+
80
+ // Create engine
81
+ let mut engine = Engine::new();
82
+
83
+ // Register a custom component
84
+ engine.register_component(Component::new("Greeting", |props| {
85
+ Element::new("Text")
86
+ .with_prop("text", Value::Binding("${state.name}".to_string()))
87
+ }));
88
+
89
+ // Set render callback
90
+ engine.set_render_callback(|patches| {
91
+ for patch in patches {
92
+ println!("Patch: {:?}", patch);
93
+ }
94
+ });
95
+
96
+ // Register action handler
97
+ engine.on_action("greet", |action| {
98
+ println!("Hello from action: {:?}", action);
99
+ });
100
+
101
+ // Render UI
102
+ let ui = Element::new("Column")
103
+ .with_child(Element::new("Greeting"));
104
+
105
+ engine.render(&ui);
106
+
107
+ // Update state
108
+ engine.update_state(json!({
109
+ "name": "Alice"
110
+ }));
111
+ ```
112
+
113
+ ### With Module Host
114
+
115
+ ```rust
116
+ use hypen_engine::lifecycle::{Module, ModuleInstance};
117
+
118
+ // Create module definition
119
+ let module = Module::new("ProfilePage")
120
+ .with_actions(vec!["signIn".to_string(), "signOut".to_string()])
121
+ .with_state_keys(vec!["user".to_string()])
122
+ .with_persist(true);
123
+
124
+ // Create module instance
125
+ let instance = ModuleInstance::new(
126
+ module,
127
+ json!({ "user": null })
128
+ );
129
+
130
+ engine.set_module(instance);
131
+ ```
132
+
133
+ ## Compilation Targets
134
+
135
+ ### Native (Development)
136
+
137
+ ```bash
138
+ cargo build
139
+ cargo test
140
+ ```
141
+
142
+ ### WASM (Web/Desktop)
143
+
144
+ ```bash
145
+ # Install wasm-pack (one time)
146
+ cargo install wasm-pack
147
+
148
+ # Build for all WASM targets
149
+ ./build-wasm.sh
150
+
151
+ # Or build manually for specific targets:
152
+ wasm-pack build --target bundler # For webpack/vite
153
+ wasm-pack build --target nodejs # For Node.js
154
+ wasm-pack build --target web # For vanilla JS
155
+ ```
156
+
157
+ Output directories:
158
+ - `pkg/bundler/` - For use with bundlers (webpack, vite, rollup)
159
+ - `pkg/nodejs/` - For Node.js
160
+ - `pkg/web/` - For vanilla HTML/JS (see `example.html`)
161
+
162
+ ### JavaScript/TypeScript API
163
+
164
+ ```typescript
165
+ import init, { WasmEngine } from './pkg/web/hypen_engine.js';
166
+
167
+ // Initialize WASM
168
+ await init();
169
+
170
+ // Create engine
171
+ const engine = new WasmEngine();
172
+
173
+ // Set render callback
174
+ engine.setRenderCallback((patches) => {
175
+ console.log('Patches:', patches);
176
+ // Apply patches to your renderer
177
+ });
178
+
179
+ // Register action handlers
180
+ engine.onAction('increment', (action) => {
181
+ console.log('Increment action:', action);
182
+ });
183
+
184
+ // Initialize module
185
+ engine.setModule(
186
+ 'CounterModule',
187
+ ['increment', 'decrement'],
188
+ ['count'],
189
+ { count: 0 }
190
+ );
191
+
192
+ // Render Hypen DSL
193
+ const source = `
194
+ Column {
195
+ Text("Count: \${state.count}")
196
+ Button("@actions.increment") { Text("+1") }
197
+ }
198
+ `;
199
+ engine.renderSource(source);
200
+
201
+ // Update state
202
+ engine.updateState({ count: 42 });
203
+
204
+ // Dispatch action
205
+ engine.dispatchAction('increment', { amount: 1 });
206
+ ```
207
+
208
+ ### Testing WASM Build
209
+
210
+ Open `example.html` in a web server:
211
+
212
+ ```bash
213
+ # Using Python
214
+ python3 -m http.server 8000
215
+
216
+ # Using Node.js
217
+ npx serve .
218
+
219
+ # Then visit: http://localhost:8000/example.html
220
+ ```
221
+
222
+ ### Mobile (UniFFI)
223
+
224
+ ```bash
225
+ # Generate Swift/Kotlin bindings (coming soon)
226
+ cargo install uniffi_bindgen
227
+ uniffi-bindgen generate src/hypen_engine.udl --language swift
228
+ uniffi-bindgen generate src/hypen_engine.udl --language kotlin
229
+ ```
230
+
231
+ ## Project Structure
232
+
233
+ ```
234
+ hypen-engine-rs/
235
+ ├── src/
236
+ │ ├── lib.rs # Public API
237
+ │ ├── engine.rs # Main orchestrator
238
+ │ ├── wasi.rs # WASI interfaces
239
+ │ ├── ir/ # IR & component expansion
240
+ │ │ ├── node.rs # NodeId, Element, Props, Value
241
+ │ │ ├── component.rs # Component registry
242
+ │ │ └── expand.rs # AST → IR lowering
243
+ │ ├── reactive/ # Reactive system
244
+ │ │ ├── binding.rs # ${state.*} parsing
245
+ │ │ ├── graph.rs # Dependency tracking
246
+ │ │ └── scheduler.rs # Dirty marking
247
+ │ ├── reconcile/ # Reconciliation
248
+ │ │ ├── tree.rs # Instance tree
249
+ │ │ ├── diff.rs # Diffing algorithm
250
+ │ │ └── patch.rs # Patch types
251
+ │ ├── dispatch/ # Events & actions
252
+ │ │ ├── action.rs # Action dispatcher
253
+ │ │ └── event.rs # Event router
254
+ │ ├── lifecycle/ # Lifecycle
255
+ │ │ ├── module.rs # Module lifecycle
256
+ │ │ ├── component.rs # Component lifecycle
257
+ │ │ └── resource.rs # Resource cache
258
+ │ └── serialize/ # Serialization
259
+ │ └── remote.rs # Remote UI protocol
260
+ ├── Cargo.toml
261
+ └── README.md
262
+ ```
263
+
264
+ ## Key Data Structures
265
+
266
+ ### Element (IR Node)
267
+ ```rust
268
+ pub struct Element {
269
+ pub element_type: String, // "Column", "Text", etc.
270
+ pub props: IndexMap<String, Value>, // Properties
271
+ pub children: Vec<Element>, // Child elements
272
+ pub key: Option<String>, // For reconciliation
273
+ pub events: IndexMap<String, String>, // event → action
274
+ }
275
+ ```
276
+
277
+ ### Value (Props)
278
+ ```rust
279
+ pub enum Value {
280
+ Static(serde_json::Value), // Literal values
281
+ Binding(String), // ${state.user.name}
282
+ Action(String), // @actions.signIn
283
+ }
284
+ ```
285
+
286
+ ### Patch (Output)
287
+ ```rust
288
+ pub enum Patch {
289
+ Create { id, element_type, props },
290
+ SetProp { id, name, value },
291
+ SetText { id, text },
292
+ Insert { parent_id, id, before_id? },
293
+ Move { parent_id, id, before_id? },
294
+ Remove { id },
295
+ AttachEvent { id, event_name },
296
+ DetachEvent { id, event_name },
297
+ }
298
+ ```
299
+
300
+ ## Integration with Parser
301
+
302
+ The engine integrates with the Hypen parser from `../parser`:
303
+
304
+ ```rust
305
+ use hypen_parser::parse_component;
306
+ use hypen_engine::ast_to_ir;
307
+
308
+ let source = r#"
309
+ Column {
310
+ Text("Hello, ${state.name}")
311
+ Button("@actions.greet") { Text("Greet") }
312
+ }
313
+ "#;
314
+
315
+ let ast = parse_component(source)?;
316
+ let element = ast_to_ir(&ast); // Convert AST → IR
317
+ engine.render(&element);
318
+ ```
319
+
320
+ ### Full Example with Parser
321
+
322
+ ```rust
323
+ use hypen_engine::{Engine, ast_to_ir};
324
+ use hypen_parser::parse_component;
325
+ use serde_json::json;
326
+
327
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
328
+ let mut engine = Engine::new();
329
+
330
+ // Set render callback
331
+ engine.set_render_callback(|patches| {
332
+ println!("Patches: {:#?}", patches);
333
+ });
334
+
335
+ // Parse Hypen DSL
336
+ let source = r#"
337
+ Column {
338
+ Text("Count: ${state.count}")
339
+ Button("@actions.increment") { Text("+1") }
340
+ }
341
+ "#;
342
+
343
+ let ast = parse_component(source)?;
344
+ let element = ast_to_ir(&ast);
345
+
346
+ // Render
347
+ engine.render(&element);
348
+
349
+ // Update state
350
+ engine.update_state(json!({"count": 42}));
351
+
352
+ Ok(())
353
+ }
354
+ ```
355
+
356
+ ## Performance Considerations
357
+
358
+ - **Keyed reconciliation**: Use `key` props for list items to minimize DOM churn
359
+ - **Dependency tracking**: Only re-render nodes affected by state changes
360
+ - **Lazy evaluation**: Bindings are resolved on-demand during reconciliation
361
+ - **Resource caching**: Images/fonts are cached with configurable eviction
362
+
363
+ ## Remote UI Protocol
364
+
365
+ For client-server streaming:
366
+
367
+ ```json
368
+ // Initial tree (client connects)
369
+ {
370
+ "type": "initialTree",
371
+ "module": "ProfilePage",
372
+ "state": { "user": null },
373
+ "patches": [...],
374
+ "revision": 0
375
+ }
376
+
377
+ // State update (server → client)
378
+ {
379
+ "type": "stateUpdate",
380
+ "module": "ProfilePage",
381
+ "state": { "user": { "name": "Alice" } }
382
+ }
383
+
384
+ // Incremental patches (server → client)
385
+ {
386
+ "type": "patch",
387
+ "module": "ProfilePage",
388
+ "patches": [{ "type": "setProp", ... }],
389
+ "revision": 42
390
+ }
391
+
392
+ // Action dispatch (client → server)
393
+ {
394
+ "type": "dispatchAction",
395
+ "module": "ProfilePage",
396
+ "action": "signIn",
397
+ "payload": { "provider": "google" }
398
+ }
399
+ ```
400
+
401
+ ## Testing
402
+
403
+ ```bash
404
+ # Run all tests
405
+ cargo test
406
+
407
+ # Run with output
408
+ cargo test -- --nocapture
409
+
410
+ # Test specific module
411
+ cargo test reactive::
412
+ ```
413
+
414
+ ## Contributing
415
+
416
+ This is part of the Hypen project. See the main repository for contribution guidelines.
417
+
418
+ ## License
419
+
420
+ See main Hypen project for license information.
421
+
422
+ ---
423
+
424
+ **Status**: ✅ Core systems implemented, WASM integration in progress
425
+ **Next**: Full keyed reconciliation, UniFFI bindings, platform renderers