@hypen-space/core 0.4.980 → 0.5.2

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 (96) hide show
  1. package/README.md +5 -8
  2. package/dist/app.js +13 -1168
  3. package/dist/app.js.map +3 -9
  4. package/dist/chunk-00zt3w6e.js +58 -0
  5. package/dist/chunk-00zt3w6e.js.map +10 -0
  6. package/dist/chunk-3zvyth50.js +87 -0
  7. package/dist/chunk-3zvyth50.js.map +10 -0
  8. package/dist/chunk-5gtx3pza.js +11 -0
  9. package/dist/chunk-5gtx3pza.js.map +9 -0
  10. package/dist/chunk-7222131a.js +191 -0
  11. package/dist/chunk-7222131a.js.map +10 -0
  12. package/dist/chunk-avchpwhf.js +200 -0
  13. package/dist/chunk-avchpwhf.js.map +10 -0
  14. package/dist/chunk-ec0tgy9a.js +154 -0
  15. package/dist/chunk-ec0tgy9a.js.map +10 -0
  16. package/dist/chunk-fnss4dm2.js +479 -0
  17. package/dist/chunk-fnss4dm2.js.map +10 -0
  18. package/dist/chunk-g2gz9pgg.js +133 -0
  19. package/dist/chunk-g2gz9pgg.js.map +10 -0
  20. package/dist/chunk-g72v6mmf.js +341 -0
  21. package/dist/chunk-g72v6mmf.js.map +10 -0
  22. package/dist/chunk-g7g3jxty.js +351 -0
  23. package/dist/chunk-g7g3jxty.js.map +10 -0
  24. package/dist/chunk-g830re2c.js +50 -0
  25. package/dist/chunk-g830re2c.js.map +10 -0
  26. package/dist/chunk-gtwh1sg7.js +127 -0
  27. package/dist/chunk-gtwh1sg7.js.map +10 -0
  28. package/dist/chunk-k12cwfst.js +72 -0
  29. package/dist/chunk-k12cwfst.js.map +10 -0
  30. package/dist/chunk-mbqqsz6r.js +128 -0
  31. package/dist/chunk-mbqqsz6r.js.map +10 -0
  32. package/dist/chunk-ne632swm.js +209 -0
  33. package/dist/chunk-ne632swm.js.map +10 -0
  34. package/dist/chunk-p3pj1bax.js +206 -0
  35. package/dist/chunk-p3pj1bax.js.map +10 -0
  36. package/dist/chunk-vshdk2pw.js +56 -0
  37. package/dist/chunk-vshdk2pw.js.map +10 -0
  38. package/dist/chunk-wp2ebn2m.js +51 -0
  39. package/dist/chunk-wp2ebn2m.js.map +10 -0
  40. package/dist/chunk-x4qv4jm9.js +88 -0
  41. package/dist/chunk-x4qv4jm9.js.map +10 -0
  42. package/dist/chunk-ycxp4drd.js +211 -0
  43. package/dist/chunk-ycxp4drd.js.map +10 -0
  44. package/dist/components/builtin.js +14 -1246
  45. package/dist/components/builtin.js.map +3 -10
  46. package/dist/context.js +7 -362
  47. package/dist/context.js.map +3 -6
  48. package/dist/datasource.js +5 -76
  49. package/dist/datasource.js.map +3 -4
  50. package/dist/disposable.js +20 -367
  51. package/dist/disposable.js.map +3 -5
  52. package/dist/engine-base.d.ts +38 -2
  53. package/dist/engine-base.js +45 -412
  54. package/dist/engine-base.js.map +4 -6
  55. package/dist/events.js +7 -285
  56. package/dist/events.js.map +3 -5
  57. package/dist/hypen.js +8 -71
  58. package/dist/hypen.js.map +3 -4
  59. package/dist/index.browser.js +58 -2152
  60. package/dist/index.browser.js.map +3 -16
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +132 -3058
  63. package/dist/index.js.map +4 -22
  64. package/dist/logger.js +16 -220
  65. package/dist/logger.js.map +3 -4
  66. package/dist/managed-router.js +12 -1367
  67. package/dist/managed-router.js.map +3 -10
  68. package/dist/portable.js +6 -70
  69. package/dist/portable.js.map +3 -4
  70. package/dist/remote/client.js +9 -1010
  71. package/dist/remote/client.js.map +3 -8
  72. package/dist/remote/index.d.ts +2 -0
  73. package/dist/remote/index.js +511 -1066
  74. package/dist/remote/index.js.map +4 -9
  75. package/dist/remote/remote-session.d.ts +245 -0
  76. package/dist/remote/session.js +5 -147
  77. package/dist/remote/session.js.map +3 -4
  78. package/dist/renderer.js +7 -271
  79. package/dist/renderer.js.map +3 -5
  80. package/dist/resolver.js +5 -484
  81. package/dist/resolver.js.map +3 -5
  82. package/dist/result.js +25 -211
  83. package/dist/result.js.map +3 -4
  84. package/dist/retry.js +10 -336
  85. package/dist/retry.js.map +3 -5
  86. package/dist/router.js +9 -807
  87. package/dist/router.js.map +3 -8
  88. package/dist/state.js +10 -274
  89. package/dist/state.js.map +3 -5
  90. package/dist/validate.d.ts +19 -0
  91. package/package.json +13 -1
  92. package/src/engine-base.ts +87 -4
  93. package/src/index.ts +5 -0
  94. package/src/remote/index.ts +16 -0
  95. package/src/remote/remote-session.ts +998 -0
  96. package/src/validate.ts +37 -0
@@ -1,1139 +1,584 @@
1
- var __defProp = Object.defineProperty;
2
- var __returnValue = (v) => v;
3
- function __exportSetter(name, newValue) {
4
- this[name] = __returnValue.bind(null, newValue);
5
- }
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, {
9
- get: all[name],
10
- enumerable: true,
11
- configurable: true,
12
- set: __exportSetter.bind(all, name)
13
- });
14
- };
15
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
17
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
18
- }) : x)(function(x) {
19
- if (typeof require !== "undefined")
20
- return require.apply(this, arguments);
21
- throw Error('Dynamic require of "' + x + '" is not supported');
22
- });
23
-
24
- // src/result.ts
25
- function Ok(value) {
26
- return { ok: true, value };
27
- }
28
- function Err(error) {
29
- return { ok: false, error };
30
- }
31
- function isOk(result) {
32
- return result.ok;
33
- }
34
- function isErr(result) {
35
- return !result.ok;
36
- }
37
- async function fromPromise(promise, mapError) {
38
- try {
39
- const value = await promise;
40
- return Ok(value);
41
- } catch (e) {
42
- if (mapError) {
43
- return Err(mapError(e));
44
- }
45
- return Err(e);
46
- }
47
- }
48
- function fromTry(fn, mapError) {
49
- try {
50
- return Ok(fn());
51
- } catch (e) {
52
- if (mapError) {
53
- return Err(mapError(e));
54
- }
55
- return Err(e);
56
- }
57
- }
58
- function map(result, fn) {
59
- if (result.ok) {
60
- return Ok(fn(result.value));
61
- }
62
- return result;
63
- }
64
- function mapErr(result, fn) {
65
- if (!result.ok) {
66
- return Err(fn(result.error));
67
- }
68
- return result;
69
- }
70
- function flatMap(result, fn) {
71
- if (result.ok) {
72
- return fn(result.value);
73
- }
74
- return result;
75
- }
76
- function unwrap(result) {
77
- if (result.ok) {
78
- return result.value;
79
- }
80
- throw result.error;
81
- }
82
- function unwrapOr(result, defaultValue) {
83
- if (result.ok) {
84
- return result.value;
85
- }
86
- return defaultValue;
87
- }
88
- function unwrapOrElse(result, fn) {
89
- if (result.ok) {
90
- return result.value;
91
- }
92
- return fn(result.error);
93
- }
94
- function match(result, handlers) {
95
- if (result.ok) {
96
- return handlers.ok(result.value);
97
- }
98
- return handlers.err(result.error);
99
- }
100
- function all(results) {
101
- const values = [];
102
- for (const result of results) {
103
- if (!result.ok) {
104
- return result;
105
- }
106
- values.push(result.value);
107
- }
108
- return Ok(values);
109
- }
110
-
111
- class HypenError extends Error {
112
- code;
113
- context;
114
- cause;
115
- constructor(code, message, options) {
116
- super(message);
117
- this.name = "HypenError";
118
- this.code = code;
119
- this.context = options?.context;
120
- this.cause = options?.cause;
121
- Object.setPrototypeOf(this, new.target.prototype);
122
- }
123
- }
124
-
125
- class ActionError extends HypenError {
126
- actionName;
127
- constructor(actionName, cause) {
128
- super("ACTION_ERROR", `Action handler "${actionName}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
129
- context: { actionName },
130
- cause: cause instanceof Error ? cause : undefined
131
- });
132
- this.name = "ActionError";
133
- this.actionName = actionName;
134
- }
135
- }
136
-
137
- class ConnectionError extends HypenError {
138
- url;
139
- attempt;
140
- constructor(url, cause, attempt) {
141
- super("CONNECTION_ERROR", `Connection to "${url}" failed${attempt ? ` (attempt ${attempt})` : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, {
142
- context: { url, attempt },
143
- cause: cause instanceof Error ? cause : undefined
144
- });
145
- this.name = "ConnectionError";
146
- this.url = url;
147
- this.attempt = attempt;
148
- }
149
- }
1
+ import {
2
+ SessionManager
3
+ } from "../chunk-gtwh1sg7.js";
4
+ import {
5
+ HypenGlobalContext
6
+ } from "../chunk-3zvyth50.js";
7
+ import"../chunk-k12cwfst.js";
8
+ import {
9
+ RemoteEngine
10
+ } from "../chunk-g7g3jxty.js";
11
+ import"../chunk-g2gz9pgg.js";
12
+ import {
13
+ HypenRouter
14
+ } from "../chunk-p3pj1bax.js";
15
+ import"../chunk-ec0tgy9a.js";
16
+ import {
17
+ ManagedRouter
18
+ } from "../chunk-ne632swm.js";
19
+ import {
20
+ HypenApp,
21
+ HypenModuleInstance
22
+ } from "../chunk-fnss4dm2.js";
23
+ import"../chunk-vshdk2pw.js";
24
+ import"../chunk-7222131a.js";
25
+ import"../chunk-ycxp4drd.js";
26
+ import"../chunk-g830re2c.js";
27
+ import {
28
+ frameworkLoggers
29
+ } from "../chunk-avchpwhf.js";
30
+ import"../chunk-5gtx3pza.js";
31
+ // src/remote/remote-session.ts
32
+ var log = frameworkLoggers.remote;
33
+ var nextSessionCounter = 1;
150
34
 
151
- class StateError extends HypenError {
152
- path;
153
- constructor(message, path, cause) {
154
- super("STATE_ERROR", message, {
155
- context: { path },
156
- cause: cause instanceof Error ? cause : undefined
35
+ class RemoteSession {
36
+ id;
37
+ connectedAt;
38
+ host;
39
+ transport;
40
+ socketHandle;
41
+ engine;
42
+ _moduleInstance = null;
43
+ _sessionId = null;
44
+ _helloReceived = false;
45
+ _stateSubscribed = false;
46
+ _revision = 0;
47
+ _destroyed = false;
48
+ helloTimeout = null;
49
+ _autoManagedRouter = null;
50
+ autoRouterEnabled = true;
51
+ ready;
52
+ closed;
53
+ _resolveReady;
54
+ _resolveClosed;
55
+ constructor(host, transport, options = {}) {
56
+ this.host = host;
57
+ this.transport = transport;
58
+ this.id = options.clientId ?? `client_${nextSessionCounter++}`;
59
+ this.connectedAt = new Date;
60
+ this.socketHandle = options.socketHandle;
61
+ this.ready = new Promise((resolve) => {
62
+ this._resolveReady = resolve;
157
63
  });
158
- this.name = "StateError";
159
- this.path = path;
160
- }
161
- }
162
-
163
- class ParseError extends HypenError {
164
- source;
165
- constructor(message, source, cause) {
166
- super("PARSE_ERROR", message, {
167
- context: { source },
168
- cause: cause instanceof Error ? cause : undefined
64
+ this.closed = new Promise((resolve) => {
65
+ this._resolveClosed = resolve;
169
66
  });
170
- this.name = "ParseError";
171
- this.source = source;
172
- }
173
- }
174
-
175
- class RenderError extends HypenError {
176
- constructor(message, cause) {
177
- super("RENDER_ERROR", message, {
178
- cause: cause instanceof Error ? cause : undefined
179
- });
180
- this.name = "RenderError";
181
- }
182
- }
183
- function classifyEngineError(err) {
184
- if (err && typeof err === "object" && "message" in err && "type" in err) {
185
- const structured = err;
186
- switch (structured.type) {
187
- case "parseError":
188
- return new ParseError(structured.message);
189
- case "stateError":
190
- return new StateError(structured.message);
191
- case "renderError":
192
- return new RenderError(structured.message);
193
- case "actionError":
194
- return new HypenError("ACTION_ERROR", structured.message);
195
- default:
196
- return new HypenError("UNKNOWN_ERROR", structured.message);
67
+ this.engine = host.createEngine();
68
+ this.engine.init().catch((err) => log.error("Engine init failed:", err));
69
+ this.setupComponentResolver();
70
+ if (Object.keys(host.resources).length > 0) {
71
+ this.engine.registerResources(host.resources);
72
+ }
73
+ log.info(`Session ${this.id} created, engine initialized`);
74
+ const graceMs = options.helloGraceMs ?? 1000;
75
+ if (graceMs !== null) {
76
+ this.helloTimeout = setTimeout(() => {
77
+ if (!this._helloReceived) {
78
+ this.initializeSession(undefined, undefined).catch((err) => log.error("Error initializing legacy session:", err));
79
+ }
80
+ }, graceMs);
197
81
  }
198
82
  }
199
- const message = err instanceof Error ? err.message : String(err);
200
- if (message.startsWith("Parse error:")) {
201
- return new ParseError(message);
83
+ get sessionId() {
84
+ return this._sessionId;
202
85
  }
203
- if (message.startsWith("Invalid state:") || message.startsWith("Invalid state patch:")) {
204
- return new StateError(message);
86
+ get moduleInstance() {
87
+ return this._moduleInstance;
205
88
  }
206
- if (message.startsWith("Parent node not found:")) {
207
- return new RenderError(message);
208
- }
209
- return new RenderError(message);
210
- }
211
-
212
- // src/logger.ts
213
- var LOG_LEVEL_ORDER = {
214
- debug: 0,
215
- info: 1,
216
- warn: 2,
217
- error: 3,
218
- none: 4
219
- };
220
- var LOG_LEVEL_COLORS = {
221
- debug: "\x1B[36m",
222
- info: "\x1B[32m",
223
- warn: "\x1B[33m",
224
- error: "\x1B[31m"
225
- };
226
- var RESET_COLOR = "\x1B[0m";
227
- function isProduction() {
228
- if (typeof process !== "undefined" && process.env) {
229
- return false;
89
+ get helloReceived() {
90
+ return this._helloReceived;
230
91
  }
231
- return false;
232
- }
233
- var config = {
234
- level: isProduction() ? "error" : "info",
235
- colors: true,
236
- timestamps: false
237
- };
238
- function setLogLevel(level) {
239
- config.level = level;
240
- }
241
- function getLogLevel() {
242
- return config.level;
243
- }
244
- function configureLogger(options) {
245
- config = { ...config, ...options };
246
- }
247
- function enableLogging() {
248
- config.level = "debug";
249
- }
250
- function disableLogging() {
251
- config.level = "none";
252
- }
253
- function setDebugMode(enabled) {
254
- config.level = enabled ? "debug" : "error";
255
- }
256
- function isDebugMode() {
257
- return config.level === "debug";
258
- }
259
- function shouldLog(level) {
260
- return LOG_LEVEL_ORDER[level] >= LOG_LEVEL_ORDER[config.level];
261
- }
262
- function formatTag(tag, level) {
263
- const timestamp = config.timestamps ? `${new Date().toISOString()} ` : "";
264
- if (config.colors && level !== "none") {
265
- const color = LOG_LEVEL_COLORS[level];
266
- return `${timestamp}${color}[${tag}]${RESET_COLOR}`;
92
+ get stateSubscribed() {
93
+ return this._stateSubscribed;
267
94
  }
268
- return `${timestamp}[${tag}]`;
269
- }
270
-
271
- class Logger {
272
- tag;
273
- constructor(tag) {
274
- this.tag = tag;
95
+ get revision() {
96
+ return this._revision;
275
97
  }
276
- debug(...args) {
277
- if (!shouldLog("debug"))
278
- return;
279
- if (config.handler) {
280
- config.handler.debug(this.tag, ...args);
281
- } else {
282
- console.log(formatTag(this.tag, "debug"), ...args);
283
- }
98
+ get isDestroyed() {
99
+ return this._destroyed;
284
100
  }
285
- info(...args) {
286
- if (!shouldLog("info"))
287
- return;
288
- if (config.handler) {
289
- config.handler.info(this.tag, ...args);
290
- } else {
291
- console.info(formatTag(this.tag, "info"), ...args);
292
- }
101
+ toRemoteClient() {
102
+ return {
103
+ id: this.id,
104
+ socket: this.socketHandle,
105
+ connectedAt: this.connectedAt
106
+ };
293
107
  }
294
- warn(...args) {
295
- if (!shouldLog("warn"))
108
+ async receive(raw) {
109
+ if (this._destroyed)
296
110
  return;
297
- if (config.handler) {
298
- config.handler.warn(this.tag, ...args);
299
- } else {
300
- console.warn(formatTag(this.tag, "warn"), ...args);
301
- }
302
- }
303
- error(...args) {
304
- if (!shouldLog("error"))
111
+ let msg;
112
+ try {
113
+ if (typeof raw === "string" || raw instanceof Buffer) {
114
+ msg = JSON.parse(raw.toString());
115
+ } else {
116
+ msg = raw;
117
+ }
118
+ } catch (err) {
119
+ log.error(`Invalid message on session ${this.id}:`, err);
305
120
  return;
306
- if (config.handler) {
307
- config.handler.error(this.tag, ...args);
308
- } else {
309
- console.error(formatTag(this.tag, "error"), ...args);
310
- }
311
- }
312
- time(label, fn) {
313
- if (!shouldLog("debug")) {
314
- return fn();
315
121
  }
316
- const start = performance.now();
317
122
  try {
318
- return fn();
319
- } finally {
320
- const duration = performance.now() - start;
321
- this.debug(`${label}: ${duration.toFixed(2)}ms`);
123
+ switch (msg.type) {
124
+ case "hello": {
125
+ const hello = msg;
126
+ await this.initializeSession(hello.sessionId, hello.props);
127
+ break;
128
+ }
129
+ case "dispatchAction": {
130
+ const action = msg;
131
+ this.engine.dispatchAction(action.action, action.payload);
132
+ if (this.host.syncActions) {
133
+ for (const other of this.host.otherSessions(this)) {
134
+ if (!other._helloReceived)
135
+ continue;
136
+ other.engine.dispatchAction(action.action, action.payload);
137
+ }
138
+ }
139
+ break;
140
+ }
141
+ case "updateState": {
142
+ const stateMsg = msg;
143
+ this._moduleInstance?.updateState(stateMsg.state);
144
+ if (this.host.syncActions) {
145
+ for (const other of this.host.otherSessions(this)) {
146
+ if (!other._helloReceived || !other._moduleInstance)
147
+ continue;
148
+ other._moduleInstance.updateState(stateMsg.state);
149
+ }
150
+ }
151
+ break;
152
+ }
153
+ case "subscribeState": {
154
+ this._stateSubscribed = true;
155
+ log.info(`Session ${this.id} subscribed to state updates`);
156
+ break;
157
+ }
158
+ default:
159
+ break;
160
+ }
161
+ } catch (err) {
162
+ log.error(`Error handling message on session ${this.id}:`, err);
322
163
  }
323
164
  }
324
- async timeAsync(label, fn) {
325
- if (!shouldLog("debug")) {
326
- return fn();
327
- }
328
- const start = performance.now();
165
+ send(message) {
166
+ if (this._destroyed)
167
+ return;
329
168
  try {
330
- return await fn();
331
- } finally {
332
- const duration = performance.now() - start;
333
- this.debug(`${label}: ${duration.toFixed(2)}ms`);
169
+ this.transport.send(message);
170
+ } catch (err) {
171
+ log.error(`Failed to send message on session ${this.id}:`, err);
334
172
  }
335
173
  }
336
- child(subTag) {
337
- return new Logger(`${this.tag}:${subTag}`);
338
- }
339
- debugIf(condition, ...args) {
340
- if (condition)
341
- this.debug(...args);
342
- }
343
- warnIf(condition, ...args) {
344
- if (condition)
345
- this.warn(...args);
346
- }
347
- errorIf(condition, ...args) {
348
- if (condition)
349
- this.error(...args);
350
- }
351
- loggedOnce = new Set;
352
- warnOnce(key, ...args) {
353
- if (this.loggedOnce.has(key))
354
- return;
355
- this.loggedOnce.add(key);
356
- this.warn(...args);
357
- }
358
- debugOnce(key, ...args) {
359
- if (this.loggedOnce.has(key))
174
+ expireAndClose(reason) {
175
+ if (!this._sessionId) {
176
+ this.transport.close(1000, "Session closed");
360
177
  return;
361
- this.loggedOnce.add(key);
362
- this.debug(...args);
363
- }
364
- }
365
- function createLogger(tag) {
366
- return new Logger(tag);
367
- }
368
- var logger = createLogger("Hypen");
369
- var log = {
370
- debug: (tag, ...args) => {
371
- if (!shouldLog("debug"))
372
- return;
373
- console.log(formatTag(tag, "debug"), ...args);
374
- },
375
- info: (tag, ...args) => {
376
- if (!shouldLog("info"))
377
- return;
378
- console.info(formatTag(tag, "info"), ...args);
379
- },
380
- warn: (tag, ...args) => {
381
- if (!shouldLog("warn"))
382
- return;
383
- console.warn(formatTag(tag, "warn"), ...args);
384
- },
385
- error: (tag, ...args) => {
386
- if (!shouldLog("error"))
387
- return;
388
- console.error(formatTag(tag, "error"), ...args);
389
- }
390
- };
391
- var frameworkLoggers = {
392
- hypen: createLogger("Hypen"),
393
- engine: createLogger("Engine"),
394
- router: createLogger("Router"),
395
- state: createLogger("State"),
396
- events: createLogger("Events"),
397
- remote: createLogger("Remote"),
398
- renderer: createLogger("Renderer"),
399
- module: createLogger("Module"),
400
- lifecycle: createLogger("Lifecycle"),
401
- loader: createLogger("Loader"),
402
- context: createLogger("Context"),
403
- discovery: createLogger("Discovery"),
404
- plugin: createLogger("Plugin"),
405
- canvas: createLogger("Canvas"),
406
- debug: createLogger("Debug")
407
- };
408
-
409
- // src/disposable.ts
410
- var log2 = frameworkLoggers.lifecycle;
411
- function isDisposable(obj) {
412
- return obj !== null && typeof obj === "object" && "dispose" in obj && typeof obj.dispose === "function";
413
- }
414
-
415
- class DisposableStack {
416
- stack = [];
417
- disposed = false;
418
- add(disposable) {
419
- if (this.disposed) {
420
- disposable.dispose();
421
- return disposable;
422
178
  }
423
- this.stack.push(disposable);
424
- return disposable;
425
- }
426
- addCallback(callback) {
427
- this.add({ dispose: callback });
428
- }
429
- addValue(value, dispose) {
430
- this.add({ dispose: () => dispose(value) });
431
- return value;
179
+ const expired = {
180
+ type: "sessionExpired",
181
+ sessionId: this._sessionId,
182
+ reason
183
+ };
184
+ this.send(expired);
185
+ this.transport.close(1000, "Session " + reason);
432
186
  }
433
- dispose() {
434
- if (this.disposed)
187
+ async destroy() {
188
+ if (this._destroyed)
435
189
  return;
436
- this.disposed = true;
437
- while (this.stack.length > 0) {
438
- const item = this.stack.pop();
439
- try {
440
- item.dispose();
441
- } catch (error) {
442
- log2.error("Error during dispose:", error);
443
- }
444
- }
445
- }
446
- get isDisposed() {
447
- return this.disposed;
448
- }
449
- get size() {
450
- return this.stack.length;
451
- }
452
- }
453
- function disposableListener(target, event, handler, options) {
454
- target.addEventListener(event, handler, options);
455
- return {
456
- dispose: () => target.removeEventListener(event, handler, options)
457
- };
458
- }
459
- function disposableTimeout(callback, ms) {
460
- const id = setTimeout(callback, ms);
461
- return {
462
- id,
463
- dispose: () => clearTimeout(id)
464
- };
465
- }
466
- function disposableInterval(callback, ms) {
467
- const id = setInterval(callback, ms);
468
- return {
469
- id,
470
- dispose: () => clearInterval(id)
471
- };
472
- }
473
- function disposableWebSocket(ws) {
474
- return {
475
- dispose: () => {
476
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
477
- ws.close();
478
- }
479
- }
480
- };
481
- }
482
- function disposableAbortController() {
483
- const controller = new AbortController;
484
- return {
485
- controller,
486
- signal: controller.signal,
487
- dispose: () => controller.abort()
488
- };
489
- }
490
- function disposableSubscription(unsubscribe) {
491
- return { dispose: unsubscribe };
492
- }
493
- var ELEMENT_DISPOSABLES = Symbol("hypen.disposables");
494
- function getElementDisposables(element) {
495
- const existing = element[ELEMENT_DISPOSABLES];
496
- if (existing instanceof DisposableStack) {
497
- return existing;
498
- }
499
- const stack = new DisposableStack;
500
- element[ELEMENT_DISPOSABLES] = stack;
501
- return stack;
502
- }
503
- function disposeElement(element) {
504
- const stack = element[ELEMENT_DISPOSABLES];
505
- if (stack instanceof DisposableStack) {
506
- stack.dispose();
507
- delete element[ELEMENT_DISPOSABLES];
508
- }
509
- }
510
- function hasElementDisposables(element) {
511
- return element[ELEMENT_DISPOSABLES] instanceof DisposableStack;
512
- }
513
-
514
- class DisposableMixin {
515
- disposables = new DisposableStack;
516
- track(disposable) {
517
- return this.disposables.add(disposable);
518
- }
519
- onDispose(callback) {
520
- this.disposables.addCallback(callback);
521
- }
522
- dispose() {
523
- this.disposables.dispose();
524
- }
525
- }
526
- function compositeDisposable(...disposables) {
527
- return {
528
- dispose: () => {
529
- for (const d of disposables) {
190
+ this._destroyed = true;
191
+ if (this.helloTimeout) {
192
+ clearTimeout(this.helloTimeout);
193
+ this.helloTimeout = null;
194
+ }
195
+ const currentState = this._moduleInstance?.getState() ?? {};
196
+ if (this._sessionId && this.host.module.handlers.onDisconnect) {
197
+ const session = this.host.sessionManager.getActiveSession(this._sessionId);
198
+ if (session) {
530
199
  try {
531
- d.dispose();
532
- } catch (error) {
533
- log2.error("Error during dispose:", error);
200
+ await this.host.module.handlers.onDisconnect({
201
+ state: currentState,
202
+ session
203
+ });
204
+ } catch (err) {
205
+ log.error(`onDisconnect threw on session ${this.id}:`, err);
534
206
  }
535
207
  }
536
208
  }
537
- };
538
- }
539
- async function using(resource, fn) {
540
- const r = typeof resource === "function" ? resource() : resource;
541
- try {
542
- return await fn(r);
543
- } finally {
544
- r.dispose();
545
- }
546
- }
547
- function usingSync(resource, fn) {
548
- const r = typeof resource === "function" ? resource() : resource;
549
- try {
550
- return fn(r);
551
- } finally {
552
- r.dispose();
553
- }
554
- }
555
-
556
- // src/retry.ts
557
- var DEFAULT_OPTIONS = {
558
- maxAttempts: 3,
559
- delayMs: 1000,
560
- backoff: "exponential",
561
- maxDelayMs: 30000,
562
- jitter: 0.1
563
- };
564
- function calculateDelay(attempt, options) {
565
- let delay;
566
- switch (options.backoff) {
567
- case "exponential":
568
- delay = options.delayMs * Math.pow(2, attempt - 1);
569
- break;
570
- case "linear":
571
- delay = options.delayMs * attempt;
572
- break;
573
- case "none":
574
- delay = options.delayMs;
575
- break;
576
- }
577
- if (options.jitter > 0) {
578
- const jitterRange = delay * options.jitter;
579
- delay += (Math.random() * 2 - 1) * jitterRange;
580
- }
581
- return Math.min(delay, options.maxDelayMs);
582
- }
583
- function sleep(ms, signal) {
584
- return new Promise((resolve, reject) => {
585
- if (signal?.aborted) {
586
- reject(new Error("Retry aborted"));
587
- return;
588
- }
589
- const timeoutId = setTimeout(resolve, ms);
590
- signal?.addEventListener("abort", () => {
591
- clearTimeout(timeoutId);
592
- reject(new Error("Retry aborted"));
593
- });
594
- });
595
- }
596
- async function retry(fn, options = {}) {
597
- const opts = { ...DEFAULT_OPTIONS, ...options };
598
- let lastError = new Error("No attempts made");
599
- for (let attempt = 1;attempt <= opts.maxAttempts; attempt++) {
600
- try {
601
- if (opts.signal?.aborted) {
602
- throw new Error("Retry aborted");
603
- }
604
- return await fn();
605
- } catch (e) {
606
- lastError = e instanceof Error ? e : new Error(String(e));
607
- if (opts.shouldRetry && !opts.shouldRetry(lastError)) {
608
- throw lastError;
609
- }
610
- if (attempt === opts.maxAttempts) {
611
- break;
209
+ if (this._sessionId) {
210
+ this.host.sessionManager.untrackConnection(this._sessionId, this);
211
+ if (this.host.sessionManager.getConnectionCount(this._sessionId) === 0) {
212
+ const session = this.host.sessionManager.getActiveSession(this._sessionId);
213
+ if (session) {
214
+ this.host.sessionManager.suspendSession(this._sessionId, currentState, async (expiredSession) => {
215
+ if (this.host.module.handlers.onExpire) {
216
+ await this.host.module.handlers.onExpire({
217
+ session: expiredSession
218
+ });
219
+ }
220
+ });
221
+ }
612
222
  }
613
- const delayMs = calculateDelay(attempt, opts);
614
- opts.onRetry?.(attempt, lastError, delayMs);
615
- await sleep(delayMs, opts.signal);
616
223
  }
617
- }
618
- throw lastError;
619
- }
620
- async function retryResult(fn, options = {}) {
621
- try {
622
- const value = await retry(fn, options);
623
- return Ok(value);
624
- } catch (e) {
625
- return Err(e instanceof Error ? e : new Error(String(e)));
626
- }
627
- }
628
- function withRetry(fn, options = {}) {
629
- return (...args) => retry(() => fn(...args), options);
630
- }
631
- var RetryConditions = {
632
- networkErrors: (error) => {
633
- const message = error.message.toLowerCase();
634
- return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("socket");
635
- },
636
- httpRetryable: (error) => {
637
- const status = error.status;
638
- if (!status)
639
- return false;
640
- return [408, 429, 500, 502, 503, 504].includes(status);
641
- },
642
- websocketErrors: (error) => {
643
- const message = error.message.toLowerCase();
644
- return message.includes("websocket") || message.includes("connection") || message.includes("close");
645
- },
646
- any: (...conditions) => (error) => conditions.some((c) => c(error)),
647
- all: (...conditions) => (error) => conditions.every((c) => c(error))
648
- };
649
- var RetryPresets = {
650
- aggressive: {
651
- maxAttempts: 10,
652
- delayMs: 500,
653
- backoff: "exponential",
654
- maxDelayMs: 60000,
655
- jitter: 0.2
656
- },
657
- conservative: {
658
- maxAttempts: 3,
659
- delayMs: 2000,
660
- backoff: "linear",
661
- maxDelayMs: 1e4,
662
- jitter: 0.1
663
- },
664
- fast: {
665
- maxAttempts: 5,
666
- delayMs: 100,
667
- backoff: "exponential",
668
- maxDelayMs: 2000,
669
- jitter: 0
670
- },
671
- websocket: {
672
- maxAttempts: 10,
673
- delayMs: 1000,
674
- backoff: "exponential",
675
- maxDelayMs: 30000,
676
- jitter: 0.1,
677
- shouldRetry: RetryConditions.websocketErrors
678
- }
679
- };
680
-
681
- // src/remote/client.ts
682
- var log3 = frameworkLoggers.remote;
683
-
684
- class RemoteEngine {
685
- ws = null;
686
- url;
687
- state = "disconnected";
688
- options;
689
- reconnectAttempts = 0;
690
- disposables = new DisposableStack;
691
- reconnectDisposable = null;
692
- currentSessionId = null;
693
- sessionOptions;
694
- patchCallbacks = [];
695
- stateCallbacks = [];
696
- connectionCallbacks = [];
697
- disconnectionCallbacks = [];
698
- errorCallbacks = [];
699
- sessionEstablishedCallbacks = [];
700
- sessionExpiredCallbacks = [];
701
- currentState = null;
702
- currentRevision = 0;
703
- moduleName = "";
704
- navigationDisposable = null;
705
- lastViewValue = null;
706
- handlingPopState = false;
707
- constructor(url, options = {}) {
708
- this.url = url;
709
- const navOpt = options.navigation;
710
- const navigation = navOpt ? {
711
- backAction: (typeof navOpt === "object" ? navOpt.backAction : undefined) ?? "navigateBack",
712
- viewStateKey: (typeof navOpt === "object" ? navOpt.viewStateKey : undefined) ?? "currentView"
713
- } : undefined;
714
- this.options = {
715
- autoReconnect: options.autoReconnect ?? true,
716
- reconnectInterval: options.reconnectInterval ?? 3000,
717
- maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
718
- session: options.session,
719
- navigation
720
- };
721
- this.sessionOptions = options.session;
722
- if (options.session?.id) {
723
- this.currentSessionId = options.session.id;
724
- }
725
- }
726
- async connect() {
727
- if (this.state === "connected" || this.state === "connecting") {
728
- return Ok(undefined);
729
- }
730
- this.state = "connecting";
731
- return new Promise((resolve) => {
224
+ if (this._autoManagedRouter) {
732
225
  try {
733
- this.ws = new WebSocket(this.url);
734
- this.disposables.add(disposableWebSocket(this.ws));
735
- const messageHandler = (event) => {
736
- this.handleMessage(event.data);
737
- };
738
- this.disposables.add(disposableListener(this.ws, "message", messageHandler));
739
- const errorHandler = () => {
740
- this.state = "error";
741
- const error = new ConnectionError(this.url, new Error("WebSocket error"));
742
- this.errorCallbacks.forEach((cb) => cb(error));
743
- resolve(Err(error));
744
- };
745
- this.disposables.add(disposableListener(this.ws, "error", errorHandler));
746
- const closeHandler = () => {
747
- this.state = "disconnected";
748
- this.disconnectionCallbacks.forEach((cb) => cb());
749
- this.attemptReconnect();
750
- };
751
- this.disposables.add(disposableListener(this.ws, "close", closeHandler));
752
- this.ws.onopen = () => {
753
- this.state = "connected";
754
- this.reconnectAttempts = 0;
755
- if (this.reconnectDisposable) {
756
- this.reconnectDisposable.dispose();
757
- this.reconnectDisposable = null;
758
- }
759
- this.sendHello();
760
- this.connectionCallbacks.forEach((cb) => cb());
761
- resolve(Ok(undefined));
762
- };
763
- } catch (e) {
764
- this.state = "error";
765
- const error = new ConnectionError(this.url, e);
766
- resolve(Err(error));
226
+ await this._autoManagedRouter.stop();
227
+ } catch (err) {
228
+ log.error(`Auto-router stop failed on ${this.id}:`, err);
767
229
  }
768
- });
769
- }
770
- sendHello() {
771
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
772
- return;
773
- const hello = {
774
- type: "hello",
775
- sessionId: this.currentSessionId ?? this.sessionOptions?.id,
776
- props: this.sessionOptions?.props,
777
- persistKey: this.sessionOptions?.persistKey
778
- };
779
- this.ws.send(JSON.stringify(hello));
780
- }
781
- disconnect() {
782
- if (this.reconnectDisposable) {
783
- this.reconnectDisposable.dispose();
784
- this.reconnectDisposable = null;
230
+ this._autoManagedRouter = null;
785
231
  }
786
- if (this.ws) {
787
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
788
- this.ws.close();
232
+ if (this._moduleInstance) {
233
+ try {
234
+ await this._moduleInstance.destroy();
235
+ } catch (err) {
236
+ log.error(`ModuleInstance.destroy() failed on ${this.id}:`, err);
789
237
  }
790
- this.ws = null;
791
238
  }
792
- this.state = "disconnected";
239
+ this.host.onSessionDestroyed(this, this.toRemoteClient());
240
+ this._resolveReady();
241
+ this._resolveClosed();
793
242
  }
794
- dispose() {
795
- this.disconnect();
796
- if (this.navigationDisposable) {
797
- this.navigationDisposable.dispose();
798
- this.navigationDisposable = null;
799
- }
800
- this.disposables.dispose();
801
- }
802
- setupNavigation() {
803
- const nav = this.options.navigation;
804
- if (!nav || typeof window === "undefined")
805
- return;
806
- if (this.navigationDisposable)
807
- return;
808
- this.navigationDisposable = disposableListener(window, "popstate", () => {
809
- if (this.handlingPopState)
810
- return;
811
- log3.debug("Back navigation detected, dispatching:", nav.backAction);
812
- this.handlingPopState = true;
813
- this.dispatchAction(nav.backAction);
243
+ setupComponentResolver() {
244
+ this.engine.setComponentResolver((componentName, _contextPath) => {
245
+ const comp = this.host.discoveredComponents.get(componentName);
246
+ if (!comp)
247
+ return null;
248
+ return { source: comp.template, path: componentName };
814
249
  });
815
250
  }
816
- trackViewChange(state) {
817
- const nav = this.options.navigation;
818
- if (!nav || typeof window === "undefined")
819
- return;
820
- const viewValue = this.resolveStateKey(state, nav.viewStateKey);
821
- if (viewValue === undefined || viewValue === this.lastViewValue)
822
- return;
823
- if (this.handlingPopState) {
824
- this.handlingPopState = false;
825
- this.lastViewValue = viewValue;
826
- return;
827
- }
828
- if (this.lastViewValue !== null) {
829
- window.history.pushState({ view: viewValue }, "", "");
830
- }
831
- this.lastViewValue = viewValue;
832
- }
833
- resolveStateKey(state, key) {
834
- if (!state || typeof state !== "object")
835
- return;
836
- const parts = key.split(".");
837
- let current = state;
838
- for (const part of parts) {
839
- if (current == null || typeof current !== "object")
840
- return;
841
- current = current[part];
251
+ registerNestedModules() {
252
+ const primary = this.host.moduleName;
253
+ if (this.host.app) {
254
+ for (const [name, def] of this.host.app.components) {
255
+ if (name === primary)
256
+ continue;
257
+ const stateKeys = def.initialState !== null && typeof def.initialState === "object" ? Object.keys(def.initialState) : [];
258
+ const snapshot = def.initialState !== null && typeof def.initialState === "object" ? structuredClone(def.initialState) : {};
259
+ this.engine.registerModule(name, def.actions ?? [], stateKeys, snapshot);
260
+ log.info(`Registered nested module "${name}" (${def.actions?.length ?? 0} actions, ${stateKeys.length} state keys)`);
261
+ }
842
262
  }
843
- return typeof current === "string" ? current : undefined;
844
- }
845
- dispatchAction(action, payload) {
846
- if (this.state !== "connected" || !this.ws) {
847
- log3.warn("Cannot dispatch action: not connected");
263
+ for (const [name, comp] of this.host.discoveredComponents) {
264
+ if (name === primary)
265
+ continue;
266
+ if (!comp.module)
267
+ continue;
268
+ if (this.host.app?.has(name))
269
+ continue;
270
+ const def = comp.module;
271
+ const hasState = def.initialState !== null && typeof def.initialState === "object" && Object.keys(def.initialState).length > 0;
272
+ const hasActions = (def.actions?.length ?? 0) > 0;
273
+ if (!hasState && !hasActions)
274
+ continue;
275
+ const stateKeys = def.initialState !== null && typeof def.initialState === "object" ? Object.keys(def.initialState) : [];
276
+ const snapshot = def.initialState !== null && typeof def.initialState === "object" ? JSON.parse(JSON.stringify(def.initialState)) : {};
277
+ this.engine.registerModule(name, def.actions ?? [], stateKeys, snapshot);
278
+ log.info(`Registered nested module "${name}" from discovery`);
279
+ }
280
+ }
281
+ async initializeSession(requestedSessionId, props) {
282
+ if (this._helloReceived || this._destroyed)
848
283
  return;
849
- }
850
- const message = {
851
- type: "dispatchAction",
852
- module: this.moduleName,
853
- action,
854
- payload
284
+ this._helloReceived = true;
285
+ log.info(`Initializing session for ${this.id} (sessionId: ${requestedSessionId ?? "new"})`);
286
+ if (this.helloTimeout) {
287
+ clearTimeout(this.helloTimeout);
288
+ this.helloTimeout = null;
289
+ }
290
+ let session;
291
+ let isNew = true;
292
+ let isRestored = false;
293
+ let restoredState = null;
294
+ const sm = this.host.sessionManager;
295
+ if (requestedSessionId) {
296
+ const resumed = sm.resumeSession(requestedSessionId);
297
+ if (resumed) {
298
+ session = resumed.session;
299
+ restoredState = resumed.savedState;
300
+ isNew = false;
301
+ isRestored = true;
302
+ } else {
303
+ const activeSession = sm.getActiveSession(requestedSessionId);
304
+ if (activeSession) {
305
+ const allowed = this.resolveConcurrentConnection(activeSession, props);
306
+ if (!allowed)
307
+ return;
308
+ session = activeSession;
309
+ isNew = false;
310
+ } else {
311
+ session = sm.createSession(props);
312
+ }
313
+ }
314
+ } else {
315
+ session = sm.createSession(props);
316
+ }
317
+ this._sessionId = session.id;
318
+ sm.trackConnection(session.id, this);
319
+ const sessionAck = {
320
+ type: "sessionAck",
321
+ sessionId: session.id,
322
+ isNew,
323
+ isRestored
855
324
  };
856
- this.ws.send(JSON.stringify(message));
857
- }
858
- subscribeState() {
859
- if (this.state !== "connected" || !this.ws) {
860
- log3.warn("Cannot subscribe to state: not connected");
861
- return;
862
- }
863
- const message = { type: "subscribeState" };
864
- this.ws.send(JSON.stringify(message));
865
- }
866
- updateState(state) {
867
- if (this.state !== "connected" || !this.ws) {
868
- log3.warn("Cannot update state: not connected");
325
+ this.send(sessionAck);
326
+ log.info(`Sent sessionAck to ${this.id} (session: ${session.id})`);
327
+ if (!this._moduleInstance) {
328
+ this._moduleInstance = new HypenModuleInstance(this.engine, this.host.module);
329
+ }
330
+ if (isRestored) {
331
+ await this.triggerReconnect(session, restoredState);
332
+ }
333
+ await this._moduleInstance.waitForReady();
334
+ this.registerNestedModules();
335
+ await new Promise((resolve) => queueMicrotask(resolve));
336
+ const initialPatches = [];
337
+ this.engine.setRenderCallback((patches) => {
338
+ initialPatches.push(...patches);
339
+ });
340
+ try {
341
+ this.engine.renderSource(this.host.ui);
342
+ } catch (err) {
343
+ log.error(`Failed to render UI for ${this.id}:`, err);
344
+ this.transport.close(1011, "Render failed");
869
345
  return;
870
346
  }
871
- const message = {
872
- type: "updateState",
873
- module: this.moduleName,
874
- state
347
+ this.setupStreamingRenderCallback();
348
+ const initialMessage = {
349
+ type: "initialTree",
350
+ module: this.host.moduleName,
351
+ state: this._moduleInstance.getState(),
352
+ patches: initialPatches,
353
+ revision: 0
875
354
  };
876
- this.ws.send(JSON.stringify(message));
877
- }
878
- onPatches(callback) {
879
- this.patchCallbacks.push(callback);
880
- return this;
881
- }
882
- onStateUpdate(callback) {
883
- this.stateCallbacks.push(callback);
884
- return this;
885
- }
886
- onConnect(callback) {
887
- this.connectionCallbacks.push(callback);
888
- return this;
889
- }
890
- onDisconnect(callback) {
891
- this.disconnectionCallbacks.push(callback);
892
- return this;
893
- }
894
- onError(callback) {
895
- this.errorCallbacks.push(callback);
896
- return this;
897
- }
898
- onSessionEstablished(callback) {
899
- this.sessionEstablishedCallbacks.push(callback);
900
- return this;
901
- }
902
- onSessionExpired(callback) {
903
- this.sessionExpiredCallbacks.push(callback);
904
- return this;
905
- }
906
- getConnectionState() {
907
- return this.state;
908
- }
909
- getCurrentState() {
910
- return this.currentState;
911
- }
912
- getRevision() {
913
- return this.currentRevision;
914
- }
915
- getSessionId() {
916
- return this.currentSessionId;
917
- }
918
- handleMessage(data) {
919
- try {
920
- const message = JSON.parse(data);
921
- switch (message.type) {
922
- case "sessionAck":
923
- this.handleSessionAck(message);
924
- break;
925
- case "sessionExpired":
926
- this.handleSessionExpired(message);
927
- break;
928
- case "initialTree":
929
- this.handleInitialTree(message);
930
- break;
931
- case "patch":
932
- this.handlePatch(message);
933
- break;
934
- case "stateUpdate":
935
- this.currentState = message.state;
936
- this.trackViewChange(this.currentState);
937
- this.stateCallbacks.forEach((cb) => cb(this.currentState));
938
- break;
355
+ this.send(initialMessage);
356
+ log.info(`Sent initialTree to ${this.id} (${initialPatches.length} patches)`);
357
+ this.host.onSessionReady(this, this.toRemoteClient());
358
+ this._resolveReady();
359
+ if (this.autoRouterEnabled) {
360
+ this.autoWireManagedRouter();
361
+ }
362
+ }
363
+ autoWireManagedRouter() {
364
+ const app = this.host.app ?? new HypenApp;
365
+ const discovered = [];
366
+ const runDiscover = (source, label) => {
367
+ try {
368
+ const blocks = this.engine.discoverRouters(source);
369
+ for (const b of blocks)
370
+ discovered.push(b);
371
+ } catch (err) {
372
+ log.error(`Auto-router: discoverRouters failed on ${label}:`, err);
939
373
  }
940
- } catch (e) {
941
- log3.error("Error handling remote message:", e);
942
- const error = e instanceof Error ? e : new Error(String(e));
943
- this.errorCallbacks.forEach((cb) => cb(error));
944
- }
945
- }
946
- handleSessionAck(message) {
947
- this.currentSessionId = message.sessionId;
948
- const info = {
949
- sessionId: message.sessionId,
950
- isNew: message.isNew,
951
- isRestored: message.isRestored
952
374
  };
953
- this.sessionEstablishedCallbacks.forEach((cb) => cb(info));
954
- }
955
- handleSessionExpired(message) {
956
- this.currentSessionId = null;
957
- this.sessionExpiredCallbacks.forEach((cb) => cb(message.reason));
958
- }
959
- handleInitialTree(message) {
960
- this.moduleName = message.module;
961
- this.currentState = message.state;
962
- this.currentRevision = message.revision;
963
- if (message.patches.length > 0) {
964
- this.patchCallbacks.forEach((cb) => cb(message.patches));
375
+ runDiscover(this.host.ui, this.id);
376
+ for (const [name, comp] of this.host.discoveredComponents) {
377
+ if (comp.template)
378
+ runDiscover(comp.template, `${this.id} / ${name}`);
965
379
  }
966
- this.trackViewChange(message.state);
967
- this.setupNavigation();
968
- if (this.options.navigation) {
969
- this.subscribeState();
970
- }
971
- this.stateCallbacks.forEach((cb) => cb(message.state));
972
- }
973
- handlePatch(message) {
974
- if (message.revision <= this.currentRevision) {
975
- log3.warn(`Out of order patch: expected > ${this.currentRevision}, got ${message.revision}`);
380
+ if (discovered.length === 0)
976
381
  return;
382
+ const primaryScope = this.host.moduleName.toLowerCase();
383
+ const router = new HypenRouter;
384
+ const globalContext = new HypenGlobalContext;
385
+ const managed = new ManagedRouter(router, this.engine, app, globalContext);
386
+ const primary = this._moduleInstance;
387
+ if (primary) {
388
+ globalContext.registerModule(primaryScope, primary);
389
+ }
390
+ let added = 0;
391
+ const seenPaths = new Set;
392
+ for (const block of discovered) {
393
+ for (const route of block.routes) {
394
+ if (seenPaths.has(route.path)) {
395
+ log.debug(`Auto-router: path "${route.path}" already registered; ignoring nested duplicate`);
396
+ continue;
397
+ }
398
+ const component = this.pickComponent(route.elementNames);
399
+ if (!component) {
400
+ log.debug(`Auto-router: no registered module matched route "${route.path}" — skipping`);
401
+ continue;
402
+ }
403
+ managed.addRoute({ path: route.path, component });
404
+ seenPaths.add(route.path);
405
+ added += 1;
406
+ }
977
407
  }
978
- this.currentRevision = message.revision;
979
- if (message.patches.length > 0) {
980
- this.patchCallbacks.forEach((cb) => cb(message.patches));
981
- }
982
- }
983
- attemptReconnect() {
984
- if (!this.options.autoReconnect) {
408
+ const locationKey = this.primaryHasLocationKey() ? "location" : null;
409
+ if (added === 0 && !locationKey) {
410
+ log.debug(`Auto-router: nothing to mount for ${this.id}`);
985
411
  return;
986
412
  }
987
- this.reconnectDisposable = disposableTimeout(() => {
988
- this.reconnectDisposable = null;
989
- retry(async () => {
990
- const result = await this.connect();
991
- if (!result.ok) {
992
- throw result.error;
993
- }
994
- }, {
995
- maxAttempts: this.options.maxReconnectAttempts,
996
- delayMs: this.options.reconnectInterval,
997
- backoff: "exponential",
998
- maxDelayMs: 30000,
999
- jitter: 0.1,
1000
- onRetry: (attempt, error) => {
1001
- log3.debug(`Reconnection attempt ${attempt}/${this.options.maxReconnectAttempts} failed: ${error.message}`);
1002
- }
1003
- }).catch((error) => {
1004
- log3.error("Max reconnection attempts reached:", error.message);
1005
- this.errorCallbacks.forEach((cb) => cb(new ConnectionError(this.url, error, this.options.maxReconnectAttempts)));
413
+ if (locationKey) {
414
+ router.onNavigate((rs) => {
415
+ const path = rs.currentPath;
416
+ queueMicrotask(() => {
417
+ try {
418
+ this._moduleInstance?.updateState({ [locationKey]: path });
419
+ } catch (err) {
420
+ log.error(`Auto-router: state.${locationKey} sync failed:`, err);
421
+ }
422
+ });
1006
423
  });
1007
- }, this.options.reconnectInterval);
1008
- }
1009
- }
1010
-
1011
- // src/remote/session.ts
1012
- class SessionManager {
1013
- activeSessions = new Map;
1014
- pendingSessions = new Map;
1015
- sessionConnections = new Map;
1016
- config;
1017
- constructor(config2 = {}) {
1018
- this.config = {
1019
- ttl: config2.ttl ?? 3600,
1020
- concurrent: config2.concurrent ?? "kick-old",
1021
- generateId: config2.generateId ?? (() => crypto.randomUUID())
1022
- };
1023
- }
1024
- getTtl() {
1025
- return this.config.ttl;
1026
- }
1027
- getConcurrentPolicy() {
1028
- return this.config.concurrent;
424
+ }
425
+ managed.start();
426
+ router.push(router.getCurrentPath());
427
+ this._autoManagedRouter = managed;
1029
428
  }
1030
- createSession(props) {
1031
- const now = new Date;
1032
- const session = {
1033
- id: this.config.generateId(),
1034
- ttl: this.config.ttl,
1035
- createdAt: now,
1036
- lastConnectedAt: now,
1037
- props
1038
- };
1039
- this.activeSessions.set(session.id, session);
1040
- return session;
429
+ pickComponent(elementNames) {
430
+ const app = this.host.app;
431
+ if (!app)
432
+ return null;
433
+ for (const name of elementNames) {
434
+ if (app.has(name))
435
+ return name;
436
+ }
437
+ return null;
1041
438
  }
1042
- getActiveSession(id) {
1043
- return this.activeSessions.get(id) ?? null;
439
+ primaryHasLocationKey() {
440
+ const state = this._moduleInstance?.getState();
441
+ return state !== null && state !== undefined && typeof state === "object" && "location" in state;
1044
442
  }
1045
- getPendingSession(id) {
1046
- return this.pendingSessions.get(id) ?? null;
443
+ setupStreamingRenderCallback() {
444
+ this.engine.setRenderCallback((patches) => {
445
+ if (this._destroyed)
446
+ return;
447
+ this._revision++;
448
+ log.info(`Streaming ${patches.length} patches to ${this.id} (rev ${this._revision})`);
449
+ const patchMessage = {
450
+ type: "patch",
451
+ module: this.host.moduleName,
452
+ patches,
453
+ revision: this._revision
454
+ };
455
+ this.send(patchMessage);
456
+ if (this._stateSubscribed && this._moduleInstance) {
457
+ const stateMessage = {
458
+ type: "stateUpdate",
459
+ module: this.host.moduleName,
460
+ state: this._moduleInstance.getState(),
461
+ revision: this._revision
462
+ };
463
+ this.send(stateMessage);
464
+ }
465
+ if (this._sessionId && this.host.sessionManager.getConcurrentPolicy() === "allow-multiple") {
466
+ for (const peer of this.host.sessionsForId(this._sessionId)) {
467
+ if (peer === this)
468
+ continue;
469
+ peer.send(patchMessage);
470
+ if (peer._stateSubscribed && this._moduleInstance) {
471
+ const stateMessage = {
472
+ type: "stateUpdate",
473
+ module: this.host.moduleName,
474
+ state: this._moduleInstance.getState(),
475
+ revision: this._revision
476
+ };
477
+ peer.send(stateMessage);
478
+ }
479
+ }
480
+ }
481
+ });
1047
482
  }
1048
- hasSession(id) {
1049
- return this.activeSessions.has(id) || this.pendingSessions.has(id);
483
+ resolveConcurrentConnection(existingSession, _props) {
484
+ const policy = this.host.sessionManager.getConcurrentPolicy();
485
+ switch (policy) {
486
+ case "kick-old": {
487
+ for (const peer of this.host.sessionsForId(existingSession.id)) {
488
+ if (peer === this)
489
+ continue;
490
+ peer.expireAndClose("kicked");
491
+ }
492
+ return true;
493
+ }
494
+ case "reject-new": {
495
+ this.expireAndClose("kicked");
496
+ return false;
497
+ }
498
+ case "allow-multiple":
499
+ default:
500
+ return true;
501
+ }
1050
502
  }
1051
- suspendSession(sessionId, savedState, onExpire) {
1052
- const session = this.activeSessions.get(sessionId);
1053
- if (!session)
503
+ async triggerReconnect(session, savedState) {
504
+ const handler = this.host.module.handlers.onReconnect;
505
+ if (!handler)
1054
506
  return;
1055
- this.activeSessions.delete(sessionId);
1056
- const expiryTimer = setTimeout(async () => {
1057
- const pending = this.pendingSessions.get(sessionId);
1058
- if (pending) {
1059
- this.pendingSessions.delete(sessionId);
1060
- await onExpire(pending.session);
507
+ const restore = (state) => {
508
+ if (state === null || typeof state !== "object") {
509
+ log.warn("restore() called with non-object state, ignoring:", typeof state);
510
+ return;
1061
511
  }
1062
- }, session.ttl * 1000);
1063
- this.pendingSessions.set(sessionId, {
1064
- session,
1065
- savedState,
1066
- expiryTimer
1067
- });
1068
- }
1069
- resumeSession(sessionId) {
1070
- const pending = this.pendingSessions.get(sessionId);
1071
- if (!pending)
1072
- return null;
1073
- clearTimeout(pending.expiryTimer);
1074
- this.pendingSessions.delete(sessionId);
1075
- pending.session.lastConnectedAt = new Date;
1076
- this.activeSessions.set(sessionId, pending.session);
1077
- return {
1078
- session: pending.session,
1079
- savedState: pending.savedState
512
+ this._moduleInstance?.updateState(state);
1080
513
  };
514
+ await handler({ session, restore });
1081
515
  }
1082
- destroySession(sessionId) {
1083
- this.activeSessions.delete(sessionId);
1084
- const pending = this.pendingSessions.get(sessionId);
1085
- if (pending) {
1086
- clearTimeout(pending.expiryTimer);
1087
- this.pendingSessions.delete(sessionId);
516
+ }
517
+ function createBunWebSocketTransport(ws) {
518
+ return {
519
+ send(message) {
520
+ ws.send(JSON.stringify(message));
521
+ },
522
+ close(code, reason) {
523
+ ws.close(code, reason);
1088
524
  }
1089
- this.sessionConnections.delete(sessionId);
1090
- }
1091
- trackConnection(sessionId, ws) {
1092
- let connections = this.sessionConnections.get(sessionId);
1093
- if (!connections) {
1094
- connections = new Set;
1095
- this.sessionConnections.set(sessionId, connections);
525
+ };
526
+ }
527
+
528
+ class AsyncQueueTransport {
529
+ queue = [];
530
+ waiters = [];
531
+ closed = false;
532
+ send(message) {
533
+ if (this.closed)
534
+ return;
535
+ const w = this.waiters.shift();
536
+ if (w) {
537
+ w({ value: message, done: false });
538
+ } else {
539
+ this.queue.push(message);
1096
540
  }
1097
- connections.add(ws);
1098
541
  }
1099
- untrackConnection(sessionId, ws) {
1100
- const connections = this.sessionConnections.get(sessionId);
1101
- if (connections) {
1102
- connections.delete(ws);
1103
- if (connections.size === 0) {
1104
- this.sessionConnections.delete(sessionId);
542
+ close(_code, _reason) {
543
+ if (this.closed)
544
+ return;
545
+ this.closed = true;
546
+ while (this.waiters.length) {
547
+ const w = this.waiters.shift();
548
+ w({ value: undefined, done: true });
549
+ }
550
+ }
551
+ stream() {
552
+ const self = this;
553
+ const iter = {
554
+ [Symbol.asyncIterator]() {
555
+ return iter;
556
+ },
557
+ async next() {
558
+ if (self.queue.length) {
559
+ return { value: self.queue.shift(), done: false };
560
+ }
561
+ if (self.closed) {
562
+ return { value: undefined, done: true };
563
+ }
564
+ return new Promise((resolve) => {
565
+ self.waiters.push(resolve);
566
+ });
567
+ },
568
+ async return() {
569
+ self.close();
570
+ return { value: undefined, done: true };
1105
571
  }
1106
- }
1107
- }
1108
- getConnections(sessionId) {
1109
- return this.sessionConnections.get(sessionId);
1110
- }
1111
- getConnectionCount(sessionId) {
1112
- return this.sessionConnections.get(sessionId)?.size ?? 0;
1113
- }
1114
- getStats() {
1115
- let totalConnections = 0;
1116
- for (const connections of this.sessionConnections.values()) {
1117
- totalConnections += connections.size;
1118
- }
1119
- return {
1120
- activeSessions: this.activeSessions.size,
1121
- pendingSessions: this.pendingSessions.size,
1122
- totalConnections
1123
572
  };
1124
- }
1125
- destroy() {
1126
- for (const pending of this.pendingSessions.values()) {
1127
- clearTimeout(pending.expiryTimer);
1128
- }
1129
- this.activeSessions.clear();
1130
- this.pendingSessions.clear();
1131
- this.sessionConnections.clear();
573
+ return iter;
1132
574
  }
1133
575
  }
1134
576
  export {
577
+ createBunWebSocketTransport,
1135
578
  SessionManager,
1136
- RemoteEngine
579
+ RemoteSession,
580
+ RemoteEngine,
581
+ AsyncQueueTransport
1137
582
  };
1138
583
 
1139
- //# debugId=88C744E190AA51D464756E2164756E21
584
+ //# debugId=EC47228D45CD496064756E2164756E21