@hypen-space/core 0.2.12 → 0.3.1

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 (49) 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.js +18 -9
  9. package/dist/src/engine.js.map +3 -3
  10. package/dist/src/index.browser.js +870 -81
  11. package/dist/src/index.browser.js.map +11 -7
  12. package/dist/src/index.js +1591 -125
  13. package/dist/src/index.js.map +17 -10
  14. package/dist/src/plugin.js +2 -2
  15. package/dist/src/plugin.js.map +2 -2
  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.ts +29 -10
  29. package/src/hypen.ts +209 -0
  30. package/src/index.browser.ts +17 -1
  31. package/src/index.ts +148 -12
  32. package/src/logger.ts +338 -0
  33. package/src/plugin.ts +1 -1
  34. package/src/remote/client.ts +263 -56
  35. package/src/remote/index.ts +25 -1
  36. package/src/remote/server.ts +652 -0
  37. package/src/remote/session.ts +256 -0
  38. package/src/remote/types.ts +68 -1
  39. package/src/result.ts +260 -0
  40. package/src/retry.ts +306 -0
  41. package/src/state.ts +103 -45
  42. package/wasm-browser/README.md +4 -0
  43. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  44. package/wasm-browser/package.json +1 -1
  45. package/wasm-node/README.md +4 -0
  46. package/wasm-node/hypen_engine_bg.wasm +0 -0
  47. package/wasm-node/package.json +1 -1
  48. package/wasm-browser/hypen_engine_bg.js +0 -736
  49. package/wasm-node/hypen_engine_bg.js +0 -736
@@ -10,6 +10,1191 @@ var __export = (target, all) => {
10
10
  };
11
11
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
12
12
 
13
+ // src/state.ts
14
+ function deepClone(obj) {
15
+ if (obj === null || typeof obj !== "object") {
16
+ return obj;
17
+ }
18
+ if (typeof obj === "function") {
19
+ return obj;
20
+ }
21
+ if (typeof obj.__getSnapshot === "function") {
22
+ return obj.__getSnapshot();
23
+ }
24
+ if (obj instanceof WeakMap || obj instanceof WeakSet) {
25
+ return obj;
26
+ }
27
+ const visited = new WeakMap;
28
+ function cloneInternal(value) {
29
+ if (value === null || typeof value !== "object") {
30
+ return value;
31
+ }
32
+ if (typeof value === "function") {
33
+ return value;
34
+ }
35
+ if (visited.has(value)) {
36
+ return visited.get(value);
37
+ }
38
+ if (value instanceof WeakMap || value instanceof WeakSet) {
39
+ return value;
40
+ }
41
+ if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
42
+ try {
43
+ return structuredClone(value);
44
+ } catch {}
45
+ }
46
+ if (Array.isArray(value)) {
47
+ const arrClone = [];
48
+ visited.set(value, arrClone);
49
+ for (let i = 0;i < value.length; i++) {
50
+ arrClone[i] = cloneInternal(value[i]);
51
+ }
52
+ return arrClone;
53
+ }
54
+ const objClone = {};
55
+ visited.set(value, objClone);
56
+ for (const key in value) {
57
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
58
+ objClone[key] = cloneInternal(value[key]);
59
+ }
60
+ }
61
+ const symbolKeys = Object.getOwnPropertySymbols(value);
62
+ for (const sym of symbolKeys) {
63
+ objClone[sym] = cloneInternal(value[sym]);
64
+ }
65
+ return objClone;
66
+ }
67
+ return cloneInternal(obj);
68
+ }
69
+ function diffState(oldState, newState, basePath = "") {
70
+ const paths = [];
71
+ const newValues = {};
72
+ function diff(oldVal, newVal, path) {
73
+ if (oldVal === newVal)
74
+ return;
75
+ if (typeof oldVal !== "object" || typeof newVal !== "object" || oldVal === null || newVal === null) {
76
+ if (oldVal !== newVal) {
77
+ paths.push(path);
78
+ newValues[path] = newVal;
79
+ }
80
+ return;
81
+ }
82
+ if (Array.isArray(oldVal) || Array.isArray(newVal)) {
83
+ if (!Array.isArray(oldVal) || !Array.isArray(newVal) || oldVal.length !== newVal.length) {
84
+ paths.push(path);
85
+ newValues[path] = newVal;
86
+ return;
87
+ }
88
+ for (let i = 0;i < newVal.length; i++) {
89
+ const itemPath = path ? `${path}.${i}` : `${i}`;
90
+ diff(oldVal[i], newVal[i], itemPath);
91
+ }
92
+ return;
93
+ }
94
+ const oldKeys = new Set(Object.keys(oldVal));
95
+ const newKeys = new Set(Object.keys(newVal));
96
+ for (const key of newKeys) {
97
+ const propPath = path ? `${path}.${key}` : key;
98
+ if (!oldKeys.has(key)) {
99
+ paths.push(propPath);
100
+ newValues[propPath] = newVal[key];
101
+ } else {
102
+ diff(oldVal[key], newVal[key], propPath);
103
+ }
104
+ }
105
+ for (const key of oldKeys) {
106
+ if (!newKeys.has(key)) {
107
+ const propPath = path ? `${path}.${key}` : key;
108
+ paths.push(propPath);
109
+ newValues[propPath] = undefined;
110
+ }
111
+ }
112
+ }
113
+ diff(oldState, newState, basePath);
114
+ return { paths, newValues };
115
+ }
116
+ function createObservableState(initialState, options) {
117
+ const opts = options || { onChange: () => {} };
118
+ if (initialState === null || initialState === undefined) {
119
+ initialState = {};
120
+ }
121
+ if (initialState instanceof Number || initialState instanceof String || initialState instanceof Boolean) {
122
+ throw new TypeError("Cannot create observable state from primitive wrapper objects (Number, String, Boolean). " + "Use plain primitives or regular objects instead.");
123
+ }
124
+ initialState = deepClone(initialState);
125
+ let lastSnapshot = deepClone(initialState);
126
+ const pathPrefix = opts.pathPrefix || "";
127
+ let batchDepth = 0;
128
+ let pendingChange = null;
129
+ function notifyChange() {
130
+ if (batchDepth > 0)
131
+ return;
132
+ const change = diffState(lastSnapshot, state, pathPrefix);
133
+ if (change.paths.length > 0) {
134
+ lastSnapshot = deepClone(state);
135
+ if (pendingChange) {
136
+ change.paths.push(...pendingChange.paths);
137
+ Object.assign(change.newValues, pendingChange.newValues);
138
+ pendingChange = null;
139
+ }
140
+ opts.onChange(change);
141
+ }
142
+ }
143
+ let notificationPending = false;
144
+ function scheduleBatch() {
145
+ if (batchDepth === 0) {
146
+ if (!notificationPending) {
147
+ notificationPending = true;
148
+ queueMicrotask(() => {
149
+ notificationPending = false;
150
+ if (batchDepth === 0) {
151
+ notifyChange();
152
+ }
153
+ });
154
+ }
155
+ }
156
+ }
157
+ const proxyCache = new WeakMap;
158
+ function createProxy(target, basePath) {
159
+ const cached = proxyCache.get(target);
160
+ if (cached)
161
+ return cached;
162
+ const proxy = new Proxy(target, {
163
+ get(obj, prop) {
164
+ if (prop === IS_PROXY)
165
+ return true;
166
+ if (prop === RAW_TARGET)
167
+ return obj;
168
+ if (prop === "__beginBatch") {
169
+ return () => {
170
+ batchDepth++;
171
+ };
172
+ }
173
+ if (prop === "__endBatch") {
174
+ return () => {
175
+ batchDepth--;
176
+ if (batchDepth === 0) {
177
+ notifyChange();
178
+ }
179
+ };
180
+ }
181
+ if (prop === "__getSnapshot") {
182
+ return () => deepClone(obj);
183
+ }
184
+ const value = obj[prop];
185
+ if (value && typeof value === "object") {
186
+ if (value[IS_PROXY]) {
187
+ return value;
188
+ }
189
+ if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
190
+ return value;
191
+ }
192
+ const cachedNested = proxyCache.get(value);
193
+ if (cachedNested) {
194
+ return cachedNested;
195
+ }
196
+ const nestedProxy = createProxy(value, basePath ? `${basePath}.${String(prop)}` : String(prop));
197
+ return nestedProxy;
198
+ }
199
+ return value;
200
+ },
201
+ set(obj, prop, value) {
202
+ const oldValue = obj[prop];
203
+ if (value && typeof value === "object" && value[IS_PROXY]) {
204
+ value = value[RAW_TARGET];
205
+ }
206
+ obj[prop] = value;
207
+ if (oldValue !== value) {
208
+ scheduleBatch();
209
+ }
210
+ return true;
211
+ },
212
+ deleteProperty(obj, prop) {
213
+ if (prop in obj) {
214
+ delete obj[prop];
215
+ scheduleBatch();
216
+ }
217
+ return true;
218
+ }
219
+ });
220
+ proxyCache.set(target, proxy);
221
+ return proxy;
222
+ }
223
+ const state = createProxy(initialState, pathPrefix);
224
+ return state;
225
+ }
226
+ function batchStateUpdates(state, fn) {
227
+ const s = state;
228
+ if (s.__beginBatch && s.__endBatch) {
229
+ s.__beginBatch();
230
+ try {
231
+ fn();
232
+ } finally {
233
+ s.__endBatch();
234
+ }
235
+ } else {
236
+ fn();
237
+ }
238
+ }
239
+ function getStateSnapshot(state) {
240
+ const s = state;
241
+ if (s.__getSnapshot) {
242
+ return s.__getSnapshot();
243
+ }
244
+ return deepClone(state);
245
+ }
246
+ function isStateProxy(value) {
247
+ return value !== null && typeof value === "object" && value[IS_PROXY] === true;
248
+ }
249
+ function unwrapProxy(value) {
250
+ if (value !== null && typeof value === "object" && value[IS_PROXY]) {
251
+ return value[RAW_TARGET];
252
+ }
253
+ return value;
254
+ }
255
+ var IS_PROXY, RAW_TARGET;
256
+ var init_state = __esm(() => {
257
+ IS_PROXY = Symbol.for("hypen.isProxy");
258
+ RAW_TARGET = Symbol.for("hypen.rawTarget");
259
+ });
260
+
261
+ // src/result.ts
262
+ function Ok(value) {
263
+ return { ok: true, value };
264
+ }
265
+ function Err(error) {
266
+ return { ok: false, error };
267
+ }
268
+ function isOk(result) {
269
+ return result.ok;
270
+ }
271
+ function isErr(result) {
272
+ return !result.ok;
273
+ }
274
+ async function fromPromise(promise, mapError) {
275
+ try {
276
+ const value = await promise;
277
+ return Ok(value);
278
+ } catch (e) {
279
+ if (mapError) {
280
+ return Err(mapError(e));
281
+ }
282
+ return Err(e);
283
+ }
284
+ }
285
+ function fromTry(fn, mapError) {
286
+ try {
287
+ return Ok(fn());
288
+ } catch (e) {
289
+ if (mapError) {
290
+ return Err(mapError(e));
291
+ }
292
+ return Err(e);
293
+ }
294
+ }
295
+ function map(result, fn) {
296
+ if (result.ok) {
297
+ return Ok(fn(result.value));
298
+ }
299
+ return result;
300
+ }
301
+ function mapErr(result, fn) {
302
+ if (!result.ok) {
303
+ return Err(fn(result.error));
304
+ }
305
+ return result;
306
+ }
307
+ function flatMap(result, fn) {
308
+ if (result.ok) {
309
+ return fn(result.value);
310
+ }
311
+ return result;
312
+ }
313
+ function unwrap(result) {
314
+ if (result.ok) {
315
+ return result.value;
316
+ }
317
+ throw result.error;
318
+ }
319
+ function unwrapOr(result, defaultValue) {
320
+ if (result.ok) {
321
+ return result.value;
322
+ }
323
+ return defaultValue;
324
+ }
325
+ function unwrapOrElse(result, fn) {
326
+ if (result.ok) {
327
+ return result.value;
328
+ }
329
+ return fn(result.error);
330
+ }
331
+ function match(result, handlers) {
332
+ if (result.ok) {
333
+ return handlers.ok(result.value);
334
+ }
335
+ return handlers.err(result.error);
336
+ }
337
+ function all(results) {
338
+ const values = [];
339
+ for (const result of results) {
340
+ if (!result.ok) {
341
+ return result;
342
+ }
343
+ values.push(result.value);
344
+ }
345
+ return Ok(values);
346
+ }
347
+ var HypenError, ActionError, ConnectionError, StateError;
348
+ var init_result = __esm(() => {
349
+ HypenError = class HypenError extends Error {
350
+ code;
351
+ context;
352
+ cause;
353
+ constructor(code, message, options) {
354
+ super(message);
355
+ this.name = "HypenError";
356
+ this.code = code;
357
+ this.context = options?.context;
358
+ this.cause = options?.cause;
359
+ Object.setPrototypeOf(this, new.target.prototype);
360
+ }
361
+ };
362
+ ActionError = class ActionError extends HypenError {
363
+ actionName;
364
+ constructor(actionName, cause) {
365
+ super("ACTION_ERROR", `Action handler "${actionName}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
366
+ context: { actionName },
367
+ cause: cause instanceof Error ? cause : undefined
368
+ });
369
+ this.name = "ActionError";
370
+ this.actionName = actionName;
371
+ }
372
+ };
373
+ ConnectionError = class ConnectionError extends HypenError {
374
+ url;
375
+ attempt;
376
+ constructor(url, cause, attempt) {
377
+ super("CONNECTION_ERROR", `Connection to "${url}" failed${attempt ? ` (attempt ${attempt})` : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, {
378
+ context: { url, attempt },
379
+ cause: cause instanceof Error ? cause : undefined
380
+ });
381
+ this.name = "ConnectionError";
382
+ this.url = url;
383
+ this.attempt = attempt;
384
+ }
385
+ };
386
+ StateError = class StateError extends HypenError {
387
+ path;
388
+ constructor(message, path, cause) {
389
+ super("STATE_ERROR", message, {
390
+ context: { path },
391
+ cause: cause instanceof Error ? cause : undefined
392
+ });
393
+ this.name = "StateError";
394
+ this.path = path;
395
+ }
396
+ };
397
+ });
398
+
399
+ // src/logger.ts
400
+ function isProduction() {
401
+ if (typeof process !== "undefined" && process.env) {
402
+ return false;
403
+ }
404
+ return false;
405
+ }
406
+ function setLogLevel(level) {
407
+ config.level = level;
408
+ }
409
+ function getLogLevel() {
410
+ return config.level;
411
+ }
412
+ function configureLogger(options) {
413
+ config = { ...config, ...options };
414
+ }
415
+ function enableLogging() {
416
+ config.level = "debug";
417
+ }
418
+ function disableLogging() {
419
+ config.level = "none";
420
+ }
421
+ function shouldLog(level) {
422
+ return LOG_LEVEL_ORDER[level] >= LOG_LEVEL_ORDER[config.level];
423
+ }
424
+ function formatTag(tag, level) {
425
+ const timestamp = config.timestamps ? `${new Date().toISOString()} ` : "";
426
+ if (config.colors && level !== "none") {
427
+ const color = LOG_LEVEL_COLORS[level];
428
+ return `${timestamp}${color}[${tag}]${RESET_COLOR}`;
429
+ }
430
+ return `${timestamp}[${tag}]`;
431
+ }
432
+
433
+ class Logger {
434
+ tag;
435
+ constructor(tag) {
436
+ this.tag = tag;
437
+ }
438
+ debug(...args) {
439
+ if (!shouldLog("debug"))
440
+ return;
441
+ if (config.handler) {
442
+ config.handler.debug(this.tag, ...args);
443
+ } else {
444
+ console.log(formatTag(this.tag, "debug"), ...args);
445
+ }
446
+ }
447
+ info(...args) {
448
+ if (!shouldLog("info"))
449
+ return;
450
+ if (config.handler) {
451
+ config.handler.info(this.tag, ...args);
452
+ } else {
453
+ console.info(formatTag(this.tag, "info"), ...args);
454
+ }
455
+ }
456
+ warn(...args) {
457
+ if (!shouldLog("warn"))
458
+ return;
459
+ if (config.handler) {
460
+ config.handler.warn(this.tag, ...args);
461
+ } else {
462
+ console.warn(formatTag(this.tag, "warn"), ...args);
463
+ }
464
+ }
465
+ error(...args) {
466
+ if (!shouldLog("error"))
467
+ return;
468
+ if (config.handler) {
469
+ config.handler.error(this.tag, ...args);
470
+ } else {
471
+ console.error(formatTag(this.tag, "error"), ...args);
472
+ }
473
+ }
474
+ time(label, fn) {
475
+ if (!shouldLog("debug")) {
476
+ return fn();
477
+ }
478
+ const start = performance.now();
479
+ try {
480
+ return fn();
481
+ } finally {
482
+ const duration = performance.now() - start;
483
+ this.debug(`${label}: ${duration.toFixed(2)}ms`);
484
+ }
485
+ }
486
+ async timeAsync(label, fn) {
487
+ if (!shouldLog("debug")) {
488
+ return fn();
489
+ }
490
+ const start = performance.now();
491
+ try {
492
+ return await fn();
493
+ } finally {
494
+ const duration = performance.now() - start;
495
+ this.debug(`${label}: ${duration.toFixed(2)}ms`);
496
+ }
497
+ }
498
+ child(subTag) {
499
+ return new Logger(`${this.tag}:${subTag}`);
500
+ }
501
+ debugIf(condition, ...args) {
502
+ if (condition)
503
+ this.debug(...args);
504
+ }
505
+ warnIf(condition, ...args) {
506
+ if (condition)
507
+ this.warn(...args);
508
+ }
509
+ errorIf(condition, ...args) {
510
+ if (condition)
511
+ this.error(...args);
512
+ }
513
+ loggedOnce = new Set;
514
+ warnOnce(key, ...args) {
515
+ if (this.loggedOnce.has(key))
516
+ return;
517
+ this.loggedOnce.add(key);
518
+ this.warn(...args);
519
+ }
520
+ debugOnce(key, ...args) {
521
+ if (this.loggedOnce.has(key))
522
+ return;
523
+ this.loggedOnce.add(key);
524
+ this.debug(...args);
525
+ }
526
+ }
527
+ function createLogger(tag) {
528
+ return new Logger(tag);
529
+ }
530
+ var LOG_LEVEL_ORDER, LOG_LEVEL_COLORS, RESET_COLOR = "\x1B[0m", config, logger, log, frameworkLoggers;
531
+ var init_logger = __esm(() => {
532
+ LOG_LEVEL_ORDER = {
533
+ debug: 0,
534
+ info: 1,
535
+ warn: 2,
536
+ error: 3,
537
+ none: 4
538
+ };
539
+ LOG_LEVEL_COLORS = {
540
+ debug: "\x1B[36m",
541
+ info: "\x1B[32m",
542
+ warn: "\x1B[33m",
543
+ error: "\x1B[31m"
544
+ };
545
+ config = {
546
+ level: isProduction() ? "error" : "debug",
547
+ colors: true,
548
+ timestamps: false
549
+ };
550
+ logger = createLogger("Hypen");
551
+ log = {
552
+ debug: (tag, ...args) => {
553
+ if (!shouldLog("debug"))
554
+ return;
555
+ console.log(formatTag(tag, "debug"), ...args);
556
+ },
557
+ info: (tag, ...args) => {
558
+ if (!shouldLog("info"))
559
+ return;
560
+ console.info(formatTag(tag, "info"), ...args);
561
+ },
562
+ warn: (tag, ...args) => {
563
+ if (!shouldLog("warn"))
564
+ return;
565
+ console.warn(formatTag(tag, "warn"), ...args);
566
+ },
567
+ error: (tag, ...args) => {
568
+ if (!shouldLog("error"))
569
+ return;
570
+ console.error(formatTag(tag, "error"), ...args);
571
+ }
572
+ };
573
+ frameworkLoggers = {
574
+ engine: createLogger("Engine"),
575
+ router: createLogger("Router"),
576
+ state: createLogger("State"),
577
+ events: createLogger("Events"),
578
+ remote: createLogger("Remote"),
579
+ renderer: createLogger("Renderer"),
580
+ module: createLogger("Module"),
581
+ lifecycle: createLogger("Lifecycle")
582
+ };
583
+ });
584
+
585
+ // src/app.ts
586
+ var exports_app = {};
587
+ __export(exports_app, {
588
+ app: () => app,
589
+ HypenModuleInstance: () => HypenModuleInstance,
590
+ HypenAppBuilder: () => HypenAppBuilder,
591
+ HypenApp: () => HypenApp
592
+ });
593
+
594
+ class HypenAppBuilder {
595
+ initialState;
596
+ options;
597
+ createdHandler;
598
+ actionHandlers = new Map;
599
+ destroyedHandler;
600
+ disconnectHandler;
601
+ reconnectHandler;
602
+ expireHandler;
603
+ errorHandler;
604
+ template;
605
+ constructor(initialState, options) {
606
+ this.initialState = initialState;
607
+ this.options = options || {};
608
+ }
609
+ onCreated(fn) {
610
+ this.createdHandler = fn;
611
+ return this;
612
+ }
613
+ onAction(name, fn) {
614
+ this.actionHandlers.set(name, fn);
615
+ return this;
616
+ }
617
+ onDestroyed(fn) {
618
+ this.destroyedHandler = fn;
619
+ return this;
620
+ }
621
+ onDisconnect(fn) {
622
+ this.disconnectHandler = fn;
623
+ return this;
624
+ }
625
+ onReconnect(fn) {
626
+ this.reconnectHandler = fn;
627
+ return this;
628
+ }
629
+ onExpire(fn) {
630
+ this.expireHandler = fn;
631
+ return this;
632
+ }
633
+ onError(fn) {
634
+ this.errorHandler = fn;
635
+ return this;
636
+ }
637
+ ui(template) {
638
+ this.template = template;
639
+ return this.build();
640
+ }
641
+ build() {
642
+ const stateKeys = this.initialState !== null && typeof this.initialState === "object" ? Object.keys(this.initialState) : [];
643
+ return {
644
+ name: this.options.name,
645
+ actions: Array.from(this.actionHandlers.keys()),
646
+ stateKeys,
647
+ persist: this.options.persist,
648
+ version: this.options.version,
649
+ initialState: this.initialState,
650
+ template: this.template,
651
+ handlers: {
652
+ onCreated: this.createdHandler,
653
+ onAction: this.actionHandlers,
654
+ onDestroyed: this.destroyedHandler,
655
+ onDisconnect: this.disconnectHandler,
656
+ onReconnect: this.reconnectHandler,
657
+ onExpire: this.expireHandler,
658
+ onError: this.errorHandler
659
+ }
660
+ };
661
+ }
662
+ }
663
+
664
+ class HypenApp {
665
+ defineState(initial, options) {
666
+ return new HypenAppBuilder(initial, options);
667
+ }
668
+ }
669
+
670
+ class HypenModuleInstance {
671
+ engine;
672
+ definition;
673
+ state;
674
+ isDestroyed = false;
675
+ routerContext;
676
+ globalContext;
677
+ stateChangeCallbacks = [];
678
+ constructor(engine, definition, routerContext, globalContext) {
679
+ this.engine = engine;
680
+ this.definition = definition;
681
+ this.routerContext = routerContext;
682
+ this.globalContext = globalContext;
683
+ this.state = createObservableState(definition.initialState, {
684
+ onChange: (change) => {
685
+ this.engine.notifyStateChange(change.paths, change.newValues);
686
+ this.stateChangeCallbacks.forEach((cb) => cb());
687
+ }
688
+ });
689
+ this.engine.setModule(definition.name || "AnonymousModule", definition.actions, definition.stateKeys, getStateSnapshot(this.state));
690
+ for (const [actionName, handler] of definition.handlers.onAction) {
691
+ log2.debug(`Registering action handler: ${actionName} for module ${definition.name}`);
692
+ this.engine.onAction(actionName, async (action) => {
693
+ log2.debug(`Action handler fired: ${actionName}`, action);
694
+ const actionCtx = {
695
+ name: action.name,
696
+ payload: action.payload,
697
+ sender: action.sender
698
+ };
699
+ const next = {
700
+ router: this.routerContext?.root || null
701
+ };
702
+ const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
703
+ const result = await this.executeAction(actionName, handler, {
704
+ action: actionCtx,
705
+ state: this.state,
706
+ next,
707
+ context
708
+ });
709
+ if (!result.ok) {
710
+ const shouldRethrow = await this.handleError(result.error, { actionName });
711
+ if (shouldRethrow) {
712
+ throw result.error;
713
+ }
714
+ } else {
715
+ log2.debug(`Action handler completed: ${actionName}`);
716
+ }
717
+ });
718
+ }
719
+ this.callCreatedHandler();
720
+ }
721
+ createGlobalContextAPI() {
722
+ if (!this.globalContext) {
723
+ throw new Error("Global context not available");
724
+ }
725
+ const ctx = this.globalContext;
726
+ const api = {
727
+ getModule: (id) => ctx.getModule(id),
728
+ hasModule: (id) => ctx.hasModule(id),
729
+ getModuleIds: () => ctx.getModuleIds(),
730
+ getGlobalState: () => ctx.getGlobalState(),
731
+ emit: (event, payload) => ctx.emit(event, payload),
732
+ on: (event, handler) => ctx.on(event, handler)
733
+ };
734
+ if (ctx.__router) {
735
+ api.__router = ctx.__router;
736
+ }
737
+ if (ctx.__hypenEngine) {
738
+ api.__hypenEngine = ctx.__hypenEngine;
739
+ }
740
+ return api;
741
+ }
742
+ async executeAction(actionName, handler, ctx) {
743
+ try {
744
+ const result = handler(ctx);
745
+ await result;
746
+ return Ok(undefined);
747
+ } catch (e) {
748
+ return Err(new ActionError(actionName, e));
749
+ }
750
+ }
751
+ async handleError(error, context) {
752
+ const errorCtx = {
753
+ error,
754
+ state: this.state,
755
+ actionName: context.actionName,
756
+ lifecycle: context.lifecycle
757
+ };
758
+ if (this.definition.handlers.onError) {
759
+ try {
760
+ const result = await this.definition.handlers.onError(errorCtx);
761
+ if (result && typeof result === "object") {
762
+ if ("handled" in result && result.handled) {
763
+ return false;
764
+ }
765
+ if ("rethrow" in result && result.rethrow) {
766
+ return true;
767
+ }
768
+ }
769
+ } catch (handlerError) {
770
+ log2.error("Error in onError handler:", handlerError);
771
+ }
772
+ }
773
+ if (this.globalContext) {
774
+ const eventContext = context.actionName ? `action:${context.actionName}` : context.lifecycle ? `lifecycle:${context.lifecycle}` : "unknown";
775
+ this.globalContext.emit("error", {
776
+ message: error.message,
777
+ error,
778
+ context: eventContext
779
+ });
780
+ }
781
+ log2.error(`${context.actionName ? `Action "${context.actionName}"` : `Lifecycle "${context.lifecycle}"`} error:`, error);
782
+ return false;
783
+ }
784
+ async callCreatedHandler() {
785
+ if (this.definition.handlers.onCreated) {
786
+ const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
787
+ await this.definition.handlers.onCreated(this.state, context);
788
+ }
789
+ }
790
+ onStateChange(callback) {
791
+ this.stateChangeCallbacks.push(callback);
792
+ }
793
+ async destroy() {
794
+ if (this.isDestroyed)
795
+ return;
796
+ if (this.definition.handlers.onDestroyed) {
797
+ await this.definition.handlers.onDestroyed(this.state);
798
+ }
799
+ this.isDestroyed = true;
800
+ }
801
+ getState() {
802
+ return getStateSnapshot(this.state);
803
+ }
804
+ getLiveState() {
805
+ return this.state;
806
+ }
807
+ updateState(patch) {
808
+ Object.assign(this.state, patch);
809
+ }
810
+ }
811
+ var log2, app;
812
+ var init_app = __esm(() => {
813
+ init_result();
814
+ init_state();
815
+ init_logger();
816
+ log2 = createLogger("ModuleInstance");
817
+ app = new HypenApp;
818
+ });
819
+
820
+ // src/engine.ts
821
+ import { WasmEngine } from "../wasm-node/hypen_engine.js";
822
+ function unwrapForWasm(value) {
823
+ if (value === null || typeof value !== "object") {
824
+ return value;
825
+ }
826
+ if (typeof value.__getSnapshot === "function") {
827
+ return value.__getSnapshot();
828
+ }
829
+ try {
830
+ return structuredClone(value);
831
+ } catch {
832
+ return JSON.parse(JSON.stringify(value));
833
+ }
834
+ }
835
+
836
+ class Engine {
837
+ wasmEngine = null;
838
+ initialized = false;
839
+ async init() {
840
+ if (this.initialized)
841
+ return;
842
+ this.wasmEngine = new WasmEngine;
843
+ this.initialized = true;
844
+ }
845
+ ensureInitialized() {
846
+ if (!this.wasmEngine) {
847
+ throw new Error("Engine not initialized. Call init() first.");
848
+ }
849
+ return this.wasmEngine;
850
+ }
851
+ setRenderCallback(callback) {
852
+ const engine = this.ensureInitialized();
853
+ engine.setRenderCallback((patches) => {
854
+ callback(patches);
855
+ });
856
+ }
857
+ setComponentResolver(resolver) {
858
+ const engine = this.ensureInitialized();
859
+ engine.setComponentResolver((componentName, contextPath) => {
860
+ const result = resolver(componentName, contextPath);
861
+ return result;
862
+ });
863
+ }
864
+ renderSource(source) {
865
+ const engine = this.ensureInitialized();
866
+ engine.renderSource(source);
867
+ }
868
+ renderLazyComponent(source) {
869
+ const engine = this.ensureInitialized();
870
+ engine.renderLazyComponent(source);
871
+ }
872
+ renderInto(source, parentNodeId, state) {
873
+ const engine = this.ensureInitialized();
874
+ engine.renderInto(source, parentNodeId, unwrapForWasm(state));
875
+ }
876
+ notifyStateChange(paths, values) {
877
+ const engine = this.ensureInitialized();
878
+ if (paths.length === 0) {
879
+ return;
880
+ }
881
+ engine.updateStateSparse(paths, unwrapForWasm(values));
882
+ console.debug("[Hypen] State changed (sparse):", paths);
883
+ }
884
+ notifyStateChangeFull(paths, currentState) {
885
+ const engine = this.ensureInitialized();
886
+ engine.updateState(unwrapForWasm(currentState));
887
+ if (paths.length > 0) {
888
+ console.debug("[Hypen] State changed (full):", paths);
889
+ }
890
+ }
891
+ updateState(statePatch) {
892
+ const engine = this.ensureInitialized();
893
+ engine.updateState(unwrapForWasm(statePatch));
894
+ }
895
+ dispatchAction(name, payload) {
896
+ const engine = this.ensureInitialized();
897
+ engine.dispatchAction(name, payload ?? null);
898
+ }
899
+ onAction(actionName, handler) {
900
+ const engine = this.ensureInitialized();
901
+ engine.onAction(actionName, (action) => {
902
+ Promise.resolve(handler(action)).catch(console.error);
903
+ });
904
+ }
905
+ setModule(name, actions, stateKeys, initialState) {
906
+ const engine = this.ensureInitialized();
907
+ engine.setModule(name, actions, stateKeys, initialState);
908
+ }
909
+ getRevision() {
910
+ const engine = this.ensureInitialized();
911
+ return Number(engine.getRevision());
912
+ }
913
+ clearTree() {
914
+ const engine = this.ensureInitialized();
915
+ engine.clearTree();
916
+ }
917
+ debugParseComponent(source) {
918
+ const engine = this.ensureInitialized();
919
+ return engine.debugParseComponent(source);
920
+ }
921
+ }
922
+
923
+ // src/remote/client.ts
924
+ init_result();
925
+
926
+ // src/disposable.ts
927
+ function isDisposable(obj) {
928
+ return obj !== null && typeof obj === "object" && "dispose" in obj && typeof obj.dispose === "function";
929
+ }
930
+
931
+ class DisposableStack {
932
+ stack = [];
933
+ disposed = false;
934
+ add(disposable) {
935
+ if (this.disposed) {
936
+ disposable.dispose();
937
+ return disposable;
938
+ }
939
+ this.stack.push(disposable);
940
+ return disposable;
941
+ }
942
+ addCallback(callback) {
943
+ this.add({ dispose: callback });
944
+ }
945
+ addValue(value, dispose) {
946
+ this.add({ dispose: () => dispose(value) });
947
+ return value;
948
+ }
949
+ dispose() {
950
+ if (this.disposed)
951
+ return;
952
+ this.disposed = true;
953
+ while (this.stack.length > 0) {
954
+ const item = this.stack.pop();
955
+ try {
956
+ item.dispose();
957
+ } catch (error) {
958
+ console.error("[DisposableStack] Error during dispose:", error);
959
+ }
960
+ }
961
+ }
962
+ get isDisposed() {
963
+ return this.disposed;
964
+ }
965
+ get size() {
966
+ return this.stack.length;
967
+ }
968
+ }
969
+ function disposableListener(target, event, handler, options) {
970
+ target.addEventListener(event, handler, options);
971
+ return {
972
+ dispose: () => target.removeEventListener(event, handler, options)
973
+ };
974
+ }
975
+ function disposableTimeout(callback, ms) {
976
+ const id = setTimeout(callback, ms);
977
+ return {
978
+ id,
979
+ dispose: () => clearTimeout(id)
980
+ };
981
+ }
982
+ function disposableInterval(callback, ms) {
983
+ const id = setInterval(callback, ms);
984
+ return {
985
+ id,
986
+ dispose: () => clearInterval(id)
987
+ };
988
+ }
989
+ function disposableWebSocket(ws) {
990
+ return {
991
+ dispose: () => {
992
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
993
+ ws.close();
994
+ }
995
+ }
996
+ };
997
+ }
998
+ function disposableAbortController() {
999
+ const controller = new AbortController;
1000
+ return {
1001
+ controller,
1002
+ signal: controller.signal,
1003
+ dispose: () => controller.abort()
1004
+ };
1005
+ }
1006
+ function disposableSubscription(unsubscribe) {
1007
+ return { dispose: unsubscribe };
1008
+ }
1009
+ var ELEMENT_DISPOSABLES = Symbol("hypen.disposables");
1010
+ function getElementDisposables(element) {
1011
+ const existing = element[ELEMENT_DISPOSABLES];
1012
+ if (existing instanceof DisposableStack) {
1013
+ return existing;
1014
+ }
1015
+ const stack = new DisposableStack;
1016
+ element[ELEMENT_DISPOSABLES] = stack;
1017
+ return stack;
1018
+ }
1019
+ function disposeElement(element) {
1020
+ const stack = element[ELEMENT_DISPOSABLES];
1021
+ if (stack instanceof DisposableStack) {
1022
+ stack.dispose();
1023
+ delete element[ELEMENT_DISPOSABLES];
1024
+ }
1025
+ }
1026
+ function hasElementDisposables(element) {
1027
+ return element[ELEMENT_DISPOSABLES] instanceof DisposableStack;
1028
+ }
1029
+
1030
+ class DisposableMixin {
1031
+ disposables = new DisposableStack;
1032
+ track(disposable) {
1033
+ return this.disposables.add(disposable);
1034
+ }
1035
+ onDispose(callback) {
1036
+ this.disposables.addCallback(callback);
1037
+ }
1038
+ dispose() {
1039
+ this.disposables.dispose();
1040
+ }
1041
+ }
1042
+ function compositeDisposable(...disposables) {
1043
+ return {
1044
+ dispose: () => {
1045
+ for (const d of disposables) {
1046
+ try {
1047
+ d.dispose();
1048
+ } catch (error) {
1049
+ console.error("[compositeDisposable] Error during dispose:", error);
1050
+ }
1051
+ }
1052
+ }
1053
+ };
1054
+ }
1055
+ async function using(resource, fn) {
1056
+ const r = typeof resource === "function" ? resource() : resource;
1057
+ try {
1058
+ return await fn(r);
1059
+ } finally {
1060
+ r.dispose();
1061
+ }
1062
+ }
1063
+ function usingSync(resource, fn) {
1064
+ const r = typeof resource === "function" ? resource() : resource;
1065
+ try {
1066
+ return fn(r);
1067
+ } finally {
1068
+ r.dispose();
1069
+ }
1070
+ }
1071
+
1072
+ // src/retry.ts
1073
+ init_result();
1074
+ var DEFAULT_OPTIONS = {
1075
+ maxAttempts: 3,
1076
+ delayMs: 1000,
1077
+ backoff: "exponential",
1078
+ maxDelayMs: 30000,
1079
+ jitter: 0.1
1080
+ };
1081
+ function calculateDelay(attempt, options) {
1082
+ let delay;
1083
+ switch (options.backoff) {
1084
+ case "exponential":
1085
+ delay = options.delayMs * Math.pow(2, attempt - 1);
1086
+ break;
1087
+ case "linear":
1088
+ delay = options.delayMs * attempt;
1089
+ break;
1090
+ case "none":
1091
+ delay = options.delayMs;
1092
+ break;
1093
+ }
1094
+ if (options.jitter > 0) {
1095
+ const jitterRange = delay * options.jitter;
1096
+ delay += (Math.random() * 2 - 1) * jitterRange;
1097
+ }
1098
+ return Math.min(delay, options.maxDelayMs);
1099
+ }
1100
+ function sleep(ms, signal) {
1101
+ return new Promise((resolve, reject) => {
1102
+ if (signal?.aborted) {
1103
+ reject(new Error("Retry aborted"));
1104
+ return;
1105
+ }
1106
+ const timeoutId = setTimeout(resolve, ms);
1107
+ signal?.addEventListener("abort", () => {
1108
+ clearTimeout(timeoutId);
1109
+ reject(new Error("Retry aborted"));
1110
+ });
1111
+ });
1112
+ }
1113
+ async function retry(fn, options = {}) {
1114
+ const opts = { ...DEFAULT_OPTIONS, ...options };
1115
+ let lastError = new Error("No attempts made");
1116
+ for (let attempt = 1;attempt <= opts.maxAttempts; attempt++) {
1117
+ try {
1118
+ if (opts.signal?.aborted) {
1119
+ throw new Error("Retry aborted");
1120
+ }
1121
+ return await fn();
1122
+ } catch (e) {
1123
+ lastError = e instanceof Error ? e : new Error(String(e));
1124
+ if (opts.shouldRetry && !opts.shouldRetry(lastError)) {
1125
+ throw lastError;
1126
+ }
1127
+ if (attempt === opts.maxAttempts) {
1128
+ break;
1129
+ }
1130
+ const delayMs = calculateDelay(attempt, opts);
1131
+ opts.onRetry?.(attempt, lastError, delayMs);
1132
+ await sleep(delayMs, opts.signal);
1133
+ }
1134
+ }
1135
+ throw lastError;
1136
+ }
1137
+ async function retryResult(fn, options = {}) {
1138
+ try {
1139
+ const value = await retry(fn, options);
1140
+ return Ok(value);
1141
+ } catch (e) {
1142
+ return Err(e instanceof Error ? e : new Error(String(e)));
1143
+ }
1144
+ }
1145
+ function withRetry(fn, options = {}) {
1146
+ return (...args) => retry(() => fn(...args), options);
1147
+ }
1148
+ var RetryConditions = {
1149
+ networkErrors: (error) => {
1150
+ const message = error.message.toLowerCase();
1151
+ return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("socket");
1152
+ },
1153
+ httpRetryable: (error) => {
1154
+ const status = error.status;
1155
+ if (!status)
1156
+ return false;
1157
+ return [408, 429, 500, 502, 503, 504].includes(status);
1158
+ },
1159
+ websocketErrors: (error) => {
1160
+ const message = error.message.toLowerCase();
1161
+ return message.includes("websocket") || message.includes("connection") || message.includes("close");
1162
+ },
1163
+ any: (...conditions) => (error) => conditions.some((c) => c(error)),
1164
+ all: (...conditions) => (error) => conditions.every((c) => c(error))
1165
+ };
1166
+ var RetryPresets = {
1167
+ aggressive: {
1168
+ maxAttempts: 10,
1169
+ delayMs: 500,
1170
+ backoff: "exponential",
1171
+ maxDelayMs: 60000,
1172
+ jitter: 0.2
1173
+ },
1174
+ conservative: {
1175
+ maxAttempts: 3,
1176
+ delayMs: 2000,
1177
+ backoff: "linear",
1178
+ maxDelayMs: 1e4,
1179
+ jitter: 0.1
1180
+ },
1181
+ fast: {
1182
+ maxAttempts: 5,
1183
+ delayMs: 100,
1184
+ backoff: "exponential",
1185
+ maxDelayMs: 2000,
1186
+ jitter: 0
1187
+ },
1188
+ websocket: {
1189
+ maxAttempts: 10,
1190
+ delayMs: 1000,
1191
+ backoff: "exponential",
1192
+ maxDelayMs: 30000,
1193
+ jitter: 0.1,
1194
+ shouldRetry: RetryConditions.websocketErrors
1195
+ }
1196
+ };
1197
+
13
1198
  // src/remote/client.ts
14
1199
  class RemoteEngine {
15
1200
  ws = null;
@@ -17,12 +1202,17 @@ class RemoteEngine {
17
1202
  state = "disconnected";
18
1203
  options;
19
1204
  reconnectAttempts = 0;
20
- reconnectTimer = null;
1205
+ disposables = new DisposableStack;
1206
+ reconnectDisposable = null;
1207
+ currentSessionId = null;
1208
+ sessionOptions;
21
1209
  patchCallbacks = [];
22
1210
  stateCallbacks = [];
23
1211
  connectionCallbacks = [];
24
1212
  disconnectionCallbacks = [];
25
1213
  errorCallbacks = [];
1214
+ sessionEstablishedCallbacks = [];
1215
+ sessionExpiredCallbacks = [];
26
1216
  currentState = null;
27
1217
  currentRevision = 0;
28
1218
  moduleName = "";
@@ -31,54 +1221,85 @@ class RemoteEngine {
31
1221
  this.options = {
32
1222
  autoReconnect: options.autoReconnect ?? true,
33
1223
  reconnectInterval: options.reconnectInterval ?? 3000,
34
- maxReconnectAttempts: options.maxReconnectAttempts ?? 10
1224
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
1225
+ session: options.session
35
1226
  };
1227
+ this.sessionOptions = options.session;
1228
+ if (options.session?.id) {
1229
+ this.currentSessionId = options.session.id;
1230
+ }
36
1231
  }
37
1232
  async connect() {
38
1233
  if (this.state === "connected" || this.state === "connecting") {
39
- return;
1234
+ return Ok(undefined);
40
1235
  }
41
1236
  this.state = "connecting";
42
- return new Promise((resolve, reject) => {
1237
+ return new Promise((resolve) => {
43
1238
  try {
44
1239
  this.ws = new WebSocket(this.url);
45
- this.ws.onopen = () => {
46
- this.state = "connected";
47
- this.reconnectAttempts = 0;
48
- this.connectionCallbacks.forEach((cb) => cb());
49
- resolve();
50
- };
51
- this.ws.onmessage = (event) => {
1240
+ this.disposables.add(disposableWebSocket(this.ws));
1241
+ const messageHandler = (event) => {
52
1242
  this.handleMessage(event.data);
53
1243
  };
54
- this.ws.onerror = () => {
1244
+ this.disposables.add(disposableListener(this.ws, "message", messageHandler));
1245
+ const errorHandler = () => {
55
1246
  this.state = "error";
56
- const error = new Error("WebSocket error");
1247
+ const error = new ConnectionError(this.url, new Error("WebSocket error"));
57
1248
  this.errorCallbacks.forEach((cb) => cb(error));
58
- reject(error);
1249
+ resolve(Err(error));
59
1250
  };
60
- this.ws.onclose = () => {
1251
+ this.disposables.add(disposableListener(this.ws, "error", errorHandler));
1252
+ const closeHandler = () => {
61
1253
  this.state = "disconnected";
62
1254
  this.disconnectionCallbacks.forEach((cb) => cb());
63
1255
  this.attemptReconnect();
64
1256
  };
65
- } catch (error) {
1257
+ this.disposables.add(disposableListener(this.ws, "close", closeHandler));
1258
+ this.ws.onopen = () => {
1259
+ this.state = "connected";
1260
+ this.reconnectAttempts = 0;
1261
+ if (this.reconnectDisposable) {
1262
+ this.reconnectDisposable.dispose();
1263
+ this.reconnectDisposable = null;
1264
+ }
1265
+ this.sendHello();
1266
+ this.connectionCallbacks.forEach((cb) => cb());
1267
+ resolve(Ok(undefined));
1268
+ };
1269
+ } catch (e) {
66
1270
  this.state = "error";
67
- reject(error);
1271
+ const error = new ConnectionError(this.url, e);
1272
+ resolve(Err(error));
68
1273
  }
69
1274
  });
70
1275
  }
1276
+ sendHello() {
1277
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
1278
+ return;
1279
+ const hello = {
1280
+ type: "hello",
1281
+ sessionId: this.currentSessionId ?? this.sessionOptions?.id,
1282
+ props: this.sessionOptions?.props
1283
+ };
1284
+ this.ws.send(JSON.stringify(hello));
1285
+ }
71
1286
  disconnect() {
72
- if (this.reconnectTimer) {
73
- clearTimeout(this.reconnectTimer);
74
- this.reconnectTimer = null;
1287
+ if (this.reconnectDisposable) {
1288
+ this.reconnectDisposable.dispose();
1289
+ this.reconnectDisposable = null;
75
1290
  }
76
1291
  if (this.ws) {
77
- this.ws.close();
1292
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
1293
+ this.ws.close();
1294
+ }
78
1295
  this.ws = null;
79
1296
  }
80
1297
  this.state = "disconnected";
81
1298
  }
1299
+ dispose() {
1300
+ this.disconnect();
1301
+ this.disposables.dispose();
1302
+ }
82
1303
  dispatchAction(action, payload) {
83
1304
  if (this.state !== "connected" || !this.ws) {
84
1305
  console.warn("Cannot dispatch action: not connected");
@@ -112,6 +1333,14 @@ class RemoteEngine {
112
1333
  this.errorCallbacks.push(callback);
113
1334
  return this;
114
1335
  }
1336
+ onSessionEstablished(callback) {
1337
+ this.sessionEstablishedCallbacks.push(callback);
1338
+ return this;
1339
+ }
1340
+ onSessionExpired(callback) {
1341
+ this.sessionExpiredCallbacks.push(callback);
1342
+ return this;
1343
+ }
115
1344
  getConnectionState() {
116
1345
  return this.state;
117
1346
  }
@@ -121,10 +1350,19 @@ class RemoteEngine {
121
1350
  getRevision() {
122
1351
  return this.currentRevision;
123
1352
  }
1353
+ getSessionId() {
1354
+ return this.currentSessionId;
1355
+ }
124
1356
  handleMessage(data) {
125
1357
  try {
126
1358
  const message = JSON.parse(data);
127
1359
  switch (message.type) {
1360
+ case "sessionAck":
1361
+ this.handleSessionAck(message);
1362
+ break;
1363
+ case "sessionExpired":
1364
+ this.handleSessionExpired(message);
1365
+ break;
128
1366
  case "initialTree":
129
1367
  this.handleInitialTree(message);
130
1368
  break;
@@ -133,14 +1371,28 @@ class RemoteEngine {
133
1371
  break;
134
1372
  case "stateUpdate":
135
1373
  this.currentState = message.state;
136
- this.stateCallbacks.forEach((cb) => cb(message.state));
1374
+ this.stateCallbacks.forEach((cb) => cb(this.currentState));
137
1375
  break;
138
1376
  }
139
- } catch (error) {
140
- console.error("Error handling remote message:", error);
141
- this.errorCallbacks.forEach((cb) => cb(error instanceof Error ? error : new Error(String(error))));
1377
+ } catch (e) {
1378
+ console.error("Error handling remote message:", e);
1379
+ const error = e instanceof Error ? e : new Error(String(e));
1380
+ this.errorCallbacks.forEach((cb) => cb(error));
142
1381
  }
143
1382
  }
1383
+ handleSessionAck(message) {
1384
+ this.currentSessionId = message.sessionId;
1385
+ const info = {
1386
+ sessionId: message.sessionId,
1387
+ isNew: message.isNew,
1388
+ isRestored: message.isRestored
1389
+ };
1390
+ this.sessionEstablishedCallbacks.forEach((cb) => cb(info));
1391
+ }
1392
+ handleSessionExpired(message) {
1393
+ this.currentSessionId = null;
1394
+ this.sessionExpiredCallbacks.forEach((cb) => cb(message.reason));
1395
+ }
144
1396
  handleInitialTree(message) {
145
1397
  this.moduleName = message.module;
146
1398
  this.currentState = message.state;
@@ -164,21 +1416,530 @@ class RemoteEngine {
164
1416
  if (!this.options.autoReconnect) {
165
1417
  return;
166
1418
  }
167
- if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
168
- console.error("Max reconnection attempts reached");
169
- return;
170
- }
171
- this.reconnectAttempts++;
172
- console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`);
173
- this.reconnectTimer = setTimeout(() => {
174
- this.connect().catch((error) => {
175
- console.error("Reconnection failed:", error);
1419
+ this.reconnectDisposable = disposableTimeout(() => {
1420
+ this.reconnectDisposable = null;
1421
+ retry(async () => {
1422
+ const result = await this.connect();
1423
+ if (!result.ok) {
1424
+ throw result.error;
1425
+ }
1426
+ }, {
1427
+ maxAttempts: this.options.maxReconnectAttempts,
1428
+ delayMs: this.options.reconnectInterval,
1429
+ backoff: "exponential",
1430
+ maxDelayMs: 30000,
1431
+ jitter: 0.1,
1432
+ onRetry: (attempt, error) => {
1433
+ console.log(`Reconnection attempt ${attempt}/${this.options.maxReconnectAttempts} failed: ${error.message}`);
1434
+ }
1435
+ }).catch((error) => {
1436
+ console.error("Max reconnection attempts reached:", error.message);
1437
+ this.errorCallbacks.forEach((cb) => cb(new ConnectionError(this.url, error, this.options.maxReconnectAttempts)));
176
1438
  });
177
1439
  }, this.options.reconnectInterval);
178
1440
  }
179
1441
  }
1442
+ // src/remote/server.ts
1443
+ init_app();
1444
+
1445
+ // src/remote/session.ts
1446
+ class SessionManager {
1447
+ activeSessions = new Map;
1448
+ pendingSessions = new Map;
1449
+ sessionConnections = new Map;
1450
+ config;
1451
+ constructor(config2 = {}) {
1452
+ this.config = {
1453
+ ttl: config2.ttl ?? 3600,
1454
+ concurrent: config2.concurrent ?? "kick-old",
1455
+ generateId: config2.generateId ?? (() => crypto.randomUUID())
1456
+ };
1457
+ }
1458
+ getTtl() {
1459
+ return this.config.ttl;
1460
+ }
1461
+ getConcurrentPolicy() {
1462
+ return this.config.concurrent;
1463
+ }
1464
+ createSession(props) {
1465
+ const now = new Date;
1466
+ const session = {
1467
+ id: this.config.generateId(),
1468
+ ttl: this.config.ttl,
1469
+ createdAt: now,
1470
+ lastConnectedAt: now,
1471
+ props
1472
+ };
1473
+ this.activeSessions.set(session.id, session);
1474
+ return session;
1475
+ }
1476
+ getActiveSession(id) {
1477
+ return this.activeSessions.get(id) ?? null;
1478
+ }
1479
+ getPendingSession(id) {
1480
+ return this.pendingSessions.get(id) ?? null;
1481
+ }
1482
+ hasSession(id) {
1483
+ return this.activeSessions.has(id) || this.pendingSessions.has(id);
1484
+ }
1485
+ suspendSession(sessionId, savedState, onExpire) {
1486
+ const session = this.activeSessions.get(sessionId);
1487
+ if (!session)
1488
+ return;
1489
+ this.activeSessions.delete(sessionId);
1490
+ const expiryTimer = setTimeout(async () => {
1491
+ const pending = this.pendingSessions.get(sessionId);
1492
+ if (pending) {
1493
+ this.pendingSessions.delete(sessionId);
1494
+ await onExpire(pending.session);
1495
+ }
1496
+ }, session.ttl * 1000);
1497
+ this.pendingSessions.set(sessionId, {
1498
+ session,
1499
+ savedState,
1500
+ expiryTimer
1501
+ });
1502
+ }
1503
+ resumeSession(sessionId) {
1504
+ const pending = this.pendingSessions.get(sessionId);
1505
+ if (!pending)
1506
+ return null;
1507
+ clearTimeout(pending.expiryTimer);
1508
+ this.pendingSessions.delete(sessionId);
1509
+ pending.session.lastConnectedAt = new Date;
1510
+ this.activeSessions.set(sessionId, pending.session);
1511
+ return {
1512
+ session: pending.session,
1513
+ savedState: pending.savedState
1514
+ };
1515
+ }
1516
+ destroySession(sessionId) {
1517
+ this.activeSessions.delete(sessionId);
1518
+ const pending = this.pendingSessions.get(sessionId);
1519
+ if (pending) {
1520
+ clearTimeout(pending.expiryTimer);
1521
+ this.pendingSessions.delete(sessionId);
1522
+ }
1523
+ this.sessionConnections.delete(sessionId);
1524
+ }
1525
+ trackConnection(sessionId, ws) {
1526
+ let connections = this.sessionConnections.get(sessionId);
1527
+ if (!connections) {
1528
+ connections = new Set;
1529
+ this.sessionConnections.set(sessionId, connections);
1530
+ }
1531
+ connections.add(ws);
1532
+ }
1533
+ untrackConnection(sessionId, ws) {
1534
+ const connections = this.sessionConnections.get(sessionId);
1535
+ if (connections) {
1536
+ connections.delete(ws);
1537
+ if (connections.size === 0) {
1538
+ this.sessionConnections.delete(sessionId);
1539
+ }
1540
+ }
1541
+ }
1542
+ getConnections(sessionId) {
1543
+ return this.sessionConnections.get(sessionId);
1544
+ }
1545
+ getConnectionCount(sessionId) {
1546
+ return this.sessionConnections.get(sessionId)?.size ?? 0;
1547
+ }
1548
+ getStats() {
1549
+ let totalConnections = 0;
1550
+ for (const connections of this.sessionConnections.values()) {
1551
+ totalConnections += connections.size;
1552
+ }
1553
+ return {
1554
+ activeSessions: this.activeSessions.size,
1555
+ pendingSessions: this.pendingSessions.size,
1556
+ totalConnections
1557
+ };
1558
+ }
1559
+ destroy() {
1560
+ for (const pending of this.pendingSessions.values()) {
1561
+ clearTimeout(pending.expiryTimer);
1562
+ }
1563
+ this.activeSessions.clear();
1564
+ this.pendingSessions.clear();
1565
+ this.sessionConnections.clear();
1566
+ }
1567
+ }
1568
+
1569
+ // src/remote/server.ts
1570
+ class RemoteServer {
1571
+ _module = null;
1572
+ _moduleName = "App";
1573
+ _ui = "";
1574
+ _config = {};
1575
+ _sessionConfig = {};
1576
+ _onConnectionCallbacks = [];
1577
+ _onDisconnectionCallbacks = [];
1578
+ clients = new Map;
1579
+ nextClientId = 1;
1580
+ server = null;
1581
+ sessionManager = null;
1582
+ module(name, module) {
1583
+ this._moduleName = name;
1584
+ this._module = module;
1585
+ return this;
1586
+ }
1587
+ ui(dsl) {
1588
+ this._ui = dsl;
1589
+ return this;
1590
+ }
1591
+ config(config2) {
1592
+ this._config = { ...this._config, ...config2 };
1593
+ return this;
1594
+ }
1595
+ session(config2) {
1596
+ this._sessionConfig = config2;
1597
+ return this;
1598
+ }
1599
+ onConnection(callback) {
1600
+ this._onConnectionCallbacks.push(callback);
1601
+ return this;
1602
+ }
1603
+ onDisconnection(callback) {
1604
+ this._onDisconnectionCallbacks.push(callback);
1605
+ return this;
1606
+ }
1607
+ listen(port) {
1608
+ if (!this._module) {
1609
+ throw new Error("Module not set. Call .module() before .listen()");
1610
+ }
1611
+ if (!this._ui) {
1612
+ throw new Error("UI not set. Call .ui() before .listen()");
1613
+ }
1614
+ this.sessionManager = new SessionManager(this._sessionConfig);
1615
+ const finalPort = port ?? this._config.port ?? 3000;
1616
+ const hostname = this._config.hostname ?? "0.0.0.0";
1617
+ this.server = Bun.serve({
1618
+ port: finalPort,
1619
+ hostname,
1620
+ websocket: {
1621
+ open: (ws) => this.handleOpen(ws),
1622
+ message: (ws, message) => this.handleMessage(ws, message),
1623
+ close: (ws) => this.handleClose(ws)
1624
+ },
1625
+ fetch: (req, server) => {
1626
+ const url = new URL(req.url);
1627
+ if (server.upgrade(req, { data: undefined })) {
1628
+ return;
1629
+ }
1630
+ if (url.pathname === "/health") {
1631
+ return new Response("OK", { status: 200 });
1632
+ }
1633
+ if (url.pathname === "/stats") {
1634
+ const stats = this.sessionManager?.getStats() ?? {
1635
+ activeSessions: 0,
1636
+ pendingSessions: 0,
1637
+ totalConnections: 0
1638
+ };
1639
+ return new Response(JSON.stringify(stats), {
1640
+ headers: { "Content-Type": "application/json" }
1641
+ });
1642
+ }
1643
+ return new Response("Hypen Remote Server", { status: 200 });
1644
+ }
1645
+ });
1646
+ console.log(`\uD83D\uDE80 Hypen app streaming on ws://${hostname}:${finalPort}`);
1647
+ return this;
1648
+ }
1649
+ stop() {
1650
+ if (this.server) {
1651
+ this.server.stop();
1652
+ this.server = null;
1653
+ }
1654
+ if (this.sessionManager) {
1655
+ this.sessionManager.destroy();
1656
+ this.sessionManager = null;
1657
+ }
1658
+ }
1659
+ get url() {
1660
+ if (!this.server)
1661
+ return null;
1662
+ const hostname = this._config.hostname ?? "localhost";
1663
+ const port = this._config.port ?? 3000;
1664
+ return `ws://${hostname}:${port}`;
1665
+ }
1666
+ async handleOpen(ws) {
1667
+ try {
1668
+ const clientId = `client_${this.nextClientId++}`;
1669
+ const connectedAt = new Date;
1670
+ const engine = new Engine;
1671
+ await engine.init();
1672
+ const moduleInstance = new HypenModuleInstance(engine, this._module);
1673
+ const clientData = {
1674
+ id: clientId,
1675
+ engine,
1676
+ moduleInstance,
1677
+ revision: 0,
1678
+ connectedAt,
1679
+ sessionId: "",
1680
+ helloReceived: false
1681
+ };
1682
+ this.clients.set(ws, clientData);
1683
+ clientData.helloTimeout = setTimeout(() => {
1684
+ if (!clientData.helloReceived) {
1685
+ this.initializeSession(ws, clientData, undefined, undefined);
1686
+ }
1687
+ }, 1000);
1688
+ } catch (error) {
1689
+ console.error("Error handling WebSocket open:", error);
1690
+ ws.close(1011, "Internal server error");
1691
+ }
1692
+ }
1693
+ async initializeSession(ws, clientData, requestedSessionId, props) {
1694
+ if (clientData.helloReceived)
1695
+ return;
1696
+ clientData.helloReceived = true;
1697
+ if (clientData.helloTimeout) {
1698
+ clearTimeout(clientData.helloTimeout);
1699
+ clientData.helloTimeout = undefined;
1700
+ }
1701
+ let session;
1702
+ let isNew = true;
1703
+ let isRestored = false;
1704
+ let restoredState = null;
1705
+ if (requestedSessionId && this.sessionManager) {
1706
+ const resumed = this.sessionManager.resumeSession(requestedSessionId);
1707
+ if (resumed) {
1708
+ session = resumed.session;
1709
+ restoredState = resumed.savedState;
1710
+ isNew = false;
1711
+ isRestored = true;
1712
+ await this.triggerReconnect(clientData, session, restoredState);
1713
+ } else {
1714
+ const activeSession = this.sessionManager.getActiveSession(requestedSessionId);
1715
+ if (activeSession) {
1716
+ const handled = await this.handleConcurrentConnection(ws, clientData, activeSession, props);
1717
+ if (!handled)
1718
+ return;
1719
+ session = activeSession;
1720
+ isNew = false;
1721
+ } else {
1722
+ session = this.sessionManager.createSession(props);
1723
+ }
1724
+ }
1725
+ } else if (this.sessionManager) {
1726
+ session = this.sessionManager.createSession(props);
1727
+ } else {
1728
+ session = {
1729
+ id: crypto.randomUUID(),
1730
+ ttl: 3600,
1731
+ createdAt: new Date,
1732
+ lastConnectedAt: new Date,
1733
+ props
1734
+ };
1735
+ }
1736
+ clientData.sessionId = session.id;
1737
+ this.sessionManager?.trackConnection(session.id, ws);
1738
+ const sessionAck = {
1739
+ type: "sessionAck",
1740
+ sessionId: session.id,
1741
+ isNew,
1742
+ isRestored
1743
+ };
1744
+ ws.send(JSON.stringify(sessionAck));
1745
+ this.setupRenderCallback(ws, clientData);
1746
+ const initialPatches = [];
1747
+ clientData.engine.setRenderCallback((patches) => {
1748
+ initialPatches.push(...patches);
1749
+ });
1750
+ clientData.engine.renderSource(this._ui);
1751
+ this.setupRenderCallback(ws, clientData);
1752
+ const initialMessage = {
1753
+ type: "initialTree",
1754
+ module: this._moduleName,
1755
+ state: clientData.moduleInstance.getState(),
1756
+ patches: initialPatches,
1757
+ revision: 0
1758
+ };
1759
+ ws.send(JSON.stringify(initialMessage));
1760
+ const client = {
1761
+ id: clientData.id,
1762
+ socket: ws,
1763
+ connectedAt: clientData.connectedAt
1764
+ };
1765
+ this._onConnectionCallbacks.forEach((cb) => cb(client));
1766
+ }
1767
+ setupRenderCallback(ws, clientData) {
1768
+ clientData.engine.setRenderCallback((patches) => {
1769
+ const data = this.clients.get(ws);
1770
+ if (!data)
1771
+ return;
1772
+ data.revision++;
1773
+ const message = {
1774
+ type: "patch",
1775
+ module: this._moduleName,
1776
+ patches,
1777
+ revision: data.revision
1778
+ };
1779
+ ws.send(JSON.stringify(message));
1780
+ if (this.sessionManager?.getConcurrentPolicy() === "allow-multiple" && data.sessionId) {
1781
+ const connections = this.sessionManager.getConnections(data.sessionId);
1782
+ if (connections) {
1783
+ for (const conn of connections) {
1784
+ if (conn !== ws) {
1785
+ conn.send(JSON.stringify(message));
1786
+ }
1787
+ }
1788
+ }
1789
+ }
1790
+ });
1791
+ }
1792
+ async handleConcurrentConnection(ws, clientData, existingSession, props) {
1793
+ const policy = this.sessionManager?.getConcurrentPolicy() ?? "kick-old";
1794
+ switch (policy) {
1795
+ case "kick-old": {
1796
+ const existingConnections = this.sessionManager?.getConnections(existingSession.id);
1797
+ if (existingConnections) {
1798
+ for (const conn of existingConnections) {
1799
+ const oldWs = conn;
1800
+ const expiredMsg = {
1801
+ type: "sessionExpired",
1802
+ sessionId: existingSession.id,
1803
+ reason: "kicked"
1804
+ };
1805
+ oldWs.send(JSON.stringify(expiredMsg));
1806
+ oldWs.close(1000, "Session taken by new connection");
1807
+ }
1808
+ }
1809
+ return true;
1810
+ }
1811
+ case "reject-new": {
1812
+ const expiredMsg = {
1813
+ type: "sessionExpired",
1814
+ sessionId: existingSession.id,
1815
+ reason: "kicked"
1816
+ };
1817
+ ws.send(JSON.stringify(expiredMsg));
1818
+ ws.close(1000, "Session already active");
1819
+ return false;
1820
+ }
1821
+ case "allow-multiple": {
1822
+ return true;
1823
+ }
1824
+ default:
1825
+ return true;
1826
+ }
1827
+ }
1828
+ async triggerReconnect(clientData, session, savedState) {
1829
+ const handler = this._module?.handlers.onReconnect;
1830
+ if (!handler)
1831
+ return;
1832
+ let restored = false;
1833
+ const restore = (state) => {
1834
+ restored = true;
1835
+ clientData.moduleInstance.updateState(state);
1836
+ };
1837
+ await handler({ session, restore });
1838
+ }
1839
+ handleMessage(ws, message) {
1840
+ try {
1841
+ const clientData = this.clients.get(ws);
1842
+ if (!clientData)
1843
+ return;
1844
+ const msg = JSON.parse(message.toString());
1845
+ switch (msg.type) {
1846
+ case "hello": {
1847
+ const helloMsg = msg;
1848
+ this.initializeSession(ws, clientData, helloMsg.sessionId, helloMsg.props);
1849
+ break;
1850
+ }
1851
+ case "dispatchAction": {
1852
+ const actionMsg = msg;
1853
+ clientData.engine.dispatchAction(actionMsg.action, actionMsg.payload);
1854
+ break;
1855
+ }
1856
+ default:
1857
+ break;
1858
+ }
1859
+ } catch (error) {
1860
+ console.error("Error handling WebSocket message:", error);
1861
+ }
1862
+ }
1863
+ async handleClose(ws) {
1864
+ const clientData = this.clients.get(ws);
1865
+ if (!clientData)
1866
+ return;
1867
+ if (clientData.helloTimeout) {
1868
+ clearTimeout(clientData.helloTimeout);
1869
+ }
1870
+ const currentState = clientData.moduleInstance.getState();
1871
+ if (clientData.sessionId && this._module?.handlers.onDisconnect) {
1872
+ const session = this.sessionManager?.getActiveSession(clientData.sessionId);
1873
+ if (session) {
1874
+ await this._module.handlers.onDisconnect({
1875
+ state: currentState,
1876
+ session
1877
+ });
1878
+ }
1879
+ }
1880
+ if (clientData.sessionId && this.sessionManager) {
1881
+ this.sessionManager.untrackConnection(clientData.sessionId, ws);
1882
+ if (this.sessionManager.getConnectionCount(clientData.sessionId) === 0) {
1883
+ const session = this.sessionManager.getActiveSession(clientData.sessionId);
1884
+ if (session) {
1885
+ this.sessionManager.suspendSession(clientData.sessionId, currentState, async (expiredSession) => {
1886
+ if (this._module?.handlers.onExpire) {
1887
+ await this._module.handlers.onExpire({ session: expiredSession });
1888
+ }
1889
+ });
1890
+ }
1891
+ }
1892
+ }
1893
+ await clientData.moduleInstance.destroy();
1894
+ this.clients.delete(ws);
1895
+ const client = {
1896
+ id: clientData.id,
1897
+ socket: ws,
1898
+ connectedAt: clientData.connectedAt
1899
+ };
1900
+ this._onDisconnectionCallbacks.forEach((cb) => cb(client));
1901
+ }
1902
+ getClientCount() {
1903
+ return this.clients.size;
1904
+ }
1905
+ getSessionStats() {
1906
+ return this.sessionManager?.getStats() ?? {
1907
+ activeSessions: 0,
1908
+ pendingSessions: 0,
1909
+ totalConnections: 0
1910
+ };
1911
+ }
1912
+ broadcast(message) {
1913
+ const json = JSON.stringify(message);
1914
+ this.clients.forEach((_, ws) => {
1915
+ ws.send(json);
1916
+ });
1917
+ }
1918
+ }
1919
+ function serve(options) {
1920
+ const server = new RemoteServer().module(options.moduleName ?? "App", options.module).ui(options.ui);
1921
+ if (options.port || options.hostname) {
1922
+ server.config({
1923
+ port: options.port,
1924
+ hostname: options.hostname
1925
+ });
1926
+ }
1927
+ if (options.session) {
1928
+ server.session(options.session);
1929
+ }
1930
+ if (options.onConnection) {
1931
+ server.onConnection(options.onConnection);
1932
+ }
1933
+ if (options.onDisconnection) {
1934
+ server.onDisconnection(options.onDisconnection);
1935
+ }
1936
+ return server.listen(options.port);
1937
+ }
180
1938
  export {
1939
+ serve,
1940
+ SessionManager,
1941
+ RemoteServer,
181
1942
  RemoteEngine
182
1943
  };
183
1944
 
184
- //# debugId=2AAF271D9A6427D964756E2164756E21
1945
+ //# debugId=A4276C2CD80CD66C64756E2164756E21