@hypen-space/core 0.2.11 → 0.3.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.
Files changed (48) hide show
  1. package/README.md +182 -11
  2. package/dist/src/app.js +470 -44
  3. package/dist/src/app.js.map +7 -5
  4. package/dist/src/components/builtin.js +470 -44
  5. package/dist/src/components/builtin.js.map +7 -5
  6. package/dist/src/discovery.js +559 -65
  7. package/dist/src/discovery.js.map +8 -6
  8. package/dist/src/engine.browser.js +2 -2
  9. package/dist/src/engine.browser.js.map +2 -2
  10. package/dist/src/engine.js +18 -9
  11. package/dist/src/engine.js.map +3 -3
  12. package/dist/src/index.browser.js +863 -82
  13. package/dist/src/index.browser.js.map +11 -7
  14. package/dist/src/index.js +1591 -125
  15. package/dist/src/index.js.map +17 -10
  16. package/dist/src/remote/client.js +525 -35
  17. package/dist/src/remote/client.js.map +7 -4
  18. package/dist/src/remote/index.js +1796 -35
  19. package/dist/src/remote/index.js.map +13 -4
  20. package/dist/src/router.js +55 -29
  21. package/dist/src/router.js.map +3 -3
  22. package/dist/src/state.js +57 -29
  23. package/dist/src/state.js.map +3 -3
  24. package/package.json +8 -2
  25. package/src/app.ts +292 -13
  26. package/src/discovery.ts +123 -18
  27. package/src/disposable.ts +281 -0
  28. package/src/engine.browser.ts +1 -1
  29. package/src/engine.ts +29 -10
  30. package/src/hypen.ts +209 -0
  31. package/src/index.ts +147 -11
  32. package/src/logger.ts +338 -0
  33. package/src/remote/client.ts +263 -56
  34. package/src/remote/index.ts +25 -1
  35. package/src/remote/server.ts +652 -0
  36. package/src/remote/session.ts +256 -0
  37. package/src/remote/types.ts +68 -1
  38. package/src/result.ts +260 -0
  39. package/src/retry.ts +306 -0
  40. package/src/state.ts +103 -45
  41. package/wasm-browser/README.md +4 -0
  42. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  43. package/wasm-browser/package.json +1 -1
  44. package/wasm-node/README.md +4 -0
  45. package/wasm-node/hypen_engine_bg.wasm +0 -0
  46. package/wasm-node/package.json +1 -1
  47. package/wasm-browser/hypen_engine_bg.js +0 -736
  48. package/wasm-node/hypen_engine_bg.js +0 -736
package/src/retry.ts ADDED
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Retry Utility for Network Operations
3
+ *
4
+ * Provides configurable retry logic with exponential/linear backoff
5
+ * for handling transient failures in network operations.
6
+ */
7
+
8
+ import { type Result, Ok, Err } from "./result.js";
9
+
10
+ /**
11
+ * Options for retry behavior
12
+ */
13
+ export interface RetryOptions {
14
+ /** Maximum number of attempts (default: 3) */
15
+ maxAttempts?: number;
16
+ /** Initial delay in milliseconds (default: 1000) */
17
+ delayMs?: number;
18
+ /** Backoff strategy (default: 'exponential') */
19
+ backoff?: "linear" | "exponential" | "none";
20
+ /** Maximum delay cap in milliseconds (default: 30000) */
21
+ maxDelayMs?: number;
22
+ /** Jitter factor 0-1 to randomize delays (default: 0.1) */
23
+ jitter?: number;
24
+ /** Callback on each retry attempt */
25
+ onRetry?: (attempt: number, error: Error, nextDelayMs: number) => void;
26
+ /** Optional predicate to determine if error is retryable */
27
+ shouldRetry?: (error: Error) => boolean;
28
+ /** AbortSignal for cancellation */
29
+ signal?: AbortSignal;
30
+ }
31
+
32
+ /**
33
+ * Default retry options
34
+ */
35
+ const DEFAULT_OPTIONS: Required<Omit<RetryOptions, "onRetry" | "shouldRetry" | "signal">> = {
36
+ maxAttempts: 3,
37
+ delayMs: 1000,
38
+ backoff: "exponential",
39
+ maxDelayMs: 30000,
40
+ jitter: 0.1,
41
+ };
42
+
43
+ /**
44
+ * Calculate delay for a given attempt
45
+ */
46
+ function calculateDelay(
47
+ attempt: number,
48
+ options: Required<Omit<RetryOptions, "onRetry" | "shouldRetry" | "signal">>
49
+ ): number {
50
+ let delay: number;
51
+
52
+ switch (options.backoff) {
53
+ case "exponential":
54
+ // 2^(attempt-1) * delayMs: 1x, 2x, 4x, 8x...
55
+ delay = options.delayMs * Math.pow(2, attempt - 1);
56
+ break;
57
+ case "linear":
58
+ // attempt * delayMs: 1x, 2x, 3x, 4x...
59
+ delay = options.delayMs * attempt;
60
+ break;
61
+ case "none":
62
+ delay = options.delayMs;
63
+ break;
64
+ }
65
+
66
+ // Apply jitter (randomize ±jitter%)
67
+ if (options.jitter > 0) {
68
+ const jitterRange = delay * options.jitter;
69
+ delay += (Math.random() * 2 - 1) * jitterRange;
70
+ }
71
+
72
+ // Cap at maxDelayMs
73
+ return Math.min(delay, options.maxDelayMs);
74
+ }
75
+
76
+ /**
77
+ * Sleep for a given duration, respecting abort signal
78
+ */
79
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
80
+ return new Promise((resolve, reject) => {
81
+ if (signal?.aborted) {
82
+ reject(new Error("Retry aborted"));
83
+ return;
84
+ }
85
+
86
+ const timeoutId = setTimeout(resolve, ms);
87
+
88
+ signal?.addEventListener("abort", () => {
89
+ clearTimeout(timeoutId);
90
+ reject(new Error("Retry aborted"));
91
+ });
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Retry a function with configurable backoff
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * // Basic usage
101
+ * const result = await retry(() => fetch('/api/data'));
102
+ *
103
+ * // With options
104
+ * const result = await retry(
105
+ * () => fetch('/api/data'),
106
+ * {
107
+ * maxAttempts: 5,
108
+ * delayMs: 2000,
109
+ * backoff: 'exponential',
110
+ * onRetry: (n, err) => console.log(`Attempt ${n} failed: ${err.message}`)
111
+ * }
112
+ * );
113
+ * ```
114
+ */
115
+ export async function retry<T>(
116
+ fn: () => T | Promise<T>,
117
+ options: RetryOptions = {}
118
+ ): Promise<T> {
119
+ const opts = { ...DEFAULT_OPTIONS, ...options };
120
+ let lastError: Error = new Error("No attempts made");
121
+
122
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
123
+ try {
124
+ // Check for abort before each attempt
125
+ if (opts.signal?.aborted) {
126
+ throw new Error("Retry aborted");
127
+ }
128
+
129
+ return await fn();
130
+ } catch (e) {
131
+ lastError = e instanceof Error ? e : new Error(String(e));
132
+
133
+ // Check if we should retry this error
134
+ if (opts.shouldRetry && !opts.shouldRetry(lastError)) {
135
+ throw lastError;
136
+ }
137
+
138
+ // If this was the last attempt, don't wait
139
+ if (attempt === opts.maxAttempts) {
140
+ break;
141
+ }
142
+
143
+ // Calculate delay and notify
144
+ const delayMs = calculateDelay(attempt, opts);
145
+ opts.onRetry?.(attempt, lastError, delayMs);
146
+
147
+ // Wait before next attempt
148
+ await sleep(delayMs, opts.signal);
149
+ }
150
+ }
151
+
152
+ throw lastError;
153
+ }
154
+
155
+ /**
156
+ * Retry a function, returning a Result instead of throwing
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * const result = await retryResult(() => fetch('/api/data'));
161
+ * if (result.ok) {
162
+ * console.log('Success:', result.value);
163
+ * } else {
164
+ * console.error('All retries failed:', result.error);
165
+ * }
166
+ * ```
167
+ */
168
+ export async function retryResult<T>(
169
+ fn: () => T | Promise<T>,
170
+ options: RetryOptions = {}
171
+ ): Promise<Result<T, Error>> {
172
+ try {
173
+ const value = await retry(fn, options);
174
+ return Ok(value);
175
+ } catch (e) {
176
+ return Err(e instanceof Error ? e : new Error(String(e)));
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Create a retryable version of a function
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const fetchWithRetry = withRetry(
186
+ * (url: string) => fetch(url),
187
+ * { maxAttempts: 3 }
188
+ * );
189
+ *
190
+ * const response = await fetchWithRetry('/api/data');
191
+ * ```
192
+ */
193
+ export function withRetry<TArgs extends unknown[], TReturn>(
194
+ fn: (...args: TArgs) => TReturn | Promise<TReturn>,
195
+ options: RetryOptions = {}
196
+ ): (...args: TArgs) => Promise<TReturn> {
197
+ return (...args: TArgs) => retry(() => fn(...args), options);
198
+ }
199
+
200
+ /**
201
+ * Predefined retry conditions
202
+ */
203
+ export const RetryConditions = {
204
+ /**
205
+ * Retry on network errors (fetch failures, timeouts)
206
+ */
207
+ networkErrors: (error: Error): boolean => {
208
+ const message = error.message.toLowerCase();
209
+ return (
210
+ message.includes("network") ||
211
+ message.includes("fetch") ||
212
+ message.includes("timeout") ||
213
+ message.includes("econnrefused") ||
214
+ message.includes("econnreset") ||
215
+ message.includes("socket")
216
+ );
217
+ },
218
+
219
+ /**
220
+ * Retry on specific HTTP status codes (from fetch Response)
221
+ */
222
+ httpRetryable: (error: Error & { status?: number }): boolean => {
223
+ const status = error.status;
224
+ if (!status) return false;
225
+ // Retry on 408, 429, 500, 502, 503, 504
226
+ return [408, 429, 500, 502, 503, 504].includes(status);
227
+ },
228
+
229
+ /**
230
+ * Retry on transient WebSocket errors
231
+ */
232
+ websocketErrors: (error: Error): boolean => {
233
+ const message = error.message.toLowerCase();
234
+ return (
235
+ message.includes("websocket") ||
236
+ message.includes("connection") ||
237
+ message.includes("close")
238
+ );
239
+ },
240
+
241
+ /**
242
+ * Combine multiple conditions (retry if any match)
243
+ */
244
+ any:
245
+ (...conditions: Array<(error: Error) => boolean>) =>
246
+ (error: Error): boolean =>
247
+ conditions.some((c) => c(error)),
248
+
249
+ /**
250
+ * Combine multiple conditions (retry if all match)
251
+ */
252
+ all:
253
+ (...conditions: Array<(error: Error) => boolean>) =>
254
+ (error: Error): boolean =>
255
+ conditions.every((c) => c(error)),
256
+ };
257
+
258
+ /**
259
+ * Preset configurations for common use cases
260
+ */
261
+ export const RetryPresets = {
262
+ /**
263
+ * Aggressive retry for critical operations
264
+ */
265
+ aggressive: {
266
+ maxAttempts: 10,
267
+ delayMs: 500,
268
+ backoff: "exponential" as const,
269
+ maxDelayMs: 60000,
270
+ jitter: 0.2,
271
+ },
272
+
273
+ /**
274
+ * Conservative retry for non-critical operations
275
+ */
276
+ conservative: {
277
+ maxAttempts: 3,
278
+ delayMs: 2000,
279
+ backoff: "linear" as const,
280
+ maxDelayMs: 10000,
281
+ jitter: 0.1,
282
+ },
283
+
284
+ /**
285
+ * Fast retry for local operations (short delays)
286
+ */
287
+ fast: {
288
+ maxAttempts: 5,
289
+ delayMs: 100,
290
+ backoff: "exponential" as const,
291
+ maxDelayMs: 2000,
292
+ jitter: 0,
293
+ },
294
+
295
+ /**
296
+ * WebSocket reconnection preset
297
+ */
298
+ websocket: {
299
+ maxAttempts: 10,
300
+ delayMs: 1000,
301
+ backoff: "exponential" as const,
302
+ maxDelayMs: 30000,
303
+ jitter: 0.1,
304
+ shouldRetry: RetryConditions.websocketErrors,
305
+ },
306
+ };
package/src/state.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  /**
2
2
  * State management with observer pattern and diffing
3
+ *
4
+ * Uses a proxy-based approach with self-detection to avoid
5
+ * creating nested proxies repeatedly.
3
6
  */
4
7
 
8
+ // Symbol for proxy detection (more robust than string property)
9
+ const IS_PROXY = Symbol.for('hypen.isProxy');
10
+ const RAW_TARGET = Symbol.for('hypen.rawTarget');
11
+
5
12
  export type StatePath = string; // e.g., "user.name", "items.0.title"
6
13
 
7
14
  /**
@@ -21,7 +28,8 @@ export interface StateObserverOptions {
21
28
  }
22
29
 
23
30
  /**
24
- * Deep clone an object, handling circular references
31
+ * Deep clone an object, using structuredClone when available.
32
+ * Handles proxy objects, circular references, and special types.
25
33
  */
26
34
  function deepClone<T>(obj: T): T {
27
35
  // Handle primitives and null
@@ -29,56 +37,62 @@ function deepClone<T>(obj: T): T {
29
37
  return obj;
30
38
  }
31
39
 
40
+ // Handle functions (pass through, can't be cloned)
41
+ if (typeof obj === 'function') {
42
+ return obj;
43
+ }
44
+
45
+ // Handle proxy objects with snapshot method
46
+ if (typeof (obj as any).__getSnapshot === 'function') {
47
+ return (obj as any).__getSnapshot() as T;
48
+ }
49
+
50
+ // Handle types that can't be cloned (return as-is)
51
+ if (obj instanceof WeakMap || obj instanceof WeakSet) {
52
+ return obj;
53
+ }
54
+
32
55
  // Use a WeakMap to track visited objects and handle circular references
33
56
  const visited = new WeakMap();
34
57
 
35
58
  function cloneInternal(value: any): any {
36
- // Handle primitives and null
37
59
  if (value === null || typeof value !== 'object') {
38
60
  return value;
39
61
  }
40
62
 
41
- // Check if we've already cloned this object (circular reference)
42
- if (visited.has(value)) {
43
- return visited.get(value);
44
- }
45
-
46
- // Handle Date
47
- if (value instanceof Date) {
48
- return new Date(value.getTime());
63
+ // Functions can't be cloned, pass through
64
+ if (typeof value === 'function') {
65
+ return value;
49
66
  }
50
67
 
51
- // Handle RegExp
52
- if (value instanceof RegExp) {
53
- return new RegExp(value.source, value.flags);
68
+ // Check for circular reference
69
+ if (visited.has(value)) {
70
+ return visited.get(value);
54
71
  }
55
72
 
56
- // Handle Map
57
- if (value instanceof Map) {
58
- const mapClone = new Map();
59
- visited.set(value, mapClone);
60
- for (const [k, v] of value.entries()) {
61
- mapClone.set(cloneInternal(k), cloneInternal(v));
62
- }
63
- return mapClone;
73
+ // Handle types that can't be cloned
74
+ if (value instanceof WeakMap || value instanceof WeakSet) {
75
+ return value;
64
76
  }
65
77
 
66
- // Handle Set
67
- if (value instanceof Set) {
68
- const setClone = new Set();
69
- visited.set(value, setClone);
70
- for (const item of value.values()) {
71
- setClone.add(cloneInternal(item));
78
+ // Try structuredClone for supported types (Date, Map, Set, etc.)
79
+ // This is faster than manual cloning for these types
80
+ if (
81
+ value instanceof Date ||
82
+ value instanceof RegExp ||
83
+ value instanceof Map ||
84
+ value instanceof Set ||
85
+ ArrayBuffer.isView(value) ||
86
+ value instanceof ArrayBuffer
87
+ ) {
88
+ try {
89
+ return structuredClone(value);
90
+ } catch {
91
+ // If structuredClone fails, fall through to manual handling
72
92
  }
73
- return setClone;
74
- }
75
-
76
- // Handle WeakMap/WeakSet - cannot be cloned, return as-is
77
- if (value instanceof WeakMap || value instanceof WeakSet) {
78
- return value;
79
93
  }
80
94
 
81
- // Handle Array
95
+ // Handle arrays
82
96
  if (Array.isArray(value)) {
83
97
  const arrClone: any[] = [];
84
98
  visited.set(value, arrClone);
@@ -94,12 +108,12 @@ function deepClone<T>(obj: T): T {
94
108
 
95
109
  // Clone string keys
96
110
  for (const key in value) {
97
- if (value.hasOwnProperty(key)) {
111
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
98
112
  objClone[key] = cloneInternal(value[key]);
99
113
  }
100
114
  }
101
115
 
102
- // Clone Symbol keys (not enumerable by for...in)
116
+ // Clone Symbol keys
103
117
  const symbolKeys = Object.getOwnPropertySymbols(value);
104
118
  for (const sym of symbolKeys) {
105
119
  objClone[sym] = cloneInternal(value[sym]);
@@ -271,18 +285,23 @@ export function createObservableState<T extends object>(
271
285
  }
272
286
  }
273
287
 
274
- // Cache proxies to handle circular references
275
- const proxyCache = new WeakMap<any, any>();
288
+ // WeakMap to cache proxies by their raw target
289
+ // This is essential for circular reference handling
290
+ const proxyCache = new WeakMap<object, any>();
276
291
 
277
292
  function createProxy(target: any, basePath: string): any {
278
- // Return cached proxy if it exists (handles circular references)
279
- if (proxyCache.has(target)) {
280
- return proxyCache.get(target);
281
- }
293
+ // Check cache first (handles circular references)
294
+ const cached = proxyCache.get(target);
295
+ if (cached) return cached;
282
296
 
283
297
  const proxy = new Proxy(target, {
284
298
  get(obj, prop) {
285
- const value = obj[prop];
299
+ // Self-detection: if checking IS_PROXY, return true
300
+ // This allows us to detect if a value is already proxied
301
+ if (prop === IS_PROXY) return true;
302
+
303
+ // Allow access to raw target (useful for debugging/serialization)
304
+ if (prop === RAW_TARGET) return obj;
286
305
 
287
306
  // Expose batch control methods
288
307
  if (prop === "__beginBatch") {
@@ -302,8 +321,16 @@ export function createObservableState<T extends object>(
302
321
  return () => deepClone(obj);
303
322
  }
304
323
 
324
+ const value = obj[prop];
325
+
305
326
  // Return proxied nested objects/arrays, but NOT special types
306
327
  if (value && typeof value === "object") {
328
+ // Fast path: if already a proxy, return as-is
329
+ // This check is O(1) and avoids WeakMap lookup
330
+ if ((value as any)[IS_PROXY]) {
331
+ return value;
332
+ }
333
+
307
334
  // Check for special object types that should not be proxied
308
335
  if (
309
336
  value instanceof Date ||
@@ -316,8 +343,15 @@ export function createObservableState<T extends object>(
316
343
  return value;
317
344
  }
318
345
 
319
- // Proxy regular objects and arrays
320
- return createProxy(value, basePath ? `${basePath}.${String(prop)}` : String(prop));
346
+ // Check cache for this value (handles circular refs and repeated access)
347
+ const cachedNested = proxyCache.get(value);
348
+ if (cachedNested) {
349
+ return cachedNested;
350
+ }
351
+
352
+ // Create proxy for nested object/array
353
+ const nestedProxy = createProxy(value, basePath ? `${basePath}.${String(prop)}` : String(prop));
354
+ return nestedProxy;
321
355
  }
322
356
 
323
357
  return value;
@@ -326,6 +360,12 @@ export function createObservableState<T extends object>(
326
360
  set(obj, prop, value) {
327
361
  const oldValue = obj[prop];
328
362
 
363
+ // If setting an object that's already a proxy, unwrap it first
364
+ // to store the raw value (prevents proxy-wrapping-proxy)
365
+ if (value && typeof value === "object" && (value as any)[IS_PROXY]) {
366
+ value = (value as any)[RAW_TARGET];
367
+ }
368
+
329
369
  // Set the new value
330
370
  obj[prop] = value;
331
371
 
@@ -381,3 +421,21 @@ export function getStateSnapshot<T>(state: T): T {
381
421
  }
382
422
  return deepClone(state);
383
423
  }
424
+
425
+ /**
426
+ * Check if a value is a Hypen state proxy
427
+ */
428
+ export function isStateProxy(value: unknown): boolean {
429
+ return value !== null && typeof value === 'object' && (value as any)[IS_PROXY] === true;
430
+ }
431
+
432
+ /**
433
+ * Get the raw (unwrapped) target from a proxy
434
+ * Returns the value as-is if not a proxy
435
+ */
436
+ export function unwrapProxy<T>(value: T): T {
437
+ if (value !== null && typeof value === 'object' && (value as any)[IS_PROXY]) {
438
+ return (value as any)[RAW_TARGET];
439
+ }
440
+ return value;
441
+ }
@@ -1,5 +1,9 @@
1
1
  # Hypen Engine
2
2
 
3
+ [![Rust](https://img.shields.io/badge/Rust-1.70+-orange?logo=rust)](https://www.rust-lang.org/)
4
+ [![WASM](https://img.shields.io/badge/WASM-Ready-654FF0?logo=webassembly)](https://webassembly.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../LICENSE)
6
+
3
7
  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
8
 
5
9
  ## Quick Start
Binary file
@@ -5,7 +5,7 @@
5
5
  "Hypen Contributors"
6
6
  ],
7
7
  "description": "A Rust implementation of the Hypen engine",
8
- "version": "0.1.2",
8
+ "version": "0.2.0",
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
@@ -1,5 +1,9 @@
1
1
  # Hypen Engine
2
2
 
3
+ [![Rust](https://img.shields.io/badge/Rust-1.70+-orange?logo=rust)](https://www.rust-lang.org/)
4
+ [![WASM](https://img.shields.io/badge/WASM-Ready-654FF0?logo=webassembly)](https://webassembly.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../LICENSE)
6
+
3
7
  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
8
 
5
9
  ## Quick Start
Binary file
@@ -4,7 +4,7 @@
4
4
  "Hypen Contributors"
5
5
  ],
6
6
  "description": "A Rust implementation of the Hypen engine",
7
- "version": "0.1.2",
7
+ "version": "0.2.0",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",