@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.
- package/README.md +182 -11
- package/dist/src/app.js +470 -44
- package/dist/src/app.js.map +7 -5
- package/dist/src/components/builtin.js +470 -44
- package/dist/src/components/builtin.js.map +7 -5
- package/dist/src/discovery.js +559 -65
- package/dist/src/discovery.js.map +8 -6
- package/dist/src/engine.browser.js +2 -2
- package/dist/src/engine.browser.js.map +2 -2
- package/dist/src/engine.js +18 -9
- package/dist/src/engine.js.map +3 -3
- package/dist/src/index.browser.js +863 -82
- package/dist/src/index.browser.js.map +11 -7
- package/dist/src/index.js +1591 -125
- package/dist/src/index.js.map +17 -10
- package/dist/src/remote/client.js +525 -35
- package/dist/src/remote/client.js.map +7 -4
- package/dist/src/remote/index.js +1796 -35
- package/dist/src/remote/index.js.map +13 -4
- package/dist/src/router.js +55 -29
- package/dist/src/router.js.map +3 -3
- package/dist/src/state.js +57 -29
- package/dist/src/state.js.map +3 -3
- package/package.json +8 -2
- package/src/app.ts +292 -13
- package/src/discovery.ts +123 -18
- package/src/disposable.ts +281 -0
- package/src/engine.browser.ts +1 -1
- package/src/engine.ts +29 -10
- package/src/hypen.ts +209 -0
- package/src/index.ts +147 -11
- package/src/logger.ts +338 -0
- package/src/remote/client.ts +263 -56
- package/src/remote/index.ts +25 -1
- package/src/remote/server.ts +652 -0
- package/src/remote/session.ts +256 -0
- package/src/remote/types.ts +68 -1
- package/src/result.ts +260 -0
- package/src/retry.ts +306 -0
- package/src/state.ts +103 -45
- package/wasm-browser/README.md +4 -0
- package/wasm-browser/hypen_engine_bg.wasm +0 -0
- package/wasm-browser/package.json +1 -1
- package/wasm-node/README.md +4 -0
- package/wasm-node/hypen_engine_bg.wasm +0 -0
- package/wasm-node/package.json +1 -1
- package/wasm-browser/hypen_engine_bg.js +0 -736
- 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,
|
|
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
|
-
//
|
|
42
|
-
if (
|
|
43
|
-
return
|
|
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
|
-
//
|
|
52
|
-
if (value
|
|
53
|
-
return
|
|
68
|
+
// Check for circular reference
|
|
69
|
+
if (visited.has(value)) {
|
|
70
|
+
return visited.get(value);
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
// Handle
|
|
57
|
-
if (value instanceof
|
|
58
|
-
|
|
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
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
275
|
-
|
|
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
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
320
|
-
|
|
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
|
+
}
|
package/wasm-browser/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Hypen Engine
|
|
2
2
|
|
|
3
|
+
[](https://www.rust-lang.org/)
|
|
4
|
+
[](https://webassembly.org/)
|
|
5
|
+
[](../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
|
package/wasm-node/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Hypen Engine
|
|
2
2
|
|
|
3
|
+
[](https://www.rust-lang.org/)
|
|
4
|
+
[](https://webassembly.org/)
|
|
5
|
+
[](../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
|