@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.
- package/README.md +182 -11
- package/dist/src/app.js +470 -44
- package/dist/src/app.js.map +7 -5
- package/dist/src/components/builtin.js +470 -44
- package/dist/src/components/builtin.js.map +7 -5
- package/dist/src/discovery.js +559 -65
- package/dist/src/discovery.js.map +8 -6
- package/dist/src/engine.js +18 -9
- package/dist/src/engine.js.map +3 -3
- package/dist/src/index.browser.js +862 -81
- package/dist/src/index.browser.js.map +10 -6
- package/dist/src/index.js +1590 -124
- package/dist/src/index.js.map +16 -9
- package/dist/src/remote/client.js +525 -35
- package/dist/src/remote/client.js.map +7 -4
- package/dist/src/remote/index.js +1796 -35
- package/dist/src/remote/index.js.map +13 -4
- package/dist/src/router.js +55 -29
- package/dist/src/router.js.map +3 -3
- package/dist/src/state.js +57 -29
- package/dist/src/state.js.map +3 -3
- package/package.json +8 -2
- package/src/app.ts +292 -13
- package/src/discovery.ts +123 -18
- package/src/disposable.ts +281 -0
- package/src/engine.ts +29 -10
- package/src/hypen.ts +209 -0
- package/src/index.ts +147 -11
- package/src/logger.ts +338 -0
- package/src/remote/client.ts +263 -56
- package/src/remote/index.ts +25 -1
- package/src/remote/server.ts +652 -0
- package/src/remote/session.ts +256 -0
- package/src/remote/types.ts +68 -1
- package/src/result.ts +260 -0
- package/src/retry.ts +306 -0
- package/src/state.ts +103 -45
- package/wasm-browser/README.md +4 -0
- package/wasm-browser/hypen_engine_bg.wasm +0 -0
- package/wasm-browser/package.json +1 -1
- package/wasm-node/README.md +4 -0
- package/wasm-node/hypen_engine_bg.wasm +0 -0
- package/wasm-node/package.json +1 -1
- package/wasm-browser/hypen_engine_bg.js +0 -736
- package/wasm-node/hypen_engine_bg.js +0 -736
package/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 (
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
691
|
+
log2.debug(`Registering action handler: ${actionName} for module ${definition.name}`);
|
|
314
692
|
this.engine.onAction(actionName, async (action) => {
|
|
315
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
806
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1771
|
+
return new Promise((resolve) => {
|
|
1050
1772
|
try {
|
|
1051
1773
|
this.ws = new WebSocket(this.url);
|
|
1052
|
-
this.
|
|
1053
|
-
|
|
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
|
|
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
|
-
|
|
1783
|
+
resolve(Err(error));
|
|
1066
1784
|
};
|
|
1067
|
-
this.ws
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1080
|
-
|
|
1081
|
-
this.
|
|
1821
|
+
if (this.reconnectDisposable) {
|
|
1822
|
+
this.reconnectDisposable.dispose();
|
|
1823
|
+
this.reconnectDisposable = null;
|
|
1082
1824
|
}
|
|
1083
1825
|
if (this.ws) {
|
|
1084
|
-
this.ws.
|
|
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(
|
|
1908
|
+
this.stateCallbacks.forEach((cb) => cb(this.currentState));
|
|
1144
1909
|
break;
|
|
1145
1910
|
}
|
|
1146
|
-
} catch (
|
|
1147
|
-
console.error("Error handling remote message:",
|
|
1148
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
|
1330
|
-
while ((
|
|
1331
|
-
const [, namedImports, defaultImport, source] =
|
|
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
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
const
|
|
2155
|
+
log3("Scanning directory:", resolvedDir);
|
|
2156
|
+
log3("Patterns:", patterns);
|
|
2157
|
+
const addTwoFileComponent = (name, hypenPath, modulePath) => {
|
|
1369
2158
|
if (seen.has(name)) {
|
|
1370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2342
|
+
log3("Added:", name);
|
|
1494
2343
|
onAdd?.(component);
|
|
1495
2344
|
} else if (existing.template !== component.template || existing.modulePath !== component.modulePath) {
|
|
1496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
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
|
|
1612
|
-
while ((
|
|
1613
|
-
const [, namedImports, defaultImport, source] =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2501
|
+
log3("Found imports:", imports);
|
|
1645
2502
|
}
|
|
1646
2503
|
const componentName = getComponentName(hypenPath);
|
|
1647
|
-
|
|
2504
|
+
log3("Component name:", componentName);
|
|
1648
2505
|
const modulePath = findModulePath(hypenPath, patterns);
|
|
1649
|
-
|
|
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
|
-
|
|
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=
|
|
3261
|
+
//# debugId=7419746F35A290F664756E2164756E21
|