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