@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
|
@@ -10,6 +10,419 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
12
|
|
|
13
|
+
// src/result.ts
|
|
14
|
+
function Ok(value) {
|
|
15
|
+
return { ok: true, value };
|
|
16
|
+
}
|
|
17
|
+
function Err(error) {
|
|
18
|
+
return { ok: false, error };
|
|
19
|
+
}
|
|
20
|
+
function isOk(result) {
|
|
21
|
+
return result.ok;
|
|
22
|
+
}
|
|
23
|
+
function isErr(result) {
|
|
24
|
+
return !result.ok;
|
|
25
|
+
}
|
|
26
|
+
async function fromPromise(promise, mapError) {
|
|
27
|
+
try {
|
|
28
|
+
const value = await promise;
|
|
29
|
+
return Ok(value);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
if (mapError) {
|
|
32
|
+
return Err(mapError(e));
|
|
33
|
+
}
|
|
34
|
+
return Err(e);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function fromTry(fn, mapError) {
|
|
38
|
+
try {
|
|
39
|
+
return Ok(fn());
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (mapError) {
|
|
42
|
+
return Err(mapError(e));
|
|
43
|
+
}
|
|
44
|
+
return Err(e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function map(result, fn) {
|
|
48
|
+
if (result.ok) {
|
|
49
|
+
return Ok(fn(result.value));
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function mapErr(result, fn) {
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
return Err(fn(result.error));
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function flatMap(result, fn) {
|
|
60
|
+
if (result.ok) {
|
|
61
|
+
return fn(result.value);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function unwrap(result) {
|
|
66
|
+
if (result.ok) {
|
|
67
|
+
return result.value;
|
|
68
|
+
}
|
|
69
|
+
throw result.error;
|
|
70
|
+
}
|
|
71
|
+
function unwrapOr(result, defaultValue) {
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
return result.value;
|
|
74
|
+
}
|
|
75
|
+
return defaultValue;
|
|
76
|
+
}
|
|
77
|
+
function unwrapOrElse(result, fn) {
|
|
78
|
+
if (result.ok) {
|
|
79
|
+
return result.value;
|
|
80
|
+
}
|
|
81
|
+
return fn(result.error);
|
|
82
|
+
}
|
|
83
|
+
function match(result, handlers) {
|
|
84
|
+
if (result.ok) {
|
|
85
|
+
return handlers.ok(result.value);
|
|
86
|
+
}
|
|
87
|
+
return handlers.err(result.error);
|
|
88
|
+
}
|
|
89
|
+
function all(results) {
|
|
90
|
+
const values = [];
|
|
91
|
+
for (const result of results) {
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
values.push(result.value);
|
|
96
|
+
}
|
|
97
|
+
return Ok(values);
|
|
98
|
+
}
|
|
99
|
+
var HypenError, ActionError, ConnectionError, StateError;
|
|
100
|
+
var init_result = __esm(() => {
|
|
101
|
+
HypenError = class HypenError extends Error {
|
|
102
|
+
code;
|
|
103
|
+
context;
|
|
104
|
+
cause;
|
|
105
|
+
constructor(code, message, options) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = "HypenError";
|
|
108
|
+
this.code = code;
|
|
109
|
+
this.context = options?.context;
|
|
110
|
+
this.cause = options?.cause;
|
|
111
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
ActionError = class ActionError extends HypenError {
|
|
115
|
+
actionName;
|
|
116
|
+
constructor(actionName, cause) {
|
|
117
|
+
super("ACTION_ERROR", `Action handler "${actionName}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
118
|
+
context: { actionName },
|
|
119
|
+
cause: cause instanceof Error ? cause : undefined
|
|
120
|
+
});
|
|
121
|
+
this.name = "ActionError";
|
|
122
|
+
this.actionName = actionName;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
ConnectionError = class ConnectionError extends HypenError {
|
|
126
|
+
url;
|
|
127
|
+
attempt;
|
|
128
|
+
constructor(url, cause, attempt) {
|
|
129
|
+
super("CONNECTION_ERROR", `Connection to "${url}" failed${attempt ? ` (attempt ${attempt})` : ""}: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
130
|
+
context: { url, attempt },
|
|
131
|
+
cause: cause instanceof Error ? cause : undefined
|
|
132
|
+
});
|
|
133
|
+
this.name = "ConnectionError";
|
|
134
|
+
this.url = url;
|
|
135
|
+
this.attempt = attempt;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
StateError = class StateError extends HypenError {
|
|
139
|
+
path;
|
|
140
|
+
constructor(message, path, cause) {
|
|
141
|
+
super("STATE_ERROR", message, {
|
|
142
|
+
context: { path },
|
|
143
|
+
cause: cause instanceof Error ? cause : undefined
|
|
144
|
+
});
|
|
145
|
+
this.name = "StateError";
|
|
146
|
+
this.path = path;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// src/remote/client.ts
|
|
152
|
+
init_result();
|
|
153
|
+
|
|
154
|
+
// src/disposable.ts
|
|
155
|
+
function isDisposable(obj) {
|
|
156
|
+
return obj !== null && typeof obj === "object" && "dispose" in obj && typeof obj.dispose === "function";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
class DisposableStack {
|
|
160
|
+
stack = [];
|
|
161
|
+
disposed = false;
|
|
162
|
+
add(disposable) {
|
|
163
|
+
if (this.disposed) {
|
|
164
|
+
disposable.dispose();
|
|
165
|
+
return disposable;
|
|
166
|
+
}
|
|
167
|
+
this.stack.push(disposable);
|
|
168
|
+
return disposable;
|
|
169
|
+
}
|
|
170
|
+
addCallback(callback) {
|
|
171
|
+
this.add({ dispose: callback });
|
|
172
|
+
}
|
|
173
|
+
addValue(value, dispose) {
|
|
174
|
+
this.add({ dispose: () => dispose(value) });
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
dispose() {
|
|
178
|
+
if (this.disposed)
|
|
179
|
+
return;
|
|
180
|
+
this.disposed = true;
|
|
181
|
+
while (this.stack.length > 0) {
|
|
182
|
+
const item = this.stack.pop();
|
|
183
|
+
try {
|
|
184
|
+
item.dispose();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error("[DisposableStack] Error during dispose:", error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
get isDisposed() {
|
|
191
|
+
return this.disposed;
|
|
192
|
+
}
|
|
193
|
+
get size() {
|
|
194
|
+
return this.stack.length;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function disposableListener(target, event, handler, options) {
|
|
198
|
+
target.addEventListener(event, handler, options);
|
|
199
|
+
return {
|
|
200
|
+
dispose: () => target.removeEventListener(event, handler, options)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function disposableTimeout(callback, ms) {
|
|
204
|
+
const id = setTimeout(callback, ms);
|
|
205
|
+
return {
|
|
206
|
+
id,
|
|
207
|
+
dispose: () => clearTimeout(id)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function disposableInterval(callback, ms) {
|
|
211
|
+
const id = setInterval(callback, ms);
|
|
212
|
+
return {
|
|
213
|
+
id,
|
|
214
|
+
dispose: () => clearInterval(id)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function disposableWebSocket(ws) {
|
|
218
|
+
return {
|
|
219
|
+
dispose: () => {
|
|
220
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
221
|
+
ws.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function disposableAbortController() {
|
|
227
|
+
const controller = new AbortController;
|
|
228
|
+
return {
|
|
229
|
+
controller,
|
|
230
|
+
signal: controller.signal,
|
|
231
|
+
dispose: () => controller.abort()
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function disposableSubscription(unsubscribe) {
|
|
235
|
+
return { dispose: unsubscribe };
|
|
236
|
+
}
|
|
237
|
+
var ELEMENT_DISPOSABLES = Symbol("hypen.disposables");
|
|
238
|
+
function getElementDisposables(element) {
|
|
239
|
+
const existing = element[ELEMENT_DISPOSABLES];
|
|
240
|
+
if (existing instanceof DisposableStack) {
|
|
241
|
+
return existing;
|
|
242
|
+
}
|
|
243
|
+
const stack = new DisposableStack;
|
|
244
|
+
element[ELEMENT_DISPOSABLES] = stack;
|
|
245
|
+
return stack;
|
|
246
|
+
}
|
|
247
|
+
function disposeElement(element) {
|
|
248
|
+
const stack = element[ELEMENT_DISPOSABLES];
|
|
249
|
+
if (stack instanceof DisposableStack) {
|
|
250
|
+
stack.dispose();
|
|
251
|
+
delete element[ELEMENT_DISPOSABLES];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function hasElementDisposables(element) {
|
|
255
|
+
return element[ELEMENT_DISPOSABLES] instanceof DisposableStack;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class DisposableMixin {
|
|
259
|
+
disposables = new DisposableStack;
|
|
260
|
+
track(disposable) {
|
|
261
|
+
return this.disposables.add(disposable);
|
|
262
|
+
}
|
|
263
|
+
onDispose(callback) {
|
|
264
|
+
this.disposables.addCallback(callback);
|
|
265
|
+
}
|
|
266
|
+
dispose() {
|
|
267
|
+
this.disposables.dispose();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function compositeDisposable(...disposables) {
|
|
271
|
+
return {
|
|
272
|
+
dispose: () => {
|
|
273
|
+
for (const d of disposables) {
|
|
274
|
+
try {
|
|
275
|
+
d.dispose();
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error("[compositeDisposable] Error during dispose:", error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function using(resource, fn) {
|
|
284
|
+
const r = typeof resource === "function" ? resource() : resource;
|
|
285
|
+
try {
|
|
286
|
+
return await fn(r);
|
|
287
|
+
} finally {
|
|
288
|
+
r.dispose();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function usingSync(resource, fn) {
|
|
292
|
+
const r = typeof resource === "function" ? resource() : resource;
|
|
293
|
+
try {
|
|
294
|
+
return fn(r);
|
|
295
|
+
} finally {
|
|
296
|
+
r.dispose();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/retry.ts
|
|
301
|
+
init_result();
|
|
302
|
+
var DEFAULT_OPTIONS = {
|
|
303
|
+
maxAttempts: 3,
|
|
304
|
+
delayMs: 1000,
|
|
305
|
+
backoff: "exponential",
|
|
306
|
+
maxDelayMs: 30000,
|
|
307
|
+
jitter: 0.1
|
|
308
|
+
};
|
|
309
|
+
function calculateDelay(attempt, options) {
|
|
310
|
+
let delay;
|
|
311
|
+
switch (options.backoff) {
|
|
312
|
+
case "exponential":
|
|
313
|
+
delay = options.delayMs * Math.pow(2, attempt - 1);
|
|
314
|
+
break;
|
|
315
|
+
case "linear":
|
|
316
|
+
delay = options.delayMs * attempt;
|
|
317
|
+
break;
|
|
318
|
+
case "none":
|
|
319
|
+
delay = options.delayMs;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (options.jitter > 0) {
|
|
323
|
+
const jitterRange = delay * options.jitter;
|
|
324
|
+
delay += (Math.random() * 2 - 1) * jitterRange;
|
|
325
|
+
}
|
|
326
|
+
return Math.min(delay, options.maxDelayMs);
|
|
327
|
+
}
|
|
328
|
+
function sleep(ms, signal) {
|
|
329
|
+
return new Promise((resolve, reject) => {
|
|
330
|
+
if (signal?.aborted) {
|
|
331
|
+
reject(new Error("Retry aborted"));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const timeoutId = setTimeout(resolve, ms);
|
|
335
|
+
signal?.addEventListener("abort", () => {
|
|
336
|
+
clearTimeout(timeoutId);
|
|
337
|
+
reject(new Error("Retry aborted"));
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async function retry(fn, options = {}) {
|
|
342
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
343
|
+
let lastError = new Error("No attempts made");
|
|
344
|
+
for (let attempt = 1;attempt <= opts.maxAttempts; attempt++) {
|
|
345
|
+
try {
|
|
346
|
+
if (opts.signal?.aborted) {
|
|
347
|
+
throw new Error("Retry aborted");
|
|
348
|
+
}
|
|
349
|
+
return await fn();
|
|
350
|
+
} catch (e) {
|
|
351
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
352
|
+
if (opts.shouldRetry && !opts.shouldRetry(lastError)) {
|
|
353
|
+
throw lastError;
|
|
354
|
+
}
|
|
355
|
+
if (attempt === opts.maxAttempts) {
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
const delayMs = calculateDelay(attempt, opts);
|
|
359
|
+
opts.onRetry?.(attempt, lastError, delayMs);
|
|
360
|
+
await sleep(delayMs, opts.signal);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
throw lastError;
|
|
364
|
+
}
|
|
365
|
+
async function retryResult(fn, options = {}) {
|
|
366
|
+
try {
|
|
367
|
+
const value = await retry(fn, options);
|
|
368
|
+
return Ok(value);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
return Err(e instanceof Error ? e : new Error(String(e)));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function withRetry(fn, options = {}) {
|
|
374
|
+
return (...args) => retry(() => fn(...args), options);
|
|
375
|
+
}
|
|
376
|
+
var RetryConditions = {
|
|
377
|
+
networkErrors: (error) => {
|
|
378
|
+
const message = error.message.toLowerCase();
|
|
379
|
+
return message.includes("network") || message.includes("fetch") || message.includes("timeout") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("socket");
|
|
380
|
+
},
|
|
381
|
+
httpRetryable: (error) => {
|
|
382
|
+
const status = error.status;
|
|
383
|
+
if (!status)
|
|
384
|
+
return false;
|
|
385
|
+
return [408, 429, 500, 502, 503, 504].includes(status);
|
|
386
|
+
},
|
|
387
|
+
websocketErrors: (error) => {
|
|
388
|
+
const message = error.message.toLowerCase();
|
|
389
|
+
return message.includes("websocket") || message.includes("connection") || message.includes("close");
|
|
390
|
+
},
|
|
391
|
+
any: (...conditions) => (error) => conditions.some((c) => c(error)),
|
|
392
|
+
all: (...conditions) => (error) => conditions.every((c) => c(error))
|
|
393
|
+
};
|
|
394
|
+
var RetryPresets = {
|
|
395
|
+
aggressive: {
|
|
396
|
+
maxAttempts: 10,
|
|
397
|
+
delayMs: 500,
|
|
398
|
+
backoff: "exponential",
|
|
399
|
+
maxDelayMs: 60000,
|
|
400
|
+
jitter: 0.2
|
|
401
|
+
},
|
|
402
|
+
conservative: {
|
|
403
|
+
maxAttempts: 3,
|
|
404
|
+
delayMs: 2000,
|
|
405
|
+
backoff: "linear",
|
|
406
|
+
maxDelayMs: 1e4,
|
|
407
|
+
jitter: 0.1
|
|
408
|
+
},
|
|
409
|
+
fast: {
|
|
410
|
+
maxAttempts: 5,
|
|
411
|
+
delayMs: 100,
|
|
412
|
+
backoff: "exponential",
|
|
413
|
+
maxDelayMs: 2000,
|
|
414
|
+
jitter: 0
|
|
415
|
+
},
|
|
416
|
+
websocket: {
|
|
417
|
+
maxAttempts: 10,
|
|
418
|
+
delayMs: 1000,
|
|
419
|
+
backoff: "exponential",
|
|
420
|
+
maxDelayMs: 30000,
|
|
421
|
+
jitter: 0.1,
|
|
422
|
+
shouldRetry: RetryConditions.websocketErrors
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
13
426
|
// src/remote/client.ts
|
|
14
427
|
class RemoteEngine {
|
|
15
428
|
ws = null;
|
|
@@ -17,12 +430,17 @@ class RemoteEngine {
|
|
|
17
430
|
state = "disconnected";
|
|
18
431
|
options;
|
|
19
432
|
reconnectAttempts = 0;
|
|
20
|
-
|
|
433
|
+
disposables = new DisposableStack;
|
|
434
|
+
reconnectDisposable = null;
|
|
435
|
+
currentSessionId = null;
|
|
436
|
+
sessionOptions;
|
|
21
437
|
patchCallbacks = [];
|
|
22
438
|
stateCallbacks = [];
|
|
23
439
|
connectionCallbacks = [];
|
|
24
440
|
disconnectionCallbacks = [];
|
|
25
441
|
errorCallbacks = [];
|
|
442
|
+
sessionEstablishedCallbacks = [];
|
|
443
|
+
sessionExpiredCallbacks = [];
|
|
26
444
|
currentState = null;
|
|
27
445
|
currentRevision = 0;
|
|
28
446
|
moduleName = "";
|
|
@@ -31,54 +449,85 @@ class RemoteEngine {
|
|
|
31
449
|
this.options = {
|
|
32
450
|
autoReconnect: options.autoReconnect ?? true,
|
|
33
451
|
reconnectInterval: options.reconnectInterval ?? 3000,
|
|
34
|
-
maxReconnectAttempts: options.maxReconnectAttempts ?? 10
|
|
452
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
453
|
+
session: options.session
|
|
35
454
|
};
|
|
455
|
+
this.sessionOptions = options.session;
|
|
456
|
+
if (options.session?.id) {
|
|
457
|
+
this.currentSessionId = options.session.id;
|
|
458
|
+
}
|
|
36
459
|
}
|
|
37
460
|
async connect() {
|
|
38
461
|
if (this.state === "connected" || this.state === "connecting") {
|
|
39
|
-
return;
|
|
462
|
+
return Ok(undefined);
|
|
40
463
|
}
|
|
41
464
|
this.state = "connecting";
|
|
42
|
-
return new Promise((resolve
|
|
465
|
+
return new Promise((resolve) => {
|
|
43
466
|
try {
|
|
44
467
|
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) => {
|
|
468
|
+
this.disposables.add(disposableWebSocket(this.ws));
|
|
469
|
+
const messageHandler = (event) => {
|
|
52
470
|
this.handleMessage(event.data);
|
|
53
471
|
};
|
|
54
|
-
this.ws
|
|
472
|
+
this.disposables.add(disposableListener(this.ws, "message", messageHandler));
|
|
473
|
+
const errorHandler = () => {
|
|
55
474
|
this.state = "error";
|
|
56
|
-
const error = new Error("WebSocket error");
|
|
475
|
+
const error = new ConnectionError(this.url, new Error("WebSocket error"));
|
|
57
476
|
this.errorCallbacks.forEach((cb) => cb(error));
|
|
58
|
-
|
|
477
|
+
resolve(Err(error));
|
|
59
478
|
};
|
|
60
|
-
this.ws
|
|
479
|
+
this.disposables.add(disposableListener(this.ws, "error", errorHandler));
|
|
480
|
+
const closeHandler = () => {
|
|
61
481
|
this.state = "disconnected";
|
|
62
482
|
this.disconnectionCallbacks.forEach((cb) => cb());
|
|
63
483
|
this.attemptReconnect();
|
|
64
484
|
};
|
|
65
|
-
|
|
485
|
+
this.disposables.add(disposableListener(this.ws, "close", closeHandler));
|
|
486
|
+
this.ws.onopen = () => {
|
|
487
|
+
this.state = "connected";
|
|
488
|
+
this.reconnectAttempts = 0;
|
|
489
|
+
if (this.reconnectDisposable) {
|
|
490
|
+
this.reconnectDisposable.dispose();
|
|
491
|
+
this.reconnectDisposable = null;
|
|
492
|
+
}
|
|
493
|
+
this.sendHello();
|
|
494
|
+
this.connectionCallbacks.forEach((cb) => cb());
|
|
495
|
+
resolve(Ok(undefined));
|
|
496
|
+
};
|
|
497
|
+
} catch (e) {
|
|
66
498
|
this.state = "error";
|
|
67
|
-
|
|
499
|
+
const error = new ConnectionError(this.url, e);
|
|
500
|
+
resolve(Err(error));
|
|
68
501
|
}
|
|
69
502
|
});
|
|
70
503
|
}
|
|
504
|
+
sendHello() {
|
|
505
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
506
|
+
return;
|
|
507
|
+
const hello = {
|
|
508
|
+
type: "hello",
|
|
509
|
+
sessionId: this.currentSessionId ?? this.sessionOptions?.id,
|
|
510
|
+
props: this.sessionOptions?.props
|
|
511
|
+
};
|
|
512
|
+
this.ws.send(JSON.stringify(hello));
|
|
513
|
+
}
|
|
71
514
|
disconnect() {
|
|
72
|
-
if (this.
|
|
73
|
-
|
|
74
|
-
this.
|
|
515
|
+
if (this.reconnectDisposable) {
|
|
516
|
+
this.reconnectDisposable.dispose();
|
|
517
|
+
this.reconnectDisposable = null;
|
|
75
518
|
}
|
|
76
519
|
if (this.ws) {
|
|
77
|
-
this.ws.
|
|
520
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
521
|
+
this.ws.close();
|
|
522
|
+
}
|
|
78
523
|
this.ws = null;
|
|
79
524
|
}
|
|
80
525
|
this.state = "disconnected";
|
|
81
526
|
}
|
|
527
|
+
dispose() {
|
|
528
|
+
this.disconnect();
|
|
529
|
+
this.disposables.dispose();
|
|
530
|
+
}
|
|
82
531
|
dispatchAction(action, payload) {
|
|
83
532
|
if (this.state !== "connected" || !this.ws) {
|
|
84
533
|
console.warn("Cannot dispatch action: not connected");
|
|
@@ -112,6 +561,14 @@ class RemoteEngine {
|
|
|
112
561
|
this.errorCallbacks.push(callback);
|
|
113
562
|
return this;
|
|
114
563
|
}
|
|
564
|
+
onSessionEstablished(callback) {
|
|
565
|
+
this.sessionEstablishedCallbacks.push(callback);
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
onSessionExpired(callback) {
|
|
569
|
+
this.sessionExpiredCallbacks.push(callback);
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
115
572
|
getConnectionState() {
|
|
116
573
|
return this.state;
|
|
117
574
|
}
|
|
@@ -121,10 +578,19 @@ class RemoteEngine {
|
|
|
121
578
|
getRevision() {
|
|
122
579
|
return this.currentRevision;
|
|
123
580
|
}
|
|
581
|
+
getSessionId() {
|
|
582
|
+
return this.currentSessionId;
|
|
583
|
+
}
|
|
124
584
|
handleMessage(data) {
|
|
125
585
|
try {
|
|
126
586
|
const message = JSON.parse(data);
|
|
127
587
|
switch (message.type) {
|
|
588
|
+
case "sessionAck":
|
|
589
|
+
this.handleSessionAck(message);
|
|
590
|
+
break;
|
|
591
|
+
case "sessionExpired":
|
|
592
|
+
this.handleSessionExpired(message);
|
|
593
|
+
break;
|
|
128
594
|
case "initialTree":
|
|
129
595
|
this.handleInitialTree(message);
|
|
130
596
|
break;
|
|
@@ -133,14 +599,28 @@ class RemoteEngine {
|
|
|
133
599
|
break;
|
|
134
600
|
case "stateUpdate":
|
|
135
601
|
this.currentState = message.state;
|
|
136
|
-
this.stateCallbacks.forEach((cb) => cb(
|
|
602
|
+
this.stateCallbacks.forEach((cb) => cb(this.currentState));
|
|
137
603
|
break;
|
|
138
604
|
}
|
|
139
|
-
} catch (
|
|
140
|
-
console.error("Error handling remote message:",
|
|
141
|
-
|
|
605
|
+
} catch (e) {
|
|
606
|
+
console.error("Error handling remote message:", e);
|
|
607
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
608
|
+
this.errorCallbacks.forEach((cb) => cb(error));
|
|
142
609
|
}
|
|
143
610
|
}
|
|
611
|
+
handleSessionAck(message) {
|
|
612
|
+
this.currentSessionId = message.sessionId;
|
|
613
|
+
const info = {
|
|
614
|
+
sessionId: message.sessionId,
|
|
615
|
+
isNew: message.isNew,
|
|
616
|
+
isRestored: message.isRestored
|
|
617
|
+
};
|
|
618
|
+
this.sessionEstablishedCallbacks.forEach((cb) => cb(info));
|
|
619
|
+
}
|
|
620
|
+
handleSessionExpired(message) {
|
|
621
|
+
this.currentSessionId = null;
|
|
622
|
+
this.sessionExpiredCallbacks.forEach((cb) => cb(message.reason));
|
|
623
|
+
}
|
|
144
624
|
handleInitialTree(message) {
|
|
145
625
|
this.moduleName = message.module;
|
|
146
626
|
this.currentState = message.state;
|
|
@@ -164,15 +644,25 @@ class RemoteEngine {
|
|
|
164
644
|
if (!this.options.autoReconnect) {
|
|
165
645
|
return;
|
|
166
646
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
647
|
+
this.reconnectDisposable = disposableTimeout(() => {
|
|
648
|
+
this.reconnectDisposable = null;
|
|
649
|
+
retry(async () => {
|
|
650
|
+
const result = await this.connect();
|
|
651
|
+
if (!result.ok) {
|
|
652
|
+
throw result.error;
|
|
653
|
+
}
|
|
654
|
+
}, {
|
|
655
|
+
maxAttempts: this.options.maxReconnectAttempts,
|
|
656
|
+
delayMs: this.options.reconnectInterval,
|
|
657
|
+
backoff: "exponential",
|
|
658
|
+
maxDelayMs: 30000,
|
|
659
|
+
jitter: 0.1,
|
|
660
|
+
onRetry: (attempt, error) => {
|
|
661
|
+
console.log(`Reconnection attempt ${attempt}/${this.options.maxReconnectAttempts} failed: ${error.message}`);
|
|
662
|
+
}
|
|
663
|
+
}).catch((error) => {
|
|
664
|
+
console.error("Max reconnection attempts reached:", error.message);
|
|
665
|
+
this.errorCallbacks.forEach((cb) => cb(new ConnectionError(this.url, error, this.options.maxReconnectAttempts)));
|
|
176
666
|
});
|
|
177
667
|
}, this.options.reconnectInterval);
|
|
178
668
|
}
|
|
@@ -181,4 +671,4 @@ export {
|
|
|
181
671
|
RemoteEngine
|
|
182
672
|
};
|
|
183
673
|
|
|
184
|
-
//# debugId=
|
|
674
|
+
//# debugId=B73E531E82606EFA64756E2164756E21
|