@forgeax/engine-host 0.1.27
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/LICENSE +202 -0
- package/README.md +32 -0
- package/dist/__tests__/host.test.d.ts +2 -0
- package/dist/__tests__/host.test.d.ts.map +1 -0
- package/dist/backend.d.ts +46 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.mjs +797 -0
- package/dist/backend.mjs.map +1 -0
- package/dist/frontend.d.ts +50 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.mjs +715 -0
- package/dist/frontend.mjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1473 -0
- package/dist/index.mjs.map +1 -0
- package/dist/protocol.d.ts +153 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.mjs +351 -0
- package/dist/protocol.mjs.map +1 -0
- package/dist/transport.d.ts +64 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.mjs +575 -0
- package/dist/transport.mjs.map +1 -0
- package/package.json +88 -0
- package/src/__tests__/host.test.ts +510 -0
- package/src/backend.ts +342 -0
- package/src/frontend.ts +508 -0
- package/src/index.ts +54 -0
- package/src/protocol.ts +586 -0
- package/src/transport.ts +717 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// src/protocol.ts
|
|
2
|
+
var HostAssemblyError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
expected;
|
|
5
|
+
hint;
|
|
6
|
+
detail;
|
|
7
|
+
constructor(code, expected, hint, detail) {
|
|
8
|
+
super(`${code}: ${expected}`);
|
|
9
|
+
this.name = "HostAssemblyError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.expected = expected;
|
|
12
|
+
this.hint = hint;
|
|
13
|
+
this.detail = detail;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/transport.ts
|
|
18
|
+
var HOST_ASSEMBLY_SERVICE = "host/assembly.get";
|
|
19
|
+
var HOST_ACTIVATION_REPORT_SERVICE = "host/assembly.report";
|
|
20
|
+
var HOST_ASSEMBLY_CHANGED_TOPIC = "host/assembly.changed";
|
|
21
|
+
function createHostTransport() {
|
|
22
|
+
const services = /* @__PURE__ */ new Map();
|
|
23
|
+
const generations = /* @__PURE__ */ new Map();
|
|
24
|
+
const clients = /* @__PURE__ */ new Set();
|
|
25
|
+
let closed = false;
|
|
26
|
+
const closeClient = (state, reason) => {
|
|
27
|
+
if (!state.connected) return;
|
|
28
|
+
state.connected = false;
|
|
29
|
+
for (const controller of state.pending) controller.abort();
|
|
30
|
+
state.pending.clear();
|
|
31
|
+
for (const callbacks of state.subscriptions.values()) callbacks.clear();
|
|
32
|
+
state.subscriptions.clear();
|
|
33
|
+
clients.delete(state);
|
|
34
|
+
const error = new HostAssemblyError(
|
|
35
|
+
"host-transport-failure",
|
|
36
|
+
"the host connection to remain available",
|
|
37
|
+
"Reconnect the frontend host before issuing another request.",
|
|
38
|
+
{ service: "host/socket", reason: String(reason ?? "host connection closed") }
|
|
39
|
+
);
|
|
40
|
+
for (const listener of state.disconnectListeners) listener(error);
|
|
41
|
+
state.disconnectListeners.clear();
|
|
42
|
+
};
|
|
43
|
+
const server = {
|
|
44
|
+
get connectedClients() {
|
|
45
|
+
return clients.size;
|
|
46
|
+
},
|
|
47
|
+
register(service, handler) {
|
|
48
|
+
if (closed) throw new Error("host transport is closed");
|
|
49
|
+
const previous = services.get(service);
|
|
50
|
+
const generation = (generations.get(service) ?? 0) + 1;
|
|
51
|
+
generations.set(service, generation);
|
|
52
|
+
for (const controller of previous?.pending ?? []) controller.abort();
|
|
53
|
+
const identity = Symbol(service);
|
|
54
|
+
services.set(service, {
|
|
55
|
+
identity,
|
|
56
|
+
generation,
|
|
57
|
+
handler,
|
|
58
|
+
pending: /* @__PURE__ */ new Set()
|
|
59
|
+
});
|
|
60
|
+
return () => {
|
|
61
|
+
const current = services.get(service);
|
|
62
|
+
if (current?.identity !== identity) return;
|
|
63
|
+
for (const controller of current.pending) controller.abort();
|
|
64
|
+
generations.set(service, current.generation + 1);
|
|
65
|
+
services.delete(service);
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
invalidate(service) {
|
|
69
|
+
const current = services.get(service);
|
|
70
|
+
if (current === void 0) return;
|
|
71
|
+
for (const controller of current.pending) controller.abort();
|
|
72
|
+
generations.set(service, current.generation + 1);
|
|
73
|
+
services.delete(service);
|
|
74
|
+
},
|
|
75
|
+
snapshot(service) {
|
|
76
|
+
const current = services.get(service);
|
|
77
|
+
return current === void 0 ? void 0 : { service, generation: current.generation };
|
|
78
|
+
},
|
|
79
|
+
publish(topic, payload) {
|
|
80
|
+
for (const state of clients) {
|
|
81
|
+
for (const listener of state.subscriptions.get(topic) ?? []) listener(payload);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
connect() {
|
|
85
|
+
if (closed) throw new Error("host transport is closed");
|
|
86
|
+
const state = {
|
|
87
|
+
connected: true,
|
|
88
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
89
|
+
pending: /* @__PURE__ */ new Set(),
|
|
90
|
+
disconnectListeners: /* @__PURE__ */ new Set()
|
|
91
|
+
};
|
|
92
|
+
clients.add(state);
|
|
93
|
+
const client = {
|
|
94
|
+
get connected() {
|
|
95
|
+
return state.connected;
|
|
96
|
+
},
|
|
97
|
+
get generation() {
|
|
98
|
+
let current = 0;
|
|
99
|
+
for (const service of services.values()) current = Math.max(current, service.generation);
|
|
100
|
+
return current;
|
|
101
|
+
},
|
|
102
|
+
async request(service, payload, options = {}) {
|
|
103
|
+
if (!state.connected) {
|
|
104
|
+
throw new HostAssemblyError(
|
|
105
|
+
"host-assembly-service-unavailable",
|
|
106
|
+
`service ${service} to be available on the current connection`,
|
|
107
|
+
"Reconnect the frontend host before issuing another request.",
|
|
108
|
+
{ service }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (options.signal?.aborted) {
|
|
112
|
+
throw new HostAssemblyError(
|
|
113
|
+
"host-assembly-request-aborted",
|
|
114
|
+
`request ${service} to start with a live AbortSignal`,
|
|
115
|
+
"Start a fresh request with a live AbortSignal.",
|
|
116
|
+
{ service }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const record = services.get(service);
|
|
120
|
+
if (record === void 0) {
|
|
121
|
+
const invalidatedGeneration = generations.get(service);
|
|
122
|
+
if (options.generation !== void 0 && invalidatedGeneration !== void 0 && options.generation < invalidatedGeneration) {
|
|
123
|
+
throw new HostAssemblyError(
|
|
124
|
+
"host-assembly-stale-request",
|
|
125
|
+
`request generation ${options.generation} to match the invalidated service ${service}`,
|
|
126
|
+
"Refresh the client capability after the backend service is enabled again.",
|
|
127
|
+
{ service, generation: options.generation }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
throw new HostAssemblyError(
|
|
131
|
+
"host-assembly-service-unavailable",
|
|
132
|
+
`service ${service} to be registered`,
|
|
133
|
+
"Wait for the backend plugin to activate before using this capability.",
|
|
134
|
+
{ service }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const generation = options.generation ?? record.generation;
|
|
138
|
+
if (generation !== record.generation) {
|
|
139
|
+
throw new HostAssemblyError(
|
|
140
|
+
"host-assembly-stale-request",
|
|
141
|
+
`request generation ${generation} to match service ${service}`,
|
|
142
|
+
"Refresh the client capability and retry only when the owning business contract permits it.",
|
|
143
|
+
{ service, generation }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const abortFromCaller = () => controller.abort();
|
|
148
|
+
options.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
149
|
+
record.pending.add(controller);
|
|
150
|
+
state.pending.add(controller);
|
|
151
|
+
let removeAbortRequest;
|
|
152
|
+
try {
|
|
153
|
+
if (controller.signal.aborted) {
|
|
154
|
+
throw new HostAssemblyError(
|
|
155
|
+
"host-assembly-request-aborted",
|
|
156
|
+
`request ${service} not to be aborted before execution`,
|
|
157
|
+
"Start a fresh request with a live AbortSignal.",
|
|
158
|
+
{ service }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
let abortReject;
|
|
162
|
+
const aborted = new Promise((_, reject) => {
|
|
163
|
+
abortReject = reject;
|
|
164
|
+
});
|
|
165
|
+
const abortRequest = () => {
|
|
166
|
+
abortReject?.(
|
|
167
|
+
new HostAssemblyError(
|
|
168
|
+
"host-assembly-request-aborted",
|
|
169
|
+
`request ${service} to finish before its service is closed or invalidated`,
|
|
170
|
+
"Treat the capability as withdrawn and refresh the service before retrying.",
|
|
171
|
+
{ service }
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
controller.signal.addEventListener("abort", abortRequest, { once: true });
|
|
176
|
+
removeAbortRequest = () => controller.signal.removeEventListener("abort", abortRequest);
|
|
177
|
+
const result = await Promise.race([
|
|
178
|
+
Promise.resolve(
|
|
179
|
+
record.handler({
|
|
180
|
+
service,
|
|
181
|
+
payload,
|
|
182
|
+
generation,
|
|
183
|
+
signal: controller.signal
|
|
184
|
+
})
|
|
185
|
+
),
|
|
186
|
+
aborted
|
|
187
|
+
]);
|
|
188
|
+
if (controller.signal.aborted) {
|
|
189
|
+
throw new HostAssemblyError(
|
|
190
|
+
"host-assembly-request-aborted",
|
|
191
|
+
`request ${service} to finish before its service is invalidated`,
|
|
192
|
+
"Treat the capability as withdrawn and refresh the service before retrying.",
|
|
193
|
+
{ service }
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
} finally {
|
|
198
|
+
removeAbortRequest?.();
|
|
199
|
+
record.pending.delete(controller);
|
|
200
|
+
state.pending.delete(controller);
|
|
201
|
+
options.signal?.removeEventListener("abort", abortFromCaller);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
subscribe(topic, listener) {
|
|
205
|
+
if (!state.connected) return () => {
|
|
206
|
+
};
|
|
207
|
+
const callbacks = state.subscriptions.get(topic) ?? /* @__PURE__ */ new Set();
|
|
208
|
+
callbacks.add(listener);
|
|
209
|
+
state.subscriptions.set(topic, callbacks);
|
|
210
|
+
return () => {
|
|
211
|
+
callbacks.delete(listener);
|
|
212
|
+
if (callbacks.size === 0) state.subscriptions.delete(topic);
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
onDisconnect(listener) {
|
|
216
|
+
if (!state.connected) {
|
|
217
|
+
listener(
|
|
218
|
+
new HostAssemblyError(
|
|
219
|
+
"host-transport-failure",
|
|
220
|
+
"the host connection to remain available",
|
|
221
|
+
"Reconnect the frontend host before issuing another request.",
|
|
222
|
+
{ service: "host/socket", reason: "host connection is already closed" }
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
return () => {
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
state.disconnectListeners.add(listener);
|
|
229
|
+
return () => state.disconnectListeners.delete(listener);
|
|
230
|
+
},
|
|
231
|
+
close(reason) {
|
|
232
|
+
closeClient(state, reason);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
return client;
|
|
236
|
+
},
|
|
237
|
+
close(reason) {
|
|
238
|
+
if (closed) return;
|
|
239
|
+
closed = true;
|
|
240
|
+
for (const service of services.values())
|
|
241
|
+
for (const controller of service.pending) controller.abort();
|
|
242
|
+
services.clear();
|
|
243
|
+
for (const client of [...clients]) closeClient(client, reason);
|
|
244
|
+
generations.clear();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
return server;
|
|
248
|
+
}
|
|
249
|
+
function addSocketListener(socket, type, listener) {
|
|
250
|
+
if (socket.addEventListener !== void 0) {
|
|
251
|
+
socket.addEventListener(type, listener);
|
|
252
|
+
return () => socket.removeEventListener?.(type, listener);
|
|
253
|
+
}
|
|
254
|
+
socket.on?.(type, listener);
|
|
255
|
+
return () => socket.off?.(type, listener);
|
|
256
|
+
}
|
|
257
|
+
function socketPayload(value) {
|
|
258
|
+
const data = Array.isArray(value) ? value[0] : value;
|
|
259
|
+
if (typeof data === "string") return data;
|
|
260
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
261
|
+
if (data instanceof Uint8Array) return new TextDecoder().decode(data);
|
|
262
|
+
if (data !== null && typeof data === "object" && "data" in data)
|
|
263
|
+
return socketPayload(data.data);
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
function parseWire(value) {
|
|
267
|
+
const source = socketPayload(value);
|
|
268
|
+
if (source === void 0) return void 0;
|
|
269
|
+
try {
|
|
270
|
+
const parsed = JSON.parse(source);
|
|
271
|
+
return parsed !== null && typeof parsed === "object" && typeof parsed.kind === "string" ? parsed : void 0;
|
|
272
|
+
} catch {
|
|
273
|
+
return void 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function serializeError(service, error) {
|
|
277
|
+
if (error !== null && typeof error === "object" && typeof error.code === "string" && typeof error.expected === "string" && typeof error.hint === "string") {
|
|
278
|
+
const detail = error.detail;
|
|
279
|
+
return {
|
|
280
|
+
code: error.code,
|
|
281
|
+
expected: error.expected,
|
|
282
|
+
hint: error.hint,
|
|
283
|
+
detail: detail !== null && typeof detail === "object" ? detail : { reason: String(detail ?? "unknown failure") }
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
code: "host-transport-failure",
|
|
288
|
+
expected: `service ${service} to complete without an exception`,
|
|
289
|
+
hint: "Inspect the backend host process and reconnect before retrying.",
|
|
290
|
+
detail: { service, reason: error instanceof Error ? error.message : String(error) }
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function errorFromSummary(service, summary) {
|
|
294
|
+
const detail = summary.detail;
|
|
295
|
+
const supported = /* @__PURE__ */ new Set([
|
|
296
|
+
"host-assembly-invalid",
|
|
297
|
+
"host-assembly-revision-mismatch",
|
|
298
|
+
"host-assembly-module-missing",
|
|
299
|
+
"host-assembly-module-version-mismatch",
|
|
300
|
+
"host-assembly-reload-required",
|
|
301
|
+
"host-assembly-service-unavailable",
|
|
302
|
+
"host-assembly-stale-request",
|
|
303
|
+
"host-assembly-request-aborted",
|
|
304
|
+
"host-assembly-not-ready",
|
|
305
|
+
"host-transport-failure"
|
|
306
|
+
]);
|
|
307
|
+
if (supported.has(summary.code)) {
|
|
308
|
+
return new HostAssemblyError(
|
|
309
|
+
summary.code,
|
|
310
|
+
summary.expected,
|
|
311
|
+
summary.hint,
|
|
312
|
+
detail
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return new HostAssemblyError("host-transport-failure", summary.expected, summary.hint, {
|
|
316
|
+
service,
|
|
317
|
+
reason: summary.detail.reason ? String(summary.detail.reason) : summary.code
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function transportFailure(service, reason) {
|
|
321
|
+
return new HostAssemblyError(
|
|
322
|
+
"host-transport-failure",
|
|
323
|
+
`service ${service} to remain connected`,
|
|
324
|
+
"Reconnect the frontend host and inspect the backend transport diagnostics.",
|
|
325
|
+
{ service, reason }
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
function sendWire(socket, message) {
|
|
329
|
+
socket.send(JSON.stringify(message));
|
|
330
|
+
}
|
|
331
|
+
async function createHostWebSocketClient(socket) {
|
|
332
|
+
if (socket.readyState !== void 0 && socket.readyState !== 1) {
|
|
333
|
+
await new Promise((resolve, reject) => {
|
|
334
|
+
const removeOpen = addSocketListener(socket, "open", () => {
|
|
335
|
+
removeOpen();
|
|
336
|
+
removeError2();
|
|
337
|
+
resolve();
|
|
338
|
+
});
|
|
339
|
+
const removeError2 = addSocketListener(socket, "error", (cause) => {
|
|
340
|
+
removeOpen();
|
|
341
|
+
removeError2();
|
|
342
|
+
reject(transportFailure("host/connect", String(cause ?? "socket error")));
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const pending = /* @__PURE__ */ new Map();
|
|
347
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
348
|
+
const disconnectListeners = /* @__PURE__ */ new Set();
|
|
349
|
+
let connected = true;
|
|
350
|
+
let generation = 0;
|
|
351
|
+
let sequence = 0;
|
|
352
|
+
const disconnectError = (reason) => transportFailure("host/socket", reason instanceof Error ? reason.message : String(reason));
|
|
353
|
+
const failPending = (reason) => {
|
|
354
|
+
if (!connected) return;
|
|
355
|
+
connected = false;
|
|
356
|
+
for (const request of pending.values()) {
|
|
357
|
+
request.cleanup();
|
|
358
|
+
request.reject(reason);
|
|
359
|
+
}
|
|
360
|
+
pending.clear();
|
|
361
|
+
subscriptions.clear();
|
|
362
|
+
const error = disconnectError(reason);
|
|
363
|
+
for (const listener of disconnectListeners) listener(error);
|
|
364
|
+
disconnectListeners.clear();
|
|
365
|
+
};
|
|
366
|
+
const removeMessage = addSocketListener(socket, "message", (event) => {
|
|
367
|
+
const message = parseWire(event);
|
|
368
|
+
if (message === void 0) return;
|
|
369
|
+
if (message.kind === "event") {
|
|
370
|
+
for (const listener of subscriptions.get(message.topic) ?? []) listener(message.payload);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (message.kind !== "response") return;
|
|
374
|
+
if (message.generation !== void 0) generation = Math.max(generation, message.generation);
|
|
375
|
+
const request = pending.get(message.id);
|
|
376
|
+
if (request === void 0) return;
|
|
377
|
+
pending.delete(message.id);
|
|
378
|
+
request.cleanup();
|
|
379
|
+
if (message.ok) request.resolve(message.value);
|
|
380
|
+
else
|
|
381
|
+
request.reject(
|
|
382
|
+
errorFromSummary(
|
|
383
|
+
"host/socket",
|
|
384
|
+
message.error ?? serializeError("host/socket", "unknown response failure")
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
const removeClose = addSocketListener(socket, "close", (reason) => {
|
|
389
|
+
removeMessage();
|
|
390
|
+
removeClose();
|
|
391
|
+
removeError();
|
|
392
|
+
failPending(disconnectError(reason));
|
|
393
|
+
});
|
|
394
|
+
const removeError = addSocketListener(socket, "error", (reason) => {
|
|
395
|
+
failPending(disconnectError(reason));
|
|
396
|
+
});
|
|
397
|
+
const client = {
|
|
398
|
+
get connected() {
|
|
399
|
+
return connected;
|
|
400
|
+
},
|
|
401
|
+
get generation() {
|
|
402
|
+
return generation;
|
|
403
|
+
},
|
|
404
|
+
request(service, payload, options = {}) {
|
|
405
|
+
if (!connected) return Promise.reject(disconnectError("socket is closed"));
|
|
406
|
+
if (options.signal?.aborted)
|
|
407
|
+
return Promise.reject(
|
|
408
|
+
new HostAssemblyError(
|
|
409
|
+
"host-assembly-request-aborted",
|
|
410
|
+
`request ${service} to start with a live AbortSignal`,
|
|
411
|
+
"Start a fresh request with a live AbortSignal.",
|
|
412
|
+
{ service }
|
|
413
|
+
)
|
|
414
|
+
);
|
|
415
|
+
sequence += 1;
|
|
416
|
+
const id = `${Date.now().toString(36)}-${sequence.toString(36)}`;
|
|
417
|
+
return new Promise((resolve, reject) => {
|
|
418
|
+
const abort = () => {
|
|
419
|
+
const request = pending.get(id);
|
|
420
|
+
if (request === void 0) return;
|
|
421
|
+
pending.delete(id);
|
|
422
|
+
request.cleanup();
|
|
423
|
+
try {
|
|
424
|
+
sendWire(socket, { kind: "cancel", id });
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
reject(
|
|
428
|
+
new HostAssemblyError(
|
|
429
|
+
"host-assembly-request-aborted",
|
|
430
|
+
`request ${service} to finish before cancellation`,
|
|
431
|
+
"Start a fresh request with a live AbortSignal.",
|
|
432
|
+
{ service }
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
};
|
|
436
|
+
const cleanup = () => options.signal?.removeEventListener("abort", abort);
|
|
437
|
+
pending.set(id, {
|
|
438
|
+
resolve,
|
|
439
|
+
reject,
|
|
440
|
+
cleanup
|
|
441
|
+
});
|
|
442
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
443
|
+
if (options.signal?.aborted) {
|
|
444
|
+
abort();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
sendWire(socket, {
|
|
449
|
+
kind: "request",
|
|
450
|
+
id,
|
|
451
|
+
service,
|
|
452
|
+
payload,
|
|
453
|
+
...options.generation === void 0 ? {} : { generation: options.generation }
|
|
454
|
+
});
|
|
455
|
+
} catch (cause) {
|
|
456
|
+
const request = pending.get(id);
|
|
457
|
+
request?.cleanup();
|
|
458
|
+
pending.delete(id);
|
|
459
|
+
reject(disconnectError(cause));
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
subscribe(topic, listener) {
|
|
464
|
+
if (!connected) return () => {
|
|
465
|
+
};
|
|
466
|
+
const listeners = subscriptions.get(topic) ?? /* @__PURE__ */ new Set();
|
|
467
|
+
listeners.add(listener);
|
|
468
|
+
subscriptions.set(topic, listeners);
|
|
469
|
+
sendWire(socket, { kind: "subscribe", topic });
|
|
470
|
+
return () => {
|
|
471
|
+
listeners.delete(listener);
|
|
472
|
+
if (listeners.size !== 0) return;
|
|
473
|
+
subscriptions.delete(topic);
|
|
474
|
+
sendWire(socket, { kind: "unsubscribe", topic });
|
|
475
|
+
};
|
|
476
|
+
},
|
|
477
|
+
onDisconnect(listener) {
|
|
478
|
+
if (!connected) {
|
|
479
|
+
listener(disconnectError("socket is already closed"));
|
|
480
|
+
return () => {
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
disconnectListeners.add(listener);
|
|
484
|
+
return () => disconnectListeners.delete(listener);
|
|
485
|
+
},
|
|
486
|
+
close(reason) {
|
|
487
|
+
if (!connected) return;
|
|
488
|
+
failPending(disconnectError(reason ?? "client closed the host connection"));
|
|
489
|
+
removeMessage();
|
|
490
|
+
removeClose();
|
|
491
|
+
removeError();
|
|
492
|
+
socket.close();
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
return client;
|
|
496
|
+
}
|
|
497
|
+
async function connectHostWebSocket(url) {
|
|
498
|
+
const Constructor = globalThis.WebSocket;
|
|
499
|
+
if (typeof Constructor !== "function")
|
|
500
|
+
throw transportFailure("host/connect", "the current runtime does not provide WebSocket");
|
|
501
|
+
return createHostWebSocketClient(new Constructor(url));
|
|
502
|
+
}
|
|
503
|
+
function attachHostWebSocketServer(socket, server) {
|
|
504
|
+
const client = server.connect();
|
|
505
|
+
const pending = /* @__PURE__ */ new Map();
|
|
506
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
507
|
+
let disposed = false;
|
|
508
|
+
const removeMessage = addSocketListener(socket, "message", (event) => {
|
|
509
|
+
const message = parseWire(event);
|
|
510
|
+
if (message === void 0 || disposed) return;
|
|
511
|
+
if (message.kind === "cancel") {
|
|
512
|
+
pending.get(message.id)?.abort();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (message.kind === "subscribe") {
|
|
516
|
+
subscriptions.get(message.topic)?.();
|
|
517
|
+
subscriptions.set(
|
|
518
|
+
message.topic,
|
|
519
|
+
client.subscribe(message.topic, (payload) => {
|
|
520
|
+
if (!disposed) sendWire(socket, { kind: "event", topic: message.topic, payload });
|
|
521
|
+
})
|
|
522
|
+
);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (message.kind === "unsubscribe") {
|
|
526
|
+
subscriptions.get(message.topic)?.();
|
|
527
|
+
subscriptions.delete(message.topic);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (message.kind !== "request") return;
|
|
531
|
+
const controller = new AbortController();
|
|
532
|
+
pending.set(message.id, controller);
|
|
533
|
+
void client.request(message.service, message.payload, {
|
|
534
|
+
signal: controller.signal,
|
|
535
|
+
...message.generation === void 0 ? {} : { generation: message.generation }
|
|
536
|
+
}).then((value) => {
|
|
537
|
+
if (!disposed)
|
|
538
|
+
sendWire(socket, {
|
|
539
|
+
kind: "response",
|
|
540
|
+
id: message.id,
|
|
541
|
+
ok: true,
|
|
542
|
+
value,
|
|
543
|
+
generation: client.generation
|
|
544
|
+
});
|
|
545
|
+
}).catch((error) => {
|
|
546
|
+
if (!disposed)
|
|
547
|
+
sendWire(socket, {
|
|
548
|
+
kind: "response",
|
|
549
|
+
id: message.id,
|
|
550
|
+
ok: false,
|
|
551
|
+
error: serializeError(message.service, error),
|
|
552
|
+
generation: client.generation
|
|
553
|
+
});
|
|
554
|
+
}).finally(() => pending.delete(message.id));
|
|
555
|
+
});
|
|
556
|
+
const removeClose = addSocketListener(socket, "close", () => dispose());
|
|
557
|
+
const removeError = addSocketListener(socket, "error", () => dispose());
|
|
558
|
+
function dispose() {
|
|
559
|
+
if (disposed) return;
|
|
560
|
+
disposed = true;
|
|
561
|
+
removeMessage();
|
|
562
|
+
removeClose();
|
|
563
|
+
removeError();
|
|
564
|
+
for (const controller of pending.values()) controller.abort();
|
|
565
|
+
pending.clear();
|
|
566
|
+
for (const unsubscribe of subscriptions.values()) unsubscribe();
|
|
567
|
+
subscriptions.clear();
|
|
568
|
+
client.close("host socket disconnected");
|
|
569
|
+
}
|
|
570
|
+
return dispose;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export { HOST_ACTIVATION_REPORT_SERVICE, HOST_ASSEMBLY_CHANGED_TOPIC, HOST_ASSEMBLY_SERVICE, attachHostWebSocketServer, connectHostWebSocket, createHostTransport, createHostWebSocketClient };
|
|
574
|
+
//# sourceMappingURL=transport.mjs.map
|
|
575
|
+
//# sourceMappingURL=transport.mjs.map
|