@hypen-space/core 0.2.12 → 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 (45) 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 +862 -81
  11. package/dist/src/index.browser.js.map +10 -6
  12. package/dist/src/index.js +1590 -124
  13. package/dist/src/index.js.map +16 -9
  14. package/dist/src/remote/client.js +525 -35
  15. package/dist/src/remote/client.js.map +7 -4
  16. package/dist/src/remote/index.js +1796 -35
  17. package/dist/src/remote/index.js.map +13 -4
  18. package/dist/src/router.js +55 -29
  19. package/dist/src/router.js.map +3 -3
  20. package/dist/src/state.js +57 -29
  21. package/dist/src/state.js.map +3 -3
  22. package/package.json +8 -2
  23. package/src/app.ts +292 -13
  24. package/src/discovery.ts +123 -18
  25. package/src/disposable.ts +281 -0
  26. package/src/engine.ts +29 -10
  27. package/src/hypen.ts +209 -0
  28. package/src/index.ts +147 -11
  29. package/src/logger.ts +338 -0
  30. package/src/remote/client.ts +263 -56
  31. package/src/remote/index.ts +25 -1
  32. package/src/remote/server.ts +652 -0
  33. package/src/remote/session.ts +256 -0
  34. package/src/remote/types.ts +68 -1
  35. package/src/result.ts +260 -0
  36. package/src/retry.ts +306 -0
  37. package/src/state.ts +103 -45
  38. package/wasm-browser/README.md +4 -0
  39. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  40. package/wasm-browser/package.json +1 -1
  41. package/wasm-node/README.md +4 -0
  42. package/wasm-node/hypen_engine_bg.wasm +0 -0
  43. package/wasm-node/package.json +1 -1
  44. package/wasm-browser/hypen_engine_bg.js +0 -736
  45. package/wasm-node/hypen_engine_bg.js +0 -736
package/dist/src/index.js CHANGED
@@ -15,39 +15,34 @@ function deepClone(obj) {
15
15
  if (obj === null || typeof obj !== "object") {
16
16
  return obj;
17
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
+ }
18
27
  const visited = new WeakMap;
19
28
  function cloneInternal(value) {
20
29
  if (value === null || typeof value !== "object") {
21
30
  return value;
22
31
  }
32
+ if (typeof value === "function") {
33
+ return value;
34
+ }
23
35
  if (visited.has(value)) {
24
36
  return visited.get(value);
25
37
  }
26
- if (value instanceof Date) {
27
- return new Date(value.getTime());
28
- }
29
- if (value instanceof RegExp) {
30
- return new RegExp(value.source, value.flags);
31
- }
32
- if (value instanceof Map) {
33
- const mapClone = new Map;
34
- visited.set(value, mapClone);
35
- for (const [k, v] of value.entries()) {
36
- mapClone.set(cloneInternal(k), cloneInternal(v));
37
- }
38
- return mapClone;
39
- }
40
- if (value instanceof Set) {
41
- const setClone = new Set;
42
- visited.set(value, setClone);
43
- for (const item of value.values()) {
44
- setClone.add(cloneInternal(item));
45
- }
46
- return setClone;
47
- }
48
38
  if (value instanceof WeakMap || value instanceof WeakSet) {
49
39
  return value;
50
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
+ }
51
46
  if (Array.isArray(value)) {
52
47
  const arrClone = [];
53
48
  visited.set(value, arrClone);
@@ -59,7 +54,7 @@ function deepClone(obj) {
59
54
  const objClone = {};
60
55
  visited.set(value, objClone);
61
56
  for (const key in value) {
62
- if (value.hasOwnProperty(key)) {
57
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
63
58
  objClone[key] = cloneInternal(value[key]);
64
59
  }
65
60
  }
@@ -161,12 +156,15 @@ function createObservableState(initialState, options) {
161
156
  }
162
157
  const proxyCache = new WeakMap;
163
158
  function createProxy(target, basePath) {
164
- if (proxyCache.has(target)) {
165
- return proxyCache.get(target);
166
- }
159
+ const cached = proxyCache.get(target);
160
+ if (cached)
161
+ return cached;
167
162
  const proxy = new Proxy(target, {
168
163
  get(obj, prop) {
169
- const value = obj[prop];
164
+ if (prop === IS_PROXY)
165
+ return true;
166
+ if (prop === RAW_TARGET)
167
+ return obj;
170
168
  if (prop === "__beginBatch") {
171
169
  return () => {
172
170
  batchDepth++;
@@ -183,16 +181,28 @@ function createObservableState(initialState, options) {
183
181
  if (prop === "__getSnapshot") {
184
182
  return () => deepClone(obj);
185
183
  }
184
+ const value = obj[prop];
186
185
  if (value && typeof value === "object") {
186
+ if (value[IS_PROXY]) {
187
+ return value;
188
+ }
187
189
  if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
188
190
  return value;
189
191
  }
190
- return createProxy(value, basePath ? `${basePath}.${String(prop)}` : String(prop));
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;
191
198
  }
192
199
  return value;
193
200
  },
194
201
  set(obj, prop, value) {
195
202
  const oldValue = obj[prop];
203
+ if (value && typeof value === "object" && value[IS_PROXY]) {
204
+ value = value[RAW_TARGET];
205
+ }
196
206
  obj[prop] = value;
197
207
  if (oldValue !== value) {
198
208
  scheduleBatch();
@@ -233,6 +243,344 @@ function getStateSnapshot(state) {
233
243
  }
234
244
  return deepClone(state);
235
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
+ });
236
584
 
237
585
  // src/app.ts
238
586
  var exports_app = {};
@@ -249,6 +597,11 @@ class HypenAppBuilder {
249
597
  createdHandler;
250
598
  actionHandlers = new Map;
251
599
  destroyedHandler;
600
+ disconnectHandler;
601
+ reconnectHandler;
602
+ expireHandler;
603
+ errorHandler;
604
+ template;
252
605
  constructor(initialState, options) {
253
606
  this.initialState = initialState;
254
607
  this.options = options || {};
@@ -265,6 +618,26 @@ class HypenAppBuilder {
265
618
  this.destroyedHandler = fn;
266
619
  return this;
267
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
+ }
268
641
  build() {
269
642
  const stateKeys = this.initialState !== null && typeof this.initialState === "object" ? Object.keys(this.initialState) : [];
270
643
  return {
@@ -274,10 +647,15 @@ class HypenAppBuilder {
274
647
  persist: this.options.persist,
275
648
  version: this.options.version,
276
649
  initialState: this.initialState,
650
+ template: this.template,
277
651
  handlers: {
278
652
  onCreated: this.createdHandler,
279
653
  onAction: this.actionHandlers,
280
- onDestroyed: this.destroyedHandler
654
+ onDestroyed: this.destroyedHandler,
655
+ onDisconnect: this.disconnectHandler,
656
+ onReconnect: this.reconnectHandler,
657
+ onExpire: this.expireHandler,
658
+ onError: this.errorHandler
281
659
  }
282
660
  };
283
661
  }
@@ -310,9 +688,9 @@ class HypenModuleInstance {
310
688
  });
311
689
  this.engine.setModule(definition.name || "AnonymousModule", definition.actions, definition.stateKeys, getStateSnapshot(this.state));
312
690
  for (const [actionName, handler] of definition.handlers.onAction) {
313
- console.log(`[ModuleInstance] Registering action handler: ${actionName} for module ${definition.name}`);
691
+ log2.debug(`Registering action handler: ${actionName} for module ${definition.name}`);
314
692
  this.engine.onAction(actionName, async (action) => {
315
- console.log(`[ModuleInstance] Action handler fired: ${actionName}`, action);
693
+ log2.debug(`Action handler fired: ${actionName}`, action);
316
694
  const actionCtx = {
317
695
  name: action.name,
318
696
  payload: action.payload,
@@ -322,17 +700,19 @@ class HypenModuleInstance {
322
700
  router: this.routerContext?.root || null
323
701
  };
324
702
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
325
- try {
326
- await handler({
327
- action: actionCtx,
328
- state: this.state,
329
- next,
330
- context
331
- });
332
- console.log(`[ModuleInstance] Action handler completed: ${actionName}`);
333
- } catch (error) {
334
- console.error(`[ModuleInstance] Action handler error for ${actionName}:`, error);
335
- throw error;
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}`);
336
716
  }
337
717
  });
338
718
  }
@@ -359,6 +739,48 @@ class HypenModuleInstance {
359
739
  }
360
740
  return api;
361
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
+ }
362
784
  async callCreatedHandler() {
363
785
  if (this.definition.handlers.onCreated) {
364
786
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
@@ -386,13 +808,30 @@ class HypenModuleInstance {
386
808
  Object.assign(this.state, patch);
387
809
  }
388
810
  }
389
- var app;
811
+ var log2, app;
390
812
  var init_app = __esm(() => {
813
+ init_result();
814
+ init_state();
815
+ init_logger();
816
+ log2 = createLogger("ModuleInstance");
391
817
  app = new HypenApp;
392
818
  });
393
819
 
394
820
  // src/engine.ts
395
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
+ }
396
835
 
397
836
  class Engine {
398
837
  wasmEngine = null;
@@ -432,30 +871,26 @@ class Engine {
432
871
  }
433
872
  renderInto(source, parentNodeId, state) {
434
873
  const engine = this.ensureInitialized();
435
- const safeState = JSON.parse(JSON.stringify(state));
436
- engine.renderInto(source, parentNodeId, safeState);
874
+ engine.renderInto(source, parentNodeId, unwrapForWasm(state));
437
875
  }
438
876
  notifyStateChange(paths, values) {
439
877
  const engine = this.ensureInitialized();
440
878
  if (paths.length === 0) {
441
879
  return;
442
880
  }
443
- const plainValues = JSON.parse(JSON.stringify(values));
444
- engine.updateStateSparse(paths, plainValues);
881
+ engine.updateStateSparse(paths, unwrapForWasm(values));
445
882
  console.debug("[Hypen] State changed (sparse):", paths);
446
883
  }
447
884
  notifyStateChangeFull(paths, currentState) {
448
885
  const engine = this.ensureInitialized();
449
- const plainObject = JSON.parse(JSON.stringify(currentState));
450
- engine.updateState(plainObject);
886
+ engine.updateState(unwrapForWasm(currentState));
451
887
  if (paths.length > 0) {
452
888
  console.debug("[Hypen] State changed (full):", paths);
453
889
  }
454
890
  }
455
891
  updateState(statePatch) {
456
892
  const engine = this.ensureInitialized();
457
- const plainObject = JSON.parse(JSON.stringify(statePatch));
458
- engine.updateState(plainObject);
893
+ engine.updateState(unwrapForWasm(statePatch));
459
894
  }
460
895
  dispatchAction(name, payload) {
461
896
  const engine = this.ensureInitialized();
@@ -656,6 +1091,8 @@ class ConsoleRenderer {
656
1091
  }
657
1092
 
658
1093
  // src/router.ts
1094
+ init_state();
1095
+
659
1096
  class HypenRouter {
660
1097
  state;
661
1098
  subscribers = new Set;
@@ -802,12 +1239,12 @@ class HypenRouter {
802
1239
  return "([^/]+)";
803
1240
  }).replace(/\*/g, ".*");
804
1241
  const regex = new RegExp(`^${regexPattern}$`);
805
- const match = path.match(regex);
806
- if (!match)
1242
+ const match2 = path.match(regex);
1243
+ if (!match2)
807
1244
  return null;
808
1245
  const params = {};
809
1246
  paramNames.forEach((name, i) => {
810
- const value = match[i + 1];
1247
+ const value = match2[i + 1];
811
1248
  if (value !== undefined) {
812
1249
  params[name] = decodeURIComponent(value);
813
1250
  }
@@ -1017,6 +1454,281 @@ class HypenGlobalContext {
1017
1454
  }
1018
1455
  }
1019
1456
 
1457
+ // src/remote/client.ts
1458
+ init_result();
1459
+
1460
+ // src/disposable.ts
1461
+ function isDisposable(obj) {
1462
+ return obj !== null && typeof obj === "object" && "dispose" in obj && typeof obj.dispose === "function";
1463
+ }
1464
+
1465
+ class DisposableStack {
1466
+ stack = [];
1467
+ disposed = false;
1468
+ add(disposable) {
1469
+ if (this.disposed) {
1470
+ disposable.dispose();
1471
+ return disposable;
1472
+ }
1473
+ this.stack.push(disposable);
1474
+ return disposable;
1475
+ }
1476
+ addCallback(callback) {
1477
+ this.add({ dispose: callback });
1478
+ }
1479
+ addValue(value, dispose) {
1480
+ this.add({ dispose: () => dispose(value) });
1481
+ return value;
1482
+ }
1483
+ dispose() {
1484
+ if (this.disposed)
1485
+ return;
1486
+ this.disposed = true;
1487
+ while (this.stack.length > 0) {
1488
+ const item = this.stack.pop();
1489
+ try {
1490
+ item.dispose();
1491
+ } catch (error) {
1492
+ console.error("[DisposableStack] Error during dispose:", error);
1493
+ }
1494
+ }
1495
+ }
1496
+ get isDisposed() {
1497
+ return this.disposed;
1498
+ }
1499
+ get size() {
1500
+ return this.stack.length;
1501
+ }
1502
+ }
1503
+ function disposableListener(target, event, handler, options) {
1504
+ target.addEventListener(event, handler, options);
1505
+ return {
1506
+ dispose: () => target.removeEventListener(event, handler, options)
1507
+ };
1508
+ }
1509
+ function disposableTimeout(callback, ms) {
1510
+ const id = setTimeout(callback, ms);
1511
+ return {
1512
+ id,
1513
+ dispose: () => clearTimeout(id)
1514
+ };
1515
+ }
1516
+ function disposableInterval(callback, ms) {
1517
+ const id = setInterval(callback, ms);
1518
+ return {
1519
+ id,
1520
+ dispose: () => clearInterval(id)
1521
+ };
1522
+ }
1523
+ function disposableWebSocket(ws) {
1524
+ return {
1525
+ dispose: () => {
1526
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
1527
+ ws.close();
1528
+ }
1529
+ }
1530
+ };
1531
+ }
1532
+ function disposableAbortController() {
1533
+ const controller = new AbortController;
1534
+ return {
1535
+ controller,
1536
+ signal: controller.signal,
1537
+ dispose: () => controller.abort()
1538
+ };
1539
+ }
1540
+ function disposableSubscription(unsubscribe) {
1541
+ return { dispose: unsubscribe };
1542
+ }
1543
+ var ELEMENT_DISPOSABLES = Symbol("hypen.disposables");
1544
+ function getElementDisposables(element) {
1545
+ const existing = element[ELEMENT_DISPOSABLES];
1546
+ if (existing instanceof DisposableStack) {
1547
+ return existing;
1548
+ }
1549
+ const stack = new DisposableStack;
1550
+ element[ELEMENT_DISPOSABLES] = stack;
1551
+ return stack;
1552
+ }
1553
+ function disposeElement(element) {
1554
+ const stack = element[ELEMENT_DISPOSABLES];
1555
+ if (stack instanceof DisposableStack) {
1556
+ stack.dispose();
1557
+ delete element[ELEMENT_DISPOSABLES];
1558
+ }
1559
+ }
1560
+ function hasElementDisposables(element) {
1561
+ return element[ELEMENT_DISPOSABLES] instanceof DisposableStack;
1562
+ }
1563
+
1564
+ class DisposableMixin {
1565
+ disposables = new DisposableStack;
1566
+ track(disposable) {
1567
+ return this.disposables.add(disposable);
1568
+ }
1569
+ onDispose(callback) {
1570
+ this.disposables.addCallback(callback);
1571
+ }
1572
+ dispose() {
1573
+ this.disposables.dispose();
1574
+ }
1575
+ }
1576
+ function compositeDisposable(...disposables) {
1577
+ return {
1578
+ dispose: () => {
1579
+ for (const d of disposables) {
1580
+ try {
1581
+ d.dispose();
1582
+ } catch (error) {
1583
+ console.error("[compositeDisposable] Error during dispose:", error);
1584
+ }
1585
+ }
1586
+ }
1587
+ };
1588
+ }
1589
+ async function using(resource, fn) {
1590
+ const r = typeof resource === "function" ? resource() : resource;
1591
+ try {
1592
+ return await fn(r);
1593
+ } finally {
1594
+ r.dispose();
1595
+ }
1596
+ }
1597
+ function usingSync(resource, fn) {
1598
+ const r = typeof resource === "function" ? resource() : resource;
1599
+ try {
1600
+ return fn(r);
1601
+ } finally {
1602
+ r.dispose();
1603
+ }
1604
+ }
1605
+
1606
+ // src/retry.ts
1607
+ init_result();
1608
+ var DEFAULT_OPTIONS = {
1609
+ maxAttempts: 3,
1610
+ delayMs: 1000,
1611
+ backoff: "exponential",
1612
+ maxDelayMs: 30000,
1613
+ jitter: 0.1
1614
+ };
1615
+ function calculateDelay(attempt, options) {
1616
+ let delay;
1617
+ switch (options.backoff) {
1618
+ case "exponential":
1619
+ delay = options.delayMs * Math.pow(2, attempt - 1);
1620
+ break;
1621
+ case "linear":
1622
+ delay = options.delayMs * attempt;
1623
+ break;
1624
+ case "none":
1625
+ delay = options.delayMs;
1626
+ break;
1627
+ }
1628
+ if (options.jitter > 0) {
1629
+ const jitterRange = delay * options.jitter;
1630
+ delay += (Math.random() * 2 - 1) * jitterRange;
1631
+ }
1632
+ return Math.min(delay, options.maxDelayMs);
1633
+ }
1634
+ function sleep(ms, signal) {
1635
+ return new Promise((resolve, reject) => {
1636
+ if (signal?.aborted) {
1637
+ reject(new Error("Retry aborted"));
1638
+ return;
1639
+ }
1640
+ const timeoutId = setTimeout(resolve, ms);
1641
+ signal?.addEventListener("abort", () => {
1642
+ clearTimeout(timeoutId);
1643
+ reject(new Error("Retry aborted"));
1644
+ });
1645
+ });
1646
+ }
1647
+ async function retry(fn, options = {}) {
1648
+ const opts = { ...DEFAULT_OPTIONS, ...options };
1649
+ let lastError = new Error("No attempts made");
1650
+ for (let attempt = 1;attempt <= opts.maxAttempts; attempt++) {
1651
+ try {
1652
+ if (opts.signal?.aborted) {
1653
+ throw new Error("Retry aborted");
1654
+ }
1655
+ return await fn();
1656
+ } catch (e) {
1657
+ lastError = e instanceof Error ? e : new Error(String(e));
1658
+ if (opts.shouldRetry && !opts.shouldRetry(lastError)) {
1659
+ throw lastError;
1660
+ }
1661
+ if (attempt === opts.maxAttempts) {
1662
+ break;
1663
+ }
1664
+ const delayMs = calculateDelay(attempt, opts);
1665
+ opts.onRetry?.(attempt, lastError, delayMs);
1666
+ await sleep(delayMs, opts.signal);
1667
+ }
1668
+ }
1669
+ throw lastError;
1670
+ }
1671
+ async function retryResult(fn, options = {}) {
1672
+ try {
1673
+ const value = await retry(fn, options);
1674
+ return Ok(value);
1675
+ } catch (e) {
1676
+ return Err(e instanceof Error ? e : new Error(String(e)));
1677
+ }
1678
+ }
1679
+ function withRetry(fn, options = {}) {
1680
+ return (...args) => retry(() => fn(...args), options);
1681
+ }
1682
+ var RetryConditions = {
1683
+ networkErrors: (error) => {
1684
+ const message = error.message.toLowerCase();
1685
+ return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("socket");
1686
+ },
1687
+ httpRetryable: (error) => {
1688
+ const status = error.status;
1689
+ if (!status)
1690
+ return false;
1691
+ return [408, 429, 500, 502, 503, 504].includes(status);
1692
+ },
1693
+ websocketErrors: (error) => {
1694
+ const message = error.message.toLowerCase();
1695
+ return message.includes("websocket") || message.includes("connection") || message.includes("close");
1696
+ },
1697
+ any: (...conditions) => (error) => conditions.some((c) => c(error)),
1698
+ all: (...conditions) => (error) => conditions.every((c) => c(error))
1699
+ };
1700
+ var RetryPresets = {
1701
+ aggressive: {
1702
+ maxAttempts: 10,
1703
+ delayMs: 500,
1704
+ backoff: "exponential",
1705
+ maxDelayMs: 60000,
1706
+ jitter: 0.2
1707
+ },
1708
+ conservative: {
1709
+ maxAttempts: 3,
1710
+ delayMs: 2000,
1711
+ backoff: "linear",
1712
+ maxDelayMs: 1e4,
1713
+ jitter: 0.1
1714
+ },
1715
+ fast: {
1716
+ maxAttempts: 5,
1717
+ delayMs: 100,
1718
+ backoff: "exponential",
1719
+ maxDelayMs: 2000,
1720
+ jitter: 0
1721
+ },
1722
+ websocket: {
1723
+ maxAttempts: 10,
1724
+ delayMs: 1000,
1725
+ backoff: "exponential",
1726
+ maxDelayMs: 30000,
1727
+ jitter: 0.1,
1728
+ shouldRetry: RetryConditions.websocketErrors
1729
+ }
1730
+ };
1731
+
1020
1732
  // src/remote/client.ts
1021
1733
  class RemoteEngine {
1022
1734
  ws = null;
@@ -1024,12 +1736,17 @@ class RemoteEngine {
1024
1736
  state = "disconnected";
1025
1737
  options;
1026
1738
  reconnectAttempts = 0;
1027
- reconnectTimer = null;
1739
+ disposables = new DisposableStack;
1740
+ reconnectDisposable = null;
1741
+ currentSessionId = null;
1742
+ sessionOptions;
1028
1743
  patchCallbacks = [];
1029
1744
  stateCallbacks = [];
1030
1745
  connectionCallbacks = [];
1031
1746
  disconnectionCallbacks = [];
1032
1747
  errorCallbacks = [];
1748
+ sessionEstablishedCallbacks = [];
1749
+ sessionExpiredCallbacks = [];
1033
1750
  currentState = null;
1034
1751
  currentRevision = 0;
1035
1752
  moduleName = "";
@@ -1038,54 +1755,85 @@ class RemoteEngine {
1038
1755
  this.options = {
1039
1756
  autoReconnect: options.autoReconnect ?? true,
1040
1757
  reconnectInterval: options.reconnectInterval ?? 3000,
1041
- maxReconnectAttempts: options.maxReconnectAttempts ?? 10
1758
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
1759
+ session: options.session
1042
1760
  };
1761
+ this.sessionOptions = options.session;
1762
+ if (options.session?.id) {
1763
+ this.currentSessionId = options.session.id;
1764
+ }
1043
1765
  }
1044
1766
  async connect() {
1045
1767
  if (this.state === "connected" || this.state === "connecting") {
1046
- return;
1768
+ return Ok(undefined);
1047
1769
  }
1048
1770
  this.state = "connecting";
1049
- return new Promise((resolve, reject) => {
1771
+ return new Promise((resolve) => {
1050
1772
  try {
1051
1773
  this.ws = new WebSocket(this.url);
1052
- this.ws.onopen = () => {
1053
- this.state = "connected";
1054
- this.reconnectAttempts = 0;
1055
- this.connectionCallbacks.forEach((cb) => cb());
1056
- resolve();
1057
- };
1058
- this.ws.onmessage = (event) => {
1774
+ this.disposables.add(disposableWebSocket(this.ws));
1775
+ const messageHandler = (event) => {
1059
1776
  this.handleMessage(event.data);
1060
1777
  };
1061
- this.ws.onerror = () => {
1778
+ this.disposables.add(disposableListener(this.ws, "message", messageHandler));
1779
+ const errorHandler = () => {
1062
1780
  this.state = "error";
1063
- const error = new Error("WebSocket error");
1781
+ const error = new ConnectionError(this.url, new Error("WebSocket error"));
1064
1782
  this.errorCallbacks.forEach((cb) => cb(error));
1065
- reject(error);
1783
+ resolve(Err(error));
1066
1784
  };
1067
- this.ws.onclose = () => {
1785
+ this.disposables.add(disposableListener(this.ws, "error", errorHandler));
1786
+ const closeHandler = () => {
1068
1787
  this.state = "disconnected";
1069
1788
  this.disconnectionCallbacks.forEach((cb) => cb());
1070
1789
  this.attemptReconnect();
1071
1790
  };
1072
- } catch (error) {
1791
+ this.disposables.add(disposableListener(this.ws, "close", closeHandler));
1792
+ this.ws.onopen = () => {
1793
+ this.state = "connected";
1794
+ this.reconnectAttempts = 0;
1795
+ if (this.reconnectDisposable) {
1796
+ this.reconnectDisposable.dispose();
1797
+ this.reconnectDisposable = null;
1798
+ }
1799
+ this.sendHello();
1800
+ this.connectionCallbacks.forEach((cb) => cb());
1801
+ resolve(Ok(undefined));
1802
+ };
1803
+ } catch (e) {
1073
1804
  this.state = "error";
1074
- reject(error);
1805
+ const error = new ConnectionError(this.url, e);
1806
+ resolve(Err(error));
1075
1807
  }
1076
1808
  });
1077
1809
  }
1810
+ sendHello() {
1811
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
1812
+ return;
1813
+ const hello = {
1814
+ type: "hello",
1815
+ sessionId: this.currentSessionId ?? this.sessionOptions?.id,
1816
+ props: this.sessionOptions?.props
1817
+ };
1818
+ this.ws.send(JSON.stringify(hello));
1819
+ }
1078
1820
  disconnect() {
1079
- if (this.reconnectTimer) {
1080
- clearTimeout(this.reconnectTimer);
1081
- this.reconnectTimer = null;
1821
+ if (this.reconnectDisposable) {
1822
+ this.reconnectDisposable.dispose();
1823
+ this.reconnectDisposable = null;
1082
1824
  }
1083
1825
  if (this.ws) {
1084
- this.ws.close();
1826
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
1827
+ this.ws.close();
1828
+ }
1085
1829
  this.ws = null;
1086
1830
  }
1087
1831
  this.state = "disconnected";
1088
1832
  }
1833
+ dispose() {
1834
+ this.disconnect();
1835
+ this.disposables.dispose();
1836
+ }
1089
1837
  dispatchAction(action, payload) {
1090
1838
  if (this.state !== "connected" || !this.ws) {
1091
1839
  console.warn("Cannot dispatch action: not connected");
@@ -1119,6 +1867,14 @@ class RemoteEngine {
1119
1867
  this.errorCallbacks.push(callback);
1120
1868
  return this;
1121
1869
  }
1870
+ onSessionEstablished(callback) {
1871
+ this.sessionEstablishedCallbacks.push(callback);
1872
+ return this;
1873
+ }
1874
+ onSessionExpired(callback) {
1875
+ this.sessionExpiredCallbacks.push(callback);
1876
+ return this;
1877
+ }
1122
1878
  getConnectionState() {
1123
1879
  return this.state;
1124
1880
  }
@@ -1128,10 +1884,19 @@ class RemoteEngine {
1128
1884
  getRevision() {
1129
1885
  return this.currentRevision;
1130
1886
  }
1887
+ getSessionId() {
1888
+ return this.currentSessionId;
1889
+ }
1131
1890
  handleMessage(data) {
1132
1891
  try {
1133
1892
  const message = JSON.parse(data);
1134
1893
  switch (message.type) {
1894
+ case "sessionAck":
1895
+ this.handleSessionAck(message);
1896
+ break;
1897
+ case "sessionExpired":
1898
+ this.handleSessionExpired(message);
1899
+ break;
1135
1900
  case "initialTree":
1136
1901
  this.handleInitialTree(message);
1137
1902
  break;
@@ -1140,13 +1905,27 @@ class RemoteEngine {
1140
1905
  break;
1141
1906
  case "stateUpdate":
1142
1907
  this.currentState = message.state;
1143
- this.stateCallbacks.forEach((cb) => cb(message.state));
1908
+ this.stateCallbacks.forEach((cb) => cb(this.currentState));
1144
1909
  break;
1145
1910
  }
1146
- } catch (error) {
1147
- console.error("Error handling remote message:", error);
1148
- this.errorCallbacks.forEach((cb) => cb(error instanceof Error ? error : new Error(String(error))));
1149
- }
1911
+ } catch (e) {
1912
+ console.error("Error handling remote message:", e);
1913
+ const error = e instanceof Error ? e : new Error(String(e));
1914
+ this.errorCallbacks.forEach((cb) => cb(error));
1915
+ }
1916
+ }
1917
+ handleSessionAck(message) {
1918
+ this.currentSessionId = message.sessionId;
1919
+ const info = {
1920
+ sessionId: message.sessionId,
1921
+ isNew: message.isNew,
1922
+ isRestored: message.isRestored
1923
+ };
1924
+ this.sessionEstablishedCallbacks.forEach((cb) => cb(info));
1925
+ }
1926
+ handleSessionExpired(message) {
1927
+ this.currentSessionId = null;
1928
+ this.sessionExpiredCallbacks.forEach((cb) => cb(message.reason));
1150
1929
  }
1151
1930
  handleInitialTree(message) {
1152
1931
  this.moduleName = message.module;
@@ -1171,15 +1950,25 @@ class RemoteEngine {
1171
1950
  if (!this.options.autoReconnect) {
1172
1951
  return;
1173
1952
  }
1174
- if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
1175
- console.error("Max reconnection attempts reached");
1176
- return;
1177
- }
1178
- this.reconnectAttempts++;
1179
- console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`);
1180
- this.reconnectTimer = setTimeout(() => {
1181
- this.connect().catch((error) => {
1182
- console.error("Reconnection failed:", error);
1953
+ this.reconnectDisposable = disposableTimeout(() => {
1954
+ this.reconnectDisposable = null;
1955
+ retry(async () => {
1956
+ const result = await this.connect();
1957
+ if (!result.ok) {
1958
+ throw result.error;
1959
+ }
1960
+ }, {
1961
+ maxAttempts: this.options.maxReconnectAttempts,
1962
+ delayMs: this.options.reconnectInterval,
1963
+ backoff: "exponential",
1964
+ maxDelayMs: 30000,
1965
+ jitter: 0.1,
1966
+ onRetry: (attempt, error) => {
1967
+ console.log(`Reconnection attempt ${attempt}/${this.options.maxReconnectAttempts} failed: ${error.message}`);
1968
+ }
1969
+ }).catch((error) => {
1970
+ console.error("Max reconnection attempts reached:", error.message);
1971
+ this.errorCallbacks.forEach((cb) => cb(new ConnectionError(this.url, error, this.options.maxReconnectAttempts)));
1183
1972
  });
1184
1973
  }, this.options.reconnectInterval);
1185
1974
  }
@@ -1326,9 +2115,9 @@ class ComponentResolver {
1326
2115
  static parseImports(text) {
1327
2116
  const imports = [];
1328
2117
  const importRegex = /import\s+(?:(\{[^}]*\})|(\w+))\s+from\s+["']([^"']+)["']/g;
1329
- let match;
1330
- while ((match = importRegex.exec(text)) !== null) {
1331
- const [, namedImports, defaultImport, source] = match;
2118
+ let match2;
2119
+ while ((match2 = importRegex.exec(text)) !== null) {
2120
+ const [, namedImports, defaultImport, source] = match2;
1332
2121
  if (!source)
1333
2122
  continue;
1334
2123
  let clause;
@@ -1355,19 +2144,19 @@ function removeImports(text) {
1355
2144
  }
1356
2145
  async function discoverComponents(baseDir, options = {}) {
1357
2146
  const {
1358
- patterns = ["folder", "sibling", "index"],
2147
+ patterns = ["single-file", "folder", "sibling", "index"],
1359
2148
  recursive = false,
1360
2149
  debug = false
1361
2150
  } = options;
1362
- const log = debug ? (...args) => console.log("[discovery]", ...args) : () => {};
2151
+ const log3 = debug ? (...args) => console.log("[discovery]", ...args) : () => {};
1363
2152
  const resolvedDir = resolve(baseDir);
1364
2153
  const components = [];
1365
2154
  const seen = new Set;
1366
- log("Scanning directory:", resolvedDir);
1367
- log("Patterns:", patterns);
1368
- const addComponent = (name, hypenPath, modulePath) => {
2155
+ log3("Scanning directory:", resolvedDir);
2156
+ log3("Patterns:", patterns);
2157
+ const addTwoFileComponent = (name, hypenPath, modulePath) => {
1369
2158
  if (seen.has(name)) {
1370
- log(`Skipping duplicate: ${name}`);
2159
+ log3(`Skipping duplicate: ${name}`);
1371
2160
  return;
1372
2161
  }
1373
2162
  seen.add(name);
@@ -1378,9 +2167,26 @@ async function discoverComponents(baseDir, options = {}) {
1378
2167
  hypenPath,
1379
2168
  modulePath,
1380
2169
  template,
1381
- hasModule: modulePath !== null
2170
+ hasModule: modulePath !== null,
2171
+ isSingleFile: false
1382
2172
  });
1383
- log(`Found: ${name} (${modulePath ? "with module" : "stateless"})`);
2173
+ log3(`Found: ${name} (two-file, ${modulePath ? "with module" : "stateless"})`);
2174
+ };
2175
+ const addSingleFileComponent = (name, modulePath, template) => {
2176
+ if (seen.has(name)) {
2177
+ log3(`Skipping duplicate: ${name}`);
2178
+ return;
2179
+ }
2180
+ seen.add(name);
2181
+ components.push({
2182
+ name,
2183
+ hypenPath: null,
2184
+ modulePath,
2185
+ template,
2186
+ hasModule: true,
2187
+ isSingleFile: true
2188
+ });
2189
+ log3(`Found: ${name} (single-file with inline template)`);
1384
2190
  };
1385
2191
  const scanForFolderComponents = (dir) => {
1386
2192
  if (!existsSync2(dir))
@@ -1395,7 +2201,7 @@ async function discoverComponents(baseDir, options = {}) {
1395
2201
  const hypenPath = join2(folderPath, "component.hypen");
1396
2202
  if (existsSync2(hypenPath)) {
1397
2203
  const modulePath = join2(folderPath, "component.ts");
1398
- addComponent(componentName, hypenPath, existsSync2(modulePath) ? modulePath : null);
2204
+ addTwoFileComponent(componentName, hypenPath, existsSync2(modulePath) ? modulePath : null);
1399
2205
  continue;
1400
2206
  }
1401
2207
  }
@@ -1403,7 +2209,7 @@ async function discoverComponents(baseDir, options = {}) {
1403
2209
  const hypenPath = join2(folderPath, "index.hypen");
1404
2210
  if (existsSync2(hypenPath)) {
1405
2211
  const modulePath = join2(folderPath, "index.ts");
1406
- addComponent(componentName, hypenPath, existsSync2(modulePath) ? modulePath : null);
2212
+ addTwoFileComponent(componentName, hypenPath, existsSync2(modulePath) ? modulePath : null);
1407
2213
  continue;
1408
2214
  }
1409
2215
  }
@@ -1430,7 +2236,43 @@ async function discoverComponents(baseDir, options = {}) {
1430
2236
  if (baseName === "component" || baseName === "index")
1431
2237
  continue;
1432
2238
  const modulePath = join2(dir, `${baseName}.ts`);
1433
- addComponent(baseName, hypenPath, existsSync2(modulePath) ? modulePath : null);
2239
+ addTwoFileComponent(baseName, hypenPath, existsSync2(modulePath) ? modulePath : null);
2240
+ }
2241
+ };
2242
+ const scanForSingleFileComponents = async (dir) => {
2243
+ if (!existsSync2(dir))
2244
+ return;
2245
+ const entries = readdirSync2(dir, { withFileTypes: true });
2246
+ for (const entry of entries) {
2247
+ if (entry.isDirectory()) {
2248
+ if (recursive) {
2249
+ await scanForSingleFileComponents(join2(dir, entry.name));
2250
+ }
2251
+ continue;
2252
+ }
2253
+ if (!entry.name.endsWith(".ts"))
2254
+ continue;
2255
+ if (entry.name.startsWith(".") || entry.name.includes(".test.") || entry.name.includes(".spec."))
2256
+ continue;
2257
+ const baseName = basename(entry.name, ".ts");
2258
+ if (baseName === "component" || baseName === "index")
2259
+ continue;
2260
+ const hypenPath = join2(dir, `${baseName}.hypen`);
2261
+ if (existsSync2(hypenPath))
2262
+ continue;
2263
+ const modulePath = join2(dir, entry.name);
2264
+ const content = readFileSync2(modulePath, "utf-8");
2265
+ if (content.includes(".ui(") || content.includes(".ui(hypen")) {
2266
+ try {
2267
+ const moduleExport = await import(modulePath);
2268
+ const module = moduleExport.default;
2269
+ if (module && typeof module === "object" && module.template) {
2270
+ addSingleFileComponent(baseName, modulePath, module.template);
2271
+ }
2272
+ } catch (e) {
2273
+ log3(`Failed to import potential single-file component: ${entry.name}`, e);
2274
+ }
2275
+ }
1434
2276
  }
1435
2277
  };
1436
2278
  if (patterns.includes("folder") || patterns.includes("index")) {
@@ -1439,7 +2281,10 @@ async function discoverComponents(baseDir, options = {}) {
1439
2281
  if (patterns.includes("sibling")) {
1440
2282
  scanForSiblingComponents(resolvedDir);
1441
2283
  }
1442
- log(`Discovered ${components.length} components`);
2284
+ if (patterns.includes("single-file")) {
2285
+ await scanForSingleFileComponents(resolvedDir);
2286
+ }
2287
+ log3(`Discovered ${components.length} components`);
1443
2288
  return components;
1444
2289
  }
1445
2290
  async function loadDiscoveredComponents(components) {
@@ -1447,16 +2292,20 @@ async function loadDiscoveredComponents(components) {
1447
2292
  const loaded = new Map;
1448
2293
  for (const component of components) {
1449
2294
  let module;
2295
+ let template = component.template;
1450
2296
  if (component.modulePath) {
1451
2297
  const moduleExport = await import(component.modulePath);
1452
2298
  module = moduleExport.default;
2299
+ if (component.isSingleFile && module.template) {
2300
+ template = module.template;
2301
+ }
1453
2302
  } else {
1454
2303
  module = app2.defineState({}).build();
1455
2304
  }
1456
2305
  loaded.set(component.name, {
1457
2306
  name: component.name,
1458
2307
  module,
1459
- template: component.template
2308
+ template
1460
2309
  });
1461
2310
  }
1462
2311
  return loaded;
@@ -1471,7 +2320,7 @@ function watchComponents(baseDir, options = {}) {
1471
2320
  debug = false,
1472
2321
  ...discoveryOptions
1473
2322
  } = options;
1474
- const log = debug ? (...args) => console.log("[discovery:watch]", ...args) : () => {};
2323
+ const log3 = debug ? (...args) => console.log("[discovery:watch]", ...args) : () => {};
1475
2324
  let currentComponents = new Map;
1476
2325
  let debounceTimer = null;
1477
2326
  const initialScan = async () => {
@@ -1484,22 +2333,22 @@ function watchComponents(baseDir, options = {}) {
1484
2333
  clearTimeout(debounceTimer);
1485
2334
  }
1486
2335
  debounceTimer = setTimeout(async () => {
1487
- log("Rescanning...");
2336
+ log3("Rescanning...");
1488
2337
  const newComponents = await discoverComponents(resolvedDir, discoveryOptions);
1489
2338
  const newMap = new Map(newComponents.map((c) => [c.name, c]));
1490
2339
  for (const [name, component] of newMap) {
1491
2340
  const existing = currentComponents.get(name);
1492
2341
  if (!existing) {
1493
- log("Added:", name);
2342
+ log3("Added:", name);
1494
2343
  onAdd?.(component);
1495
2344
  } else if (existing.template !== component.template || existing.modulePath !== component.modulePath) {
1496
- log("Updated:", name);
2345
+ log3("Updated:", name);
1497
2346
  onUpdate?.(component);
1498
2347
  }
1499
2348
  }
1500
2349
  for (const name of currentComponents.keys()) {
1501
2350
  if (!newMap.has(name)) {
1502
- log("Removed:", name);
2351
+ log3("Removed:", name);
1503
2352
  onRemove?.(name);
1504
2353
  }
1505
2354
  }
@@ -1511,7 +2360,7 @@ function watchComponents(baseDir, options = {}) {
1511
2360
  if (!filename)
1512
2361
  return;
1513
2362
  if (filename.endsWith(".hypen") || filename.endsWith(".ts")) {
1514
- log("File changed:", filename);
2363
+ log3("File changed:", filename);
1515
2364
  rescan();
1516
2365
  }
1517
2366
  });
@@ -1546,8 +2395,15 @@ import { app } from "@hypen-space/core";
1546
2395
 
1547
2396
  `;
1548
2397
  for (const component of components) {
1549
- const templateJson = JSON.stringify(component.template);
1550
- if (component.hasModule) {
2398
+ if (component.isSingleFile) {
2399
+ code += `export const ${component.name} = {
2400
+ module: ${component.name}Module,
2401
+ template: ${component.name}Module.template,
2402
+ };
2403
+
2404
+ `;
2405
+ } else if (component.hasModule) {
2406
+ const templateJson = JSON.stringify(component.template);
1551
2407
  code += `export const ${component.name} = {
1552
2408
  module: ${component.name}Module,
1553
2409
  template: ${templateJson},
@@ -1555,6 +2411,7 @@ import { app } from "@hypen-space/core";
1555
2411
 
1556
2412
  `;
1557
2413
  } else {
2414
+ const templateJson = JSON.stringify(component.template);
1558
2415
  code += `const ${component.name}Module = app.defineState({}).build();
1559
2416
  export const ${component.name} = {
1560
2417
  module: ${component.name}Module,
@@ -1608,9 +2465,9 @@ function getComponentName(hypenPath) {
1608
2465
  function parseImports(text) {
1609
2466
  const imports = [];
1610
2467
  const importRegex = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+["']([^"']+)["']/g;
1611
- let match;
1612
- while ((match = importRegex.exec(text)) !== null) {
1613
- const [, namedImports, defaultImport, source] = match;
2468
+ let match2;
2469
+ while ((match2 = importRegex.exec(text)) !== null) {
2470
+ const [, namedImports, defaultImport, source] = match2;
1614
2471
  if (!source)
1615
2472
  continue;
1616
2473
  let names;
@@ -1630,23 +2487,23 @@ function removeImports2(text) {
1630
2487
  }
1631
2488
  function hypenPlugin(options = {}) {
1632
2489
  const { debug = false, patterns = ["sibling", "component", "index"] } = options;
1633
- const log = debug ? (...args) => console.log("[hypen-plugin]", ...args) : () => {};
2490
+ const log3 = debug ? (...args) => console.log("[hypen-plugin]", ...args) : () => {};
1634
2491
  return {
1635
2492
  name: "hypen-loader",
1636
2493
  async setup(build) {
1637
2494
  build.onLoad({ filter: /\.hypen$/ }, async (args) => {
1638
2495
  const hypenPath = resolve2(args.path);
1639
- log("Loading:", hypenPath);
2496
+ log3("Loading:", hypenPath);
1640
2497
  const templateRaw = readFileSync3(hypenPath, "utf-8");
1641
2498
  const imports = parseImports(templateRaw);
1642
2499
  const template = removeImports2(templateRaw).trim();
1643
2500
  if (imports.length > 0) {
1644
- log("Found imports:", imports);
2501
+ log3("Found imports:", imports);
1645
2502
  }
1646
2503
  const componentName = getComponentName(hypenPath);
1647
- log("Component name:", componentName);
2504
+ log3("Component name:", componentName);
1648
2505
  const modulePath = findModulePath(hypenPath, patterns);
1649
- log("Module path:", modulePath);
2506
+ log3("Module path:", modulePath);
1650
2507
  let contents;
1651
2508
  if (modulePath) {
1652
2509
  const relativeModulePath = modulePath.replace(/\.ts$/, ".js");
@@ -1658,7 +2515,7 @@ export const name = ${JSON.stringify(componentName)};
1658
2515
  export default { module: _module, template: ${JSON.stringify(template)}, name: ${JSON.stringify(componentName)} };
1659
2516
  `;
1660
2517
  } else {
1661
- log("No module found, creating stateless component");
2518
+ log3("No module found, creating stateless component");
1662
2519
  contents = `
1663
2520
  import { app } from "@hypen/core";
1664
2521
  const _module = app.defineState({}).build();
@@ -1760,36 +2617,645 @@ var Link = app.defineState({
1760
2617
 
1761
2618
  // src/index.ts
1762
2619
  init_app();
2620
+
2621
+ // src/hypen.ts
2622
+ function createBindingProxy(root) {
2623
+ const handler = {
2624
+ get(_, prop) {
2625
+ if (prop === Symbol.toPrimitive || prop === "toString" || prop === "valueOf") {
2626
+ return () => `\${${root}}`;
2627
+ }
2628
+ if (typeof prop === "symbol") {
2629
+ return;
2630
+ }
2631
+ if (prop === "toJSON") {
2632
+ return () => `\${${root}}`;
2633
+ }
2634
+ return createBindingProxy(`${root}.${prop}`);
2635
+ },
2636
+ has() {
2637
+ return true;
2638
+ },
2639
+ ownKeys() {
2640
+ return [];
2641
+ },
2642
+ getOwnPropertyDescriptor() {
2643
+ return {
2644
+ configurable: true,
2645
+ enumerable: true
2646
+ };
2647
+ }
2648
+ };
2649
+ return new Proxy({}, handler);
2650
+ }
2651
+ var state = createBindingProxy("state");
2652
+ var item = createBindingProxy("item");
2653
+ var index = {
2654
+ [Symbol.toPrimitive]: () => "${index}",
2655
+ toString: () => "${index}",
2656
+ valueOf: () => "${index}",
2657
+ toJSON: () => "${index}"
2658
+ };
2659
+ function hypen(strings, ...expressions) {
2660
+ let result = strings[0];
2661
+ for (let i = 0;i < expressions.length; i++) {
2662
+ const expr = expressions[i];
2663
+ result += String(expr);
2664
+ result += strings[i + 1];
2665
+ }
2666
+ return result.trim();
2667
+ }
2668
+
2669
+ // src/index.ts
2670
+ init_state();
2671
+
2672
+ // src/remote/server.ts
2673
+ init_app();
2674
+
2675
+ // src/remote/session.ts
2676
+ class SessionManager {
2677
+ activeSessions = new Map;
2678
+ pendingSessions = new Map;
2679
+ sessionConnections = new Map;
2680
+ config;
2681
+ constructor(config2 = {}) {
2682
+ this.config = {
2683
+ ttl: config2.ttl ?? 3600,
2684
+ concurrent: config2.concurrent ?? "kick-old",
2685
+ generateId: config2.generateId ?? (() => crypto.randomUUID())
2686
+ };
2687
+ }
2688
+ getTtl() {
2689
+ return this.config.ttl;
2690
+ }
2691
+ getConcurrentPolicy() {
2692
+ return this.config.concurrent;
2693
+ }
2694
+ createSession(props) {
2695
+ const now = new Date;
2696
+ const session = {
2697
+ id: this.config.generateId(),
2698
+ ttl: this.config.ttl,
2699
+ createdAt: now,
2700
+ lastConnectedAt: now,
2701
+ props
2702
+ };
2703
+ this.activeSessions.set(session.id, session);
2704
+ return session;
2705
+ }
2706
+ getActiveSession(id) {
2707
+ return this.activeSessions.get(id) ?? null;
2708
+ }
2709
+ getPendingSession(id) {
2710
+ return this.pendingSessions.get(id) ?? null;
2711
+ }
2712
+ hasSession(id) {
2713
+ return this.activeSessions.has(id) || this.pendingSessions.has(id);
2714
+ }
2715
+ suspendSession(sessionId, savedState, onExpire) {
2716
+ const session = this.activeSessions.get(sessionId);
2717
+ if (!session)
2718
+ return;
2719
+ this.activeSessions.delete(sessionId);
2720
+ const expiryTimer = setTimeout(async () => {
2721
+ const pending = this.pendingSessions.get(sessionId);
2722
+ if (pending) {
2723
+ this.pendingSessions.delete(sessionId);
2724
+ await onExpire(pending.session);
2725
+ }
2726
+ }, session.ttl * 1000);
2727
+ this.pendingSessions.set(sessionId, {
2728
+ session,
2729
+ savedState,
2730
+ expiryTimer
2731
+ });
2732
+ }
2733
+ resumeSession(sessionId) {
2734
+ const pending = this.pendingSessions.get(sessionId);
2735
+ if (!pending)
2736
+ return null;
2737
+ clearTimeout(pending.expiryTimer);
2738
+ this.pendingSessions.delete(sessionId);
2739
+ pending.session.lastConnectedAt = new Date;
2740
+ this.activeSessions.set(sessionId, pending.session);
2741
+ return {
2742
+ session: pending.session,
2743
+ savedState: pending.savedState
2744
+ };
2745
+ }
2746
+ destroySession(sessionId) {
2747
+ this.activeSessions.delete(sessionId);
2748
+ const pending = this.pendingSessions.get(sessionId);
2749
+ if (pending) {
2750
+ clearTimeout(pending.expiryTimer);
2751
+ this.pendingSessions.delete(sessionId);
2752
+ }
2753
+ this.sessionConnections.delete(sessionId);
2754
+ }
2755
+ trackConnection(sessionId, ws) {
2756
+ let connections = this.sessionConnections.get(sessionId);
2757
+ if (!connections) {
2758
+ connections = new Set;
2759
+ this.sessionConnections.set(sessionId, connections);
2760
+ }
2761
+ connections.add(ws);
2762
+ }
2763
+ untrackConnection(sessionId, ws) {
2764
+ const connections = this.sessionConnections.get(sessionId);
2765
+ if (connections) {
2766
+ connections.delete(ws);
2767
+ if (connections.size === 0) {
2768
+ this.sessionConnections.delete(sessionId);
2769
+ }
2770
+ }
2771
+ }
2772
+ getConnections(sessionId) {
2773
+ return this.sessionConnections.get(sessionId);
2774
+ }
2775
+ getConnectionCount(sessionId) {
2776
+ return this.sessionConnections.get(sessionId)?.size ?? 0;
2777
+ }
2778
+ getStats() {
2779
+ let totalConnections = 0;
2780
+ for (const connections of this.sessionConnections.values()) {
2781
+ totalConnections += connections.size;
2782
+ }
2783
+ return {
2784
+ activeSessions: this.activeSessions.size,
2785
+ pendingSessions: this.pendingSessions.size,
2786
+ totalConnections
2787
+ };
2788
+ }
2789
+ destroy() {
2790
+ for (const pending of this.pendingSessions.values()) {
2791
+ clearTimeout(pending.expiryTimer);
2792
+ }
2793
+ this.activeSessions.clear();
2794
+ this.pendingSessions.clear();
2795
+ this.sessionConnections.clear();
2796
+ }
2797
+ }
2798
+
2799
+ // src/remote/server.ts
2800
+ class RemoteServer {
2801
+ _module = null;
2802
+ _moduleName = "App";
2803
+ _ui = "";
2804
+ _config = {};
2805
+ _sessionConfig = {};
2806
+ _onConnectionCallbacks = [];
2807
+ _onDisconnectionCallbacks = [];
2808
+ clients = new Map;
2809
+ nextClientId = 1;
2810
+ server = null;
2811
+ sessionManager = null;
2812
+ module(name, module) {
2813
+ this._moduleName = name;
2814
+ this._module = module;
2815
+ return this;
2816
+ }
2817
+ ui(dsl) {
2818
+ this._ui = dsl;
2819
+ return this;
2820
+ }
2821
+ config(config2) {
2822
+ this._config = { ...this._config, ...config2 };
2823
+ return this;
2824
+ }
2825
+ session(config2) {
2826
+ this._sessionConfig = config2;
2827
+ return this;
2828
+ }
2829
+ onConnection(callback) {
2830
+ this._onConnectionCallbacks.push(callback);
2831
+ return this;
2832
+ }
2833
+ onDisconnection(callback) {
2834
+ this._onDisconnectionCallbacks.push(callback);
2835
+ return this;
2836
+ }
2837
+ listen(port) {
2838
+ if (!this._module) {
2839
+ throw new Error("Module not set. Call .module() before .listen()");
2840
+ }
2841
+ if (!this._ui) {
2842
+ throw new Error("UI not set. Call .ui() before .listen()");
2843
+ }
2844
+ this.sessionManager = new SessionManager(this._sessionConfig);
2845
+ const finalPort = port ?? this._config.port ?? 3000;
2846
+ const hostname = this._config.hostname ?? "0.0.0.0";
2847
+ this.server = Bun.serve({
2848
+ port: finalPort,
2849
+ hostname,
2850
+ websocket: {
2851
+ open: (ws) => this.handleOpen(ws),
2852
+ message: (ws, message) => this.handleMessage(ws, message),
2853
+ close: (ws) => this.handleClose(ws)
2854
+ },
2855
+ fetch: (req, server) => {
2856
+ const url = new URL(req.url);
2857
+ if (server.upgrade(req, { data: undefined })) {
2858
+ return;
2859
+ }
2860
+ if (url.pathname === "/health") {
2861
+ return new Response("OK", { status: 200 });
2862
+ }
2863
+ if (url.pathname === "/stats") {
2864
+ const stats = this.sessionManager?.getStats() ?? {
2865
+ activeSessions: 0,
2866
+ pendingSessions: 0,
2867
+ totalConnections: 0
2868
+ };
2869
+ return new Response(JSON.stringify(stats), {
2870
+ headers: { "Content-Type": "application/json" }
2871
+ });
2872
+ }
2873
+ return new Response("Hypen Remote Server", { status: 200 });
2874
+ }
2875
+ });
2876
+ console.log(`\uD83D\uDE80 Hypen app streaming on ws://${hostname}:${finalPort}`);
2877
+ return this;
2878
+ }
2879
+ stop() {
2880
+ if (this.server) {
2881
+ this.server.stop();
2882
+ this.server = null;
2883
+ }
2884
+ if (this.sessionManager) {
2885
+ this.sessionManager.destroy();
2886
+ this.sessionManager = null;
2887
+ }
2888
+ }
2889
+ get url() {
2890
+ if (!this.server)
2891
+ return null;
2892
+ const hostname = this._config.hostname ?? "localhost";
2893
+ const port = this._config.port ?? 3000;
2894
+ return `ws://${hostname}:${port}`;
2895
+ }
2896
+ async handleOpen(ws) {
2897
+ try {
2898
+ const clientId = `client_${this.nextClientId++}`;
2899
+ const connectedAt = new Date;
2900
+ const engine = new Engine;
2901
+ await engine.init();
2902
+ const moduleInstance = new HypenModuleInstance(engine, this._module);
2903
+ const clientData = {
2904
+ id: clientId,
2905
+ engine,
2906
+ moduleInstance,
2907
+ revision: 0,
2908
+ connectedAt,
2909
+ sessionId: "",
2910
+ helloReceived: false
2911
+ };
2912
+ this.clients.set(ws, clientData);
2913
+ clientData.helloTimeout = setTimeout(() => {
2914
+ if (!clientData.helloReceived) {
2915
+ this.initializeSession(ws, clientData, undefined, undefined);
2916
+ }
2917
+ }, 1000);
2918
+ } catch (error) {
2919
+ console.error("Error handling WebSocket open:", error);
2920
+ ws.close(1011, "Internal server error");
2921
+ }
2922
+ }
2923
+ async initializeSession(ws, clientData, requestedSessionId, props) {
2924
+ if (clientData.helloReceived)
2925
+ return;
2926
+ clientData.helloReceived = true;
2927
+ if (clientData.helloTimeout) {
2928
+ clearTimeout(clientData.helloTimeout);
2929
+ clientData.helloTimeout = undefined;
2930
+ }
2931
+ let session;
2932
+ let isNew = true;
2933
+ let isRestored = false;
2934
+ let restoredState = null;
2935
+ if (requestedSessionId && this.sessionManager) {
2936
+ const resumed = this.sessionManager.resumeSession(requestedSessionId);
2937
+ if (resumed) {
2938
+ session = resumed.session;
2939
+ restoredState = resumed.savedState;
2940
+ isNew = false;
2941
+ isRestored = true;
2942
+ await this.triggerReconnect(clientData, session, restoredState);
2943
+ } else {
2944
+ const activeSession = this.sessionManager.getActiveSession(requestedSessionId);
2945
+ if (activeSession) {
2946
+ const handled = await this.handleConcurrentConnection(ws, clientData, activeSession, props);
2947
+ if (!handled)
2948
+ return;
2949
+ session = activeSession;
2950
+ isNew = false;
2951
+ } else {
2952
+ session = this.sessionManager.createSession(props);
2953
+ }
2954
+ }
2955
+ } else if (this.sessionManager) {
2956
+ session = this.sessionManager.createSession(props);
2957
+ } else {
2958
+ session = {
2959
+ id: crypto.randomUUID(),
2960
+ ttl: 3600,
2961
+ createdAt: new Date,
2962
+ lastConnectedAt: new Date,
2963
+ props
2964
+ };
2965
+ }
2966
+ clientData.sessionId = session.id;
2967
+ this.sessionManager?.trackConnection(session.id, ws);
2968
+ const sessionAck = {
2969
+ type: "sessionAck",
2970
+ sessionId: session.id,
2971
+ isNew,
2972
+ isRestored
2973
+ };
2974
+ ws.send(JSON.stringify(sessionAck));
2975
+ this.setupRenderCallback(ws, clientData);
2976
+ const initialPatches = [];
2977
+ clientData.engine.setRenderCallback((patches) => {
2978
+ initialPatches.push(...patches);
2979
+ });
2980
+ clientData.engine.renderSource(this._ui);
2981
+ this.setupRenderCallback(ws, clientData);
2982
+ const initialMessage = {
2983
+ type: "initialTree",
2984
+ module: this._moduleName,
2985
+ state: clientData.moduleInstance.getState(),
2986
+ patches: initialPatches,
2987
+ revision: 0
2988
+ };
2989
+ ws.send(JSON.stringify(initialMessage));
2990
+ const client = {
2991
+ id: clientData.id,
2992
+ socket: ws,
2993
+ connectedAt: clientData.connectedAt
2994
+ };
2995
+ this._onConnectionCallbacks.forEach((cb) => cb(client));
2996
+ }
2997
+ setupRenderCallback(ws, clientData) {
2998
+ clientData.engine.setRenderCallback((patches) => {
2999
+ const data = this.clients.get(ws);
3000
+ if (!data)
3001
+ return;
3002
+ data.revision++;
3003
+ const message = {
3004
+ type: "patch",
3005
+ module: this._moduleName,
3006
+ patches,
3007
+ revision: data.revision
3008
+ };
3009
+ ws.send(JSON.stringify(message));
3010
+ if (this.sessionManager?.getConcurrentPolicy() === "allow-multiple" && data.sessionId) {
3011
+ const connections = this.sessionManager.getConnections(data.sessionId);
3012
+ if (connections) {
3013
+ for (const conn of connections) {
3014
+ if (conn !== ws) {
3015
+ conn.send(JSON.stringify(message));
3016
+ }
3017
+ }
3018
+ }
3019
+ }
3020
+ });
3021
+ }
3022
+ async handleConcurrentConnection(ws, clientData, existingSession, props) {
3023
+ const policy = this.sessionManager?.getConcurrentPolicy() ?? "kick-old";
3024
+ switch (policy) {
3025
+ case "kick-old": {
3026
+ const existingConnections = this.sessionManager?.getConnections(existingSession.id);
3027
+ if (existingConnections) {
3028
+ for (const conn of existingConnections) {
3029
+ const oldWs = conn;
3030
+ const expiredMsg = {
3031
+ type: "sessionExpired",
3032
+ sessionId: existingSession.id,
3033
+ reason: "kicked"
3034
+ };
3035
+ oldWs.send(JSON.stringify(expiredMsg));
3036
+ oldWs.close(1000, "Session taken by new connection");
3037
+ }
3038
+ }
3039
+ return true;
3040
+ }
3041
+ case "reject-new": {
3042
+ const expiredMsg = {
3043
+ type: "sessionExpired",
3044
+ sessionId: existingSession.id,
3045
+ reason: "kicked"
3046
+ };
3047
+ ws.send(JSON.stringify(expiredMsg));
3048
+ ws.close(1000, "Session already active");
3049
+ return false;
3050
+ }
3051
+ case "allow-multiple": {
3052
+ return true;
3053
+ }
3054
+ default:
3055
+ return true;
3056
+ }
3057
+ }
3058
+ async triggerReconnect(clientData, session, savedState) {
3059
+ const handler = this._module?.handlers.onReconnect;
3060
+ if (!handler)
3061
+ return;
3062
+ let restored = false;
3063
+ const restore = (state2) => {
3064
+ restored = true;
3065
+ clientData.moduleInstance.updateState(state2);
3066
+ };
3067
+ await handler({ session, restore });
3068
+ }
3069
+ handleMessage(ws, message) {
3070
+ try {
3071
+ const clientData = this.clients.get(ws);
3072
+ if (!clientData)
3073
+ return;
3074
+ const msg = JSON.parse(message.toString());
3075
+ switch (msg.type) {
3076
+ case "hello": {
3077
+ const helloMsg = msg;
3078
+ this.initializeSession(ws, clientData, helloMsg.sessionId, helloMsg.props);
3079
+ break;
3080
+ }
3081
+ case "dispatchAction": {
3082
+ const actionMsg = msg;
3083
+ clientData.engine.dispatchAction(actionMsg.action, actionMsg.payload);
3084
+ break;
3085
+ }
3086
+ default:
3087
+ break;
3088
+ }
3089
+ } catch (error) {
3090
+ console.error("Error handling WebSocket message:", error);
3091
+ }
3092
+ }
3093
+ async handleClose(ws) {
3094
+ const clientData = this.clients.get(ws);
3095
+ if (!clientData)
3096
+ return;
3097
+ if (clientData.helloTimeout) {
3098
+ clearTimeout(clientData.helloTimeout);
3099
+ }
3100
+ const currentState = clientData.moduleInstance.getState();
3101
+ if (clientData.sessionId && this._module?.handlers.onDisconnect) {
3102
+ const session = this.sessionManager?.getActiveSession(clientData.sessionId);
3103
+ if (session) {
3104
+ await this._module.handlers.onDisconnect({
3105
+ state: currentState,
3106
+ session
3107
+ });
3108
+ }
3109
+ }
3110
+ if (clientData.sessionId && this.sessionManager) {
3111
+ this.sessionManager.untrackConnection(clientData.sessionId, ws);
3112
+ if (this.sessionManager.getConnectionCount(clientData.sessionId) === 0) {
3113
+ const session = this.sessionManager.getActiveSession(clientData.sessionId);
3114
+ if (session) {
3115
+ this.sessionManager.suspendSession(clientData.sessionId, currentState, async (expiredSession) => {
3116
+ if (this._module?.handlers.onExpire) {
3117
+ await this._module.handlers.onExpire({ session: expiredSession });
3118
+ }
3119
+ });
3120
+ }
3121
+ }
3122
+ }
3123
+ await clientData.moduleInstance.destroy();
3124
+ this.clients.delete(ws);
3125
+ const client = {
3126
+ id: clientData.id,
3127
+ socket: ws,
3128
+ connectedAt: clientData.connectedAt
3129
+ };
3130
+ this._onDisconnectionCallbacks.forEach((cb) => cb(client));
3131
+ }
3132
+ getClientCount() {
3133
+ return this.clients.size;
3134
+ }
3135
+ getSessionStats() {
3136
+ return this.sessionManager?.getStats() ?? {
3137
+ activeSessions: 0,
3138
+ pendingSessions: 0,
3139
+ totalConnections: 0
3140
+ };
3141
+ }
3142
+ broadcast(message) {
3143
+ const json = JSON.stringify(message);
3144
+ this.clients.forEach((_, ws) => {
3145
+ ws.send(json);
3146
+ });
3147
+ }
3148
+ }
3149
+ function serve(options) {
3150
+ const server = new RemoteServer().module(options.moduleName ?? "App", options.module).ui(options.ui);
3151
+ if (options.port || options.hostname) {
3152
+ server.config({
3153
+ port: options.port,
3154
+ hostname: options.hostname
3155
+ });
3156
+ }
3157
+ if (options.session) {
3158
+ server.session(options.session);
3159
+ }
3160
+ if (options.onConnection) {
3161
+ server.onConnection(options.onConnection);
3162
+ }
3163
+ if (options.onDisconnection) {
3164
+ server.onDisconnection(options.onDisconnection);
3165
+ }
3166
+ return server.listen(options.port);
3167
+ }
3168
+
3169
+ // src/index.ts
3170
+ init_result();
3171
+ init_logger();
1763
3172
  export {
3173
+ withRetry,
1764
3174
  watchComponents,
3175
+ usingSync,
3176
+ using,
3177
+ unwrapProxy,
3178
+ unwrapOrElse,
3179
+ unwrapOr,
3180
+ unwrap,
3181
+ state,
3182
+ setLogLevel,
3183
+ serve,
3184
+ retryResult,
3185
+ retry,
1765
3186
  registerHypenPlugin,
3187
+ match,
3188
+ mapErr,
3189
+ map,
3190
+ logger,
3191
+ log,
1766
3192
  loadDiscoveredComponents,
3193
+ item,
3194
+ isStateProxy,
3195
+ isOk,
3196
+ isErr,
3197
+ isDisposable,
3198
+ index,
1767
3199
  hypenPlugin,
3200
+ hypen,
3201
+ hasElementDisposables,
1768
3202
  getStateSnapshot,
3203
+ getLogLevel,
3204
+ getElementDisposables,
1769
3205
  generateComponentsCode,
3206
+ fromTry,
3207
+ fromPromise,
3208
+ frameworkLoggers,
3209
+ flatMap,
3210
+ enableLogging,
3211
+ disposeElement,
3212
+ disposableWebSocket,
3213
+ disposableTimeout,
3214
+ disposableSubscription,
3215
+ disposableListener,
3216
+ disposableInterval,
3217
+ disposableAbortController,
1770
3218
  discoverComponents,
3219
+ disableLogging,
1771
3220
  defaultHypenPlugin,
1772
3221
  createObservableState,
3222
+ createLogger,
1773
3223
  createEventEmitter,
3224
+ configureLogger,
3225
+ compositeDisposable,
1774
3226
  componentLoader,
1775
3227
  batchStateUpdates,
1776
3228
  app,
3229
+ all,
1777
3230
  TypedEventEmitter,
3231
+ StateError,
3232
+ SessionManager,
1778
3233
  Router,
1779
3234
  Route,
3235
+ RetryPresets,
3236
+ RetryConditions,
3237
+ RemoteServer,
1780
3238
  RemoteEngine,
3239
+ Ok,
3240
+ Logger,
1781
3241
  Link,
1782
3242
  HypenRouter,
1783
3243
  HypenModuleInstance,
1784
3244
  HypenGlobalContext,
3245
+ HypenError,
1785
3246
  HypenAppBuilder,
1786
3247
  HypenApp,
3248
+ Err,
1787
3249
  Engine,
3250
+ DisposableStack,
3251
+ DisposableMixin,
1788
3252
  ConsoleRenderer,
3253
+ ConnectionError,
1789
3254
  ComponentResolver,
1790
3255
  ComponentLoader,
1791
3256
  Engine2 as BrowserEngine,
1792
- BaseRenderer
3257
+ BaseRenderer,
3258
+ ActionError
1793
3259
  };
1794
3260
 
1795
- //# debugId=792BD9A476DA875964756E2164756E21
3261
+ //# debugId=7419746F35A290F664756E2164756E21