webloom-framework 0.3.0 → 0.4.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 +70 -42
- package/dist/advanced.d.ts +168 -0
- package/dist/advanced.js +441 -0
- package/dist/advanced.js.map +1 -0
- package/dist/chunk-4DSINPZD.js +158 -0
- package/dist/chunk-4DSINPZD.js.map +1 -0
- package/dist/chunk-JHJZIO2H.js +4018 -0
- package/dist/chunk-JHJZIO2H.js.map +1 -0
- package/dist/chunk-RAQOFUZY.js +1737 -0
- package/dist/chunk-RAQOFUZY.js.map +1 -0
- package/dist/chunk-YOIH6H36.js +433 -0
- package/dist/chunk-YOIH6H36.js.map +1 -0
- package/dist/index.d.ts +39 -259
- package/dist/index.js +38 -1359
- package/dist/index.js.map +1 -1
- package/dist/messageBus-CtrwkjrO.d.ts +5 -0
- package/dist/messagePortServiceTransport-B0FyJr43.d.ts +264 -0
- package/dist/react.d.ts +41 -28
- package/dist/react.js +79 -88
- package/dist/react.js.map +1 -1
- package/dist/runtimeTypes-DquUCHz-.d.ts +1640 -0
- package/dist/sharedWorkerHost-Cb2a1_KD.d.ts +190 -0
- package/dist/testing.d.ts +18 -17
- package/dist/testing.js +23 -15
- package/dist/testing.js.map +1 -1
- package/dist/windowRuntime-B6Ue8jso.d.ts +23 -0
- package/docs/api.md +148 -111
- package/docs/proposals/webloom-v4/implementation-plan.md +443 -0
- package/docs/proposals/webloom-v4/requirements.md +555 -0
- package/docs/proposals/webloom-v4/verification.md +84 -0
- package/package.json +9 -2
- package/dist/chunk-76BGPI6M.js +0 -4261
- package/dist/chunk-76BGPI6M.js.map +0 -1
- package/dist/createPluginHost-BVsUCDKN.d.ts +0 -1318
- package/dist/resourceRegistry-BFnjmeGE.d.ts +0 -267
|
@@ -0,0 +1,1737 @@
|
|
|
1
|
+
import { createRuntimeMessageCodec, normalizeRuntimeLimits, createRuntimeBudget, WebLoomError, validateTransferList, DEFAULT_RUNTIME_LIMITS, capabilityKey, createReceivePortLedger, RUNTIME_CALL_TYPE, RUNTIME_CANCEL_TYPE, RUNTIME_CREDIT_TYPE, RUNTIME_SNAPSHOT_TYPE, RUNTIME_ERROR_TYPE, RUNTIME_RESULT_TYPE, RUNTIME_ERROR_MESSAGE_TYPE, RUNTIME_NEXT_TYPE, RUNTIME_PROTOCOL_VERSION, validateDto, cloneFrozenAttributes, hostForWindowApp, RuntimeUnavailableError, validateRawDto, validateTransferListWithStats, assertReceivedPortSet } from './chunk-JHJZIO2H.js';
|
|
2
|
+
|
|
3
|
+
// src/transport/serviceBridge.ts
|
|
4
|
+
function id(prefix) {
|
|
5
|
+
try {
|
|
6
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `${prefix}:${crypto.randomUUID()}`;
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
return `${prefix}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
}
|
|
11
|
+
function timeoutMs(value, fallback) {
|
|
12
|
+
const result = value ?? fallback;
|
|
13
|
+
if (!Number.isFinite(result) || result < 1 || result > 3e5) throw new TypeError("timeoutMs must be a finite number from 1 to 300000");
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
function mergeSignals(...signals) {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const active = signals.filter((signal) => signal !== void 0);
|
|
19
|
+
const listeners = active.map((signal) => {
|
|
20
|
+
const listener = () => {
|
|
21
|
+
try {
|
|
22
|
+
controller.abort(signal.reason);
|
|
23
|
+
} catch {
|
|
24
|
+
controller.abort();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (signal.aborted) listener();
|
|
28
|
+
else signal.addEventListener("abort", listener, { once: true });
|
|
29
|
+
return { signal, listener };
|
|
30
|
+
});
|
|
31
|
+
return { signal: controller.signal, dispose() {
|
|
32
|
+
for (const item of listeners) item.signal.removeEventListener("abort", item.listener);
|
|
33
|
+
} };
|
|
34
|
+
}
|
|
35
|
+
function frameworkError(code, phase, context) {
|
|
36
|
+
const text = {
|
|
37
|
+
protocol_mismatch: "Remote Runtime protocol is not supported",
|
|
38
|
+
capability_unavailable: "Capability is unavailable",
|
|
39
|
+
request_validation_failed: "Capability request failed validation",
|
|
40
|
+
response_validation_failed: "Capability response failed validation",
|
|
41
|
+
request_clone_failed: "Capability request could not be delivered",
|
|
42
|
+
response_clone_failed: "Capability response could not be delivered",
|
|
43
|
+
transfer_invalid: "Capability transfer declaration is invalid",
|
|
44
|
+
handler_failed: "Remote capability operation failed",
|
|
45
|
+
call_timeout: "Capability call timed out; remote side effect is unknown",
|
|
46
|
+
request_cancelled: "Capability call was cancelled",
|
|
47
|
+
service_revoked: "Capability exposure was revoked",
|
|
48
|
+
service_stale: "Capability exposure is stale",
|
|
49
|
+
transport_unavailable: "Runtime transport is unavailable",
|
|
50
|
+
stream_overflow: "Stream credit or sequence is invalid",
|
|
51
|
+
resource_limit_exceeded: "Runtime resource limit exceeded",
|
|
52
|
+
invalid_snapshot: "Invalid Runtime snapshot",
|
|
53
|
+
invalid_message: "Invalid WebLoom runtime message"
|
|
54
|
+
};
|
|
55
|
+
return new WebLoomError(code, text[String(code)] ?? "WebLoom operation failed", phase, context);
|
|
56
|
+
}
|
|
57
|
+
function errorFromWire(message) {
|
|
58
|
+
return frameworkError(message.error.code, message.error.phase, { serviceInstanceId: message.serviceInstanceId });
|
|
59
|
+
}
|
|
60
|
+
function validCredit(value, maximum) {
|
|
61
|
+
return Number.isSafeInteger(value) && value >= 1 && value <= maximum;
|
|
62
|
+
}
|
|
63
|
+
function serviceMatches(reference, capability) {
|
|
64
|
+
return reference.kind === capability.kind && reference.capabilityId === capability.id && reference.contractVersion === capability.version;
|
|
65
|
+
}
|
|
66
|
+
function serviceKey(service) {
|
|
67
|
+
return `${service.kind}\0${service.capabilityId}\0${service.contractVersion}`;
|
|
68
|
+
}
|
|
69
|
+
function contextFor(capability, reference) {
|
|
70
|
+
return { capabilityId: capability.id, ...reference ? { serviceInstanceId: reference.serviceInstanceId } : {} };
|
|
71
|
+
}
|
|
72
|
+
function createCapabilityBridge(options) {
|
|
73
|
+
const codec = createRuntimeMessageCodec();
|
|
74
|
+
const limits = normalizeRuntimeLimits(options.limits);
|
|
75
|
+
const budget = options.budget ?? createRuntimeBudget(limits);
|
|
76
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
77
|
+
const pending = /* @__PURE__ */ new Map();
|
|
78
|
+
const streams = /* @__PURE__ */ new Map();
|
|
79
|
+
const waitingCalls = /* @__PURE__ */ new Set();
|
|
80
|
+
const callbackExecutions = /* @__PURE__ */ new Set();
|
|
81
|
+
const proxies = /* @__PURE__ */ new Map();
|
|
82
|
+
const clients = /* @__PURE__ */ new Map();
|
|
83
|
+
let pendingCount = 0;
|
|
84
|
+
let peerRetainedPayloadBytes = 0;
|
|
85
|
+
let currentState = "empty";
|
|
86
|
+
let terminalFailure;
|
|
87
|
+
let remoteRuntimeId;
|
|
88
|
+
let remoteRuntimeKind;
|
|
89
|
+
let remoteRuntimeInstanceId;
|
|
90
|
+
let revision = -1;
|
|
91
|
+
let appliedSnapshotFingerprint;
|
|
92
|
+
let currentServices = [];
|
|
93
|
+
let disposed = false;
|
|
94
|
+
let reservedStreamCount = 0;
|
|
95
|
+
const defaultTimeout = timeoutMs(options.defaultCallTimeoutMs, 3e4);
|
|
96
|
+
const emit = () => {
|
|
97
|
+
for (const listener of [...listeners]) {
|
|
98
|
+
try {
|
|
99
|
+
listener();
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const reserve = (payloadBytes, stream) => {
|
|
105
|
+
if (pendingCount >= limits.maxPendingCallsPerPeer || budget.pendingCalls >= limits.maxPendingCallsPerRuntime) return frameworkError("resource_limit_exceeded", "dispatch");
|
|
106
|
+
if (stream && (reservedStreamCount >= limits.maxActiveStreamsPerPeer || budget.activeStreams >= limits.maxActiveStreamsPerRuntime)) return frameworkError("resource_limit_exceeded", "dispatch");
|
|
107
|
+
if (payloadBytes > limits.maxMessageBudgetBytes || peerRetainedPayloadBytes > limits.maxRetainedPayloadBytesPerPeer - payloadBytes || budget.retainedPayloadBytes > limits.maxRetainedPayloadBytesPerRuntime - payloadBytes) return frameworkError("resource_limit_exceeded", "dispatch");
|
|
108
|
+
budget.pendingCalls += 1;
|
|
109
|
+
if (stream) budget.activeStreams += 1;
|
|
110
|
+
peerRetainedPayloadBytes += payloadBytes;
|
|
111
|
+
budget.retainedPayloadBytes += payloadBytes;
|
|
112
|
+
pendingCount += 1;
|
|
113
|
+
if (stream) reservedStreamCount += 1;
|
|
114
|
+
return { payloadBytes, pendingReserved: true, streamReserved: stream, payloadReserved: true };
|
|
115
|
+
};
|
|
116
|
+
const releasePendingReservation = (reservation) => {
|
|
117
|
+
if (!reservation.pendingReserved) return;
|
|
118
|
+
reservation.pendingReserved = false;
|
|
119
|
+
pendingCount = Math.max(0, pendingCount - 1);
|
|
120
|
+
budget.pendingCalls = Math.max(0, budget.pendingCalls - 1);
|
|
121
|
+
};
|
|
122
|
+
const releaseStreamReservation = (reservation) => {
|
|
123
|
+
if (!reservation.streamReserved) return;
|
|
124
|
+
reservation.streamReserved = false;
|
|
125
|
+
reservedStreamCount = Math.max(0, reservedStreamCount - 1);
|
|
126
|
+
budget.activeStreams = Math.max(0, budget.activeStreams - 1);
|
|
127
|
+
};
|
|
128
|
+
const releasePayloadReservation = (reservation) => {
|
|
129
|
+
if (!reservation.payloadReserved) return;
|
|
130
|
+
reservation.payloadReserved = false;
|
|
131
|
+
peerRetainedPayloadBytes = Math.max(0, peerRetainedPayloadBytes - reservation.payloadBytes);
|
|
132
|
+
budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - reservation.payloadBytes);
|
|
133
|
+
};
|
|
134
|
+
const releaseReservation = (reservation) => {
|
|
135
|
+
releasePendingReservation(reservation);
|
|
136
|
+
releaseStreamReservation(reservation);
|
|
137
|
+
releasePayloadReservation(reservation);
|
|
138
|
+
};
|
|
139
|
+
const releaseCallbackExecution = (record) => {
|
|
140
|
+
if (record.released) return;
|
|
141
|
+
record.released = true;
|
|
142
|
+
callbackExecutions.delete(record);
|
|
143
|
+
peerRetainedPayloadBytes = Math.max(0, peerRetainedPayloadBytes - record.item.budgetBytes);
|
|
144
|
+
budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - record.item.budgetBytes);
|
|
145
|
+
budget.releaseExecutionSlot();
|
|
146
|
+
emit();
|
|
147
|
+
};
|
|
148
|
+
const releaseQueueItem = (item) => {
|
|
149
|
+
item.ledger.closeUndelivered();
|
|
150
|
+
peerRetainedPayloadBytes = Math.max(0, peerRetainedPayloadBytes - item.budgetBytes);
|
|
151
|
+
budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - item.budgetBytes);
|
|
152
|
+
};
|
|
153
|
+
const settleUnary = (entry, error, value) => {
|
|
154
|
+
if (entry.settled) return;
|
|
155
|
+
entry.settled = true;
|
|
156
|
+
if (entry.timer !== void 0) clearTimeout(entry.timer);
|
|
157
|
+
pending.delete(entry.callId);
|
|
158
|
+
releaseReservation(entry.reservation);
|
|
159
|
+
entry.cleanup?.();
|
|
160
|
+
if (error !== void 0) entry.reject(error);
|
|
161
|
+
else entry.resolve(value);
|
|
162
|
+
emit();
|
|
163
|
+
};
|
|
164
|
+
const sendCancel = (entry) => {
|
|
165
|
+
if (entry.cancelSent) return;
|
|
166
|
+
entry.cancelSent = true;
|
|
167
|
+
try {
|
|
168
|
+
options.transport.send({ type: RUNTIME_CANCEL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: entry.callId, serviceInstanceId: entry.reference.serviceInstanceId });
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
entry.controller.abort();
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const settleReadyReject = (stream, error) => {
|
|
177
|
+
if (stream.readySettled) return;
|
|
178
|
+
stream.readySettled = true;
|
|
179
|
+
stream.rejectReady(error);
|
|
180
|
+
};
|
|
181
|
+
const settleClosedReject = (stream, error) => {
|
|
182
|
+
if (stream.closedSettled) return;
|
|
183
|
+
stream.closedSettled = true;
|
|
184
|
+
stream.rejectClosed(error);
|
|
185
|
+
};
|
|
186
|
+
const settleClosedResolve = (stream) => {
|
|
187
|
+
if (stream.closedSettled) return;
|
|
188
|
+
stream.closedSettled = true;
|
|
189
|
+
stream.resolveClosed();
|
|
190
|
+
};
|
|
191
|
+
const forgetStream = (stream) => {
|
|
192
|
+
if (!stream.closedSettled || stream.processing || stream.queue.length > 0) return;
|
|
193
|
+
streams.delete(stream.callId);
|
|
194
|
+
};
|
|
195
|
+
const finishDraining = (stream) => {
|
|
196
|
+
if (!stream.doneReceived || stream.processing || stream.queue.length > 0 || stream.closedSettled) return;
|
|
197
|
+
stream.state = "closed";
|
|
198
|
+
settleClosedResolve(stream);
|
|
199
|
+
forgetStream(stream);
|
|
200
|
+
emit();
|
|
201
|
+
};
|
|
202
|
+
const terminateStream = (stream, error, notifyRemote) => {
|
|
203
|
+
if (stream.state === "closed" || stream.state === "failed") return;
|
|
204
|
+
stream.state = "failed";
|
|
205
|
+
if (stream.timer !== void 0) clearTimeout(stream.timer);
|
|
206
|
+
pending.delete(stream.callId);
|
|
207
|
+
stream.executionWaiterRemove?.();
|
|
208
|
+
stream.executionWaiterRemove = void 0;
|
|
209
|
+
releaseReservation(stream.reservation);
|
|
210
|
+
try {
|
|
211
|
+
stream.controller.abort(error);
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
for (const item of stream.queue.splice(0)) releaseQueueItem(item);
|
|
215
|
+
if (!stream.readySettled) settleReadyReject(stream, error);
|
|
216
|
+
settleClosedReject(stream, error);
|
|
217
|
+
stream.cleanup?.();
|
|
218
|
+
if (notifyRemote) sendCancel(stream);
|
|
219
|
+
forgetStream(stream);
|
|
220
|
+
emit();
|
|
221
|
+
};
|
|
222
|
+
const errorWithTerminalCause = (cause, capability, reference) => new WebLoomError(cause.code, cause.message, cause.phase, { ...cause.context, ...contextFor(capability, reference) }, cause.details);
|
|
223
|
+
const revokeProxy = (proxy, reason, terminalCause) => {
|
|
224
|
+
if (proxy.revoked) return;
|
|
225
|
+
proxy.revoked = true;
|
|
226
|
+
proxy.reason = reason;
|
|
227
|
+
if (proxies.get(proxy.cacheKey) === proxy) clients.delete(proxy.cacheKey);
|
|
228
|
+
const affected = /* @__PURE__ */ new Set();
|
|
229
|
+
for (const entry of pending.values()) if (entry.proxy === proxy) affected.add(entry);
|
|
230
|
+
for (const entry of streams.values()) if (entry.proxy === proxy) affected.add(entry);
|
|
231
|
+
for (const entry of affected) {
|
|
232
|
+
if (entry.proxy !== proxy) continue;
|
|
233
|
+
const error = terminalCause ? errorWithTerminalCause(terminalCause, entry.capability, entry.reference) : frameworkError("service_revoked", "dispose", contextFor(entry.capability, entry.reference));
|
|
234
|
+
if (entry.mode === "stream") terminateStream(entry, error, true);
|
|
235
|
+
else {
|
|
236
|
+
settleUnary(entry, error);
|
|
237
|
+
sendCancel(entry);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const record of [...waitingCalls]) {
|
|
241
|
+
if (record.proxy === proxy) record.fail(terminalCause ? errorWithTerminalCause(terminalCause, record.proxy.capability, record.proxy.bound) : frameworkError("service_revoked", "dispose", contextFor(record.proxy.capability, record.proxy.bound)));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const findService = (capability) => currentServices.find((service) => serviceMatches(service, capability));
|
|
245
|
+
const terminalError = (proxy, capability) => {
|
|
246
|
+
if (terminalFailure && !disposed) return frameworkError(terminalFailure.code, terminalFailure.phase, contextFor(capability, proxy.bound));
|
|
247
|
+
if (disposed || proxy.revoked) return frameworkError("service_revoked", "dispatch", contextFor(capability, proxy.bound));
|
|
248
|
+
if (currentState === "stale" || currentState === "disposed") return frameworkError("service_revoked", "dispatch", contextFor(capability, proxy.bound));
|
|
249
|
+
if (proxy.bound && !currentServices.some((service) => serviceKey(service) === serviceKey(proxy.bound) && service.serviceInstanceId === proxy.bound?.serviceInstanceId)) {
|
|
250
|
+
return frameworkError("service_stale", "dispatch", contextFor(capability, proxy.bound));
|
|
251
|
+
}
|
|
252
|
+
if (currentState === "ready" && !proxy.bound && !findService(capability)) {
|
|
253
|
+
return frameworkError("capability_unavailable", "wait", contextFor(capability));
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
};
|
|
257
|
+
const prepareRequest = (capability, request) => {
|
|
258
|
+
validateRawDto(request, limits, "validate");
|
|
259
|
+
const parser = capability;
|
|
260
|
+
let parsed;
|
|
261
|
+
try {
|
|
262
|
+
parsed = parser.request.parse(request);
|
|
263
|
+
} catch {
|
|
264
|
+
throw frameworkError("request_validation_failed", "validate", contextFor(capability));
|
|
265
|
+
}
|
|
266
|
+
let validation;
|
|
267
|
+
try {
|
|
268
|
+
const descriptor = capability.transfer;
|
|
269
|
+
validation = validateTransferListWithStats(parsed, descriptor?.request?.(parsed), { limits, phase: "validate" });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof WebLoomError) throw error;
|
|
272
|
+
throw frameworkError("transfer_invalid", "validate", contextFor(capability));
|
|
273
|
+
}
|
|
274
|
+
return { value: parsed, transfer: validation.transfer, budgetBytes: validation.stats.budgetBytes };
|
|
275
|
+
};
|
|
276
|
+
const prepareIncoming = (capability, value, direction, ledger) => {
|
|
277
|
+
try {
|
|
278
|
+
validateRawDto(value, limits, "receive");
|
|
279
|
+
} catch (error) {
|
|
280
|
+
ledger.closeUndelivered();
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
const parser = capability;
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
const selected = direction === "response" ? parser.response : parser.item;
|
|
287
|
+
if (!selected) throw new Error("parser unavailable");
|
|
288
|
+
parsed = selected.parse(value);
|
|
289
|
+
} catch {
|
|
290
|
+
ledger.closeUndelivered();
|
|
291
|
+
throw frameworkError("response_validation_failed", "receive", contextFor(capability));
|
|
292
|
+
}
|
|
293
|
+
let validation;
|
|
294
|
+
try {
|
|
295
|
+
validation = validateTransferListWithStats(parsed, parser.transfer?.[direction]?.(parsed), { limits, phase: "receive" });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
ledger.closeUndelivered();
|
|
298
|
+
throw error instanceof WebLoomError ? error : frameworkError("transfer_invalid", "receive", contextFor(capability));
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
assertReceivedPortSet(ledger, validation.transfer, { limits, phase: "receive" });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
ledger.closeUndelivered();
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
return { value: parsed, transfer: validation.transfer, stats: validation.stats };
|
|
307
|
+
};
|
|
308
|
+
const sendWire = (message, transfer = []) => {
|
|
309
|
+
const encoded = codec.encode(message);
|
|
310
|
+
options.transport.send(encoded, transfer);
|
|
311
|
+
};
|
|
312
|
+
const startUnary = (proxy, prepared, reference, callOptions, timeout, existingReservation) => {
|
|
313
|
+
const reservation = existingReservation ?? reserve(prepared.budgetBytes, false);
|
|
314
|
+
if (reservation instanceof WebLoomError) return Promise.reject(reservation);
|
|
315
|
+
const controller = new AbortController();
|
|
316
|
+
const merged = mergeSignals(controller.signal, proxy.scope?.signal, callOptions.signal);
|
|
317
|
+
const callId = id("call");
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
const entry = { callId, capability: proxy.capability, reference, proxy, controller, cleanup: merged.dispose, reservation, timer: void 0, settled: false, cancelSent: false, mode: "unary", resolve, reject };
|
|
320
|
+
pending.set(callId, entry);
|
|
321
|
+
const onAbort = () => {
|
|
322
|
+
if (entry.settled) return;
|
|
323
|
+
settleUnary(entry, frameworkError("request_cancelled", "dispose", contextFor(entry.capability, reference)));
|
|
324
|
+
sendCancel(entry);
|
|
325
|
+
};
|
|
326
|
+
merged.signal.addEventListener("abort", onAbort, { once: true });
|
|
327
|
+
entry.timer = setTimeout(() => {
|
|
328
|
+
if (entry.settled) return;
|
|
329
|
+
settleUnary(entry, frameworkError("call_timeout", "receive", contextFor(entry.capability, reference)));
|
|
330
|
+
sendCancel(entry);
|
|
331
|
+
}, timeout);
|
|
332
|
+
if (merged.signal.aborted) {
|
|
333
|
+
onAbort();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
sendWire({ type: RUNTIME_CALL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId, capabilityId: entry.capability.id, contractVersion: entry.capability.version, serviceInstanceId: reference.serviceInstanceId, mode: "unary", timeoutMs: timeout, request: prepared.value, ...callOptions.operationId !== void 0 ? { operationId: callOptions.operationId } : {} }, prepared.transfer);
|
|
338
|
+
} catch {
|
|
339
|
+
settleUnary(entry, frameworkError("request_clone_failed", "dispatch", contextFor(entry.capability, reference)));
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
const waitForService = (proxy, prepared, callOptions) => {
|
|
344
|
+
const timeout = timeoutMs(callOptions.timeoutMs, defaultTimeout);
|
|
345
|
+
const deadline = Date.now() + timeout;
|
|
346
|
+
const reserved = reserve(prepared.budgetBytes, false);
|
|
347
|
+
if (reserved instanceof WebLoomError) return Promise.reject(reserved);
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
const record = {
|
|
350
|
+
proxy,
|
|
351
|
+
prepared,
|
|
352
|
+
reservation: reserved,
|
|
353
|
+
deadline,
|
|
354
|
+
mode: "unary",
|
|
355
|
+
finished: false,
|
|
356
|
+
fail: () => void 0,
|
|
357
|
+
start: () => void 0
|
|
358
|
+
};
|
|
359
|
+
const cleanup = () => {
|
|
360
|
+
if (record.timer !== void 0) clearTimeout(record.timer);
|
|
361
|
+
record.removeListener?.();
|
|
362
|
+
callOptions.signal?.removeEventListener("abort", onAbort);
|
|
363
|
+
waitingCalls.delete(record);
|
|
364
|
+
};
|
|
365
|
+
record.fail = (error) => {
|
|
366
|
+
if (record.finished) return;
|
|
367
|
+
record.finished = true;
|
|
368
|
+
cleanup();
|
|
369
|
+
releaseReservation(record.reservation);
|
|
370
|
+
reject(error);
|
|
371
|
+
};
|
|
372
|
+
record.start = (reference, remaining) => {
|
|
373
|
+
if (record.finished) return;
|
|
374
|
+
record.finished = true;
|
|
375
|
+
cleanup();
|
|
376
|
+
if (!proxy.bound) proxy.bound = reference;
|
|
377
|
+
void startUnary(proxy, prepared, reference, callOptions, remaining, record.reservation).then(resolve, reject);
|
|
378
|
+
};
|
|
379
|
+
const check = () => {
|
|
380
|
+
if (record.finished) return;
|
|
381
|
+
const unavailable = terminalError(proxy, proxy.capability);
|
|
382
|
+
if (unavailable) {
|
|
383
|
+
record.fail(unavailable);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const reference = proxy.bound ?? findService(proxy.capability);
|
|
387
|
+
if (!reference) return;
|
|
388
|
+
const remaining = deadline - Date.now();
|
|
389
|
+
if (remaining <= 0) {
|
|
390
|
+
record.fail(frameworkError("call_timeout", "wait", contextFor(proxy.capability)));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
record.start(reference, remaining);
|
|
394
|
+
};
|
|
395
|
+
const onAbort = () => record.fail(frameworkError("request_cancelled", "dispose", contextFor(proxy.capability)));
|
|
396
|
+
record.timer = setTimeout(() => record.fail(frameworkError("call_timeout", "wait", contextFor(proxy.capability))), timeout);
|
|
397
|
+
if (callOptions.signal) {
|
|
398
|
+
if (callOptions.signal.aborted) {
|
|
399
|
+
onAbort();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
callOptions.signal.addEventListener("abort", onAbort, { once: true });
|
|
403
|
+
}
|
|
404
|
+
listeners.add(check);
|
|
405
|
+
record.removeListener = () => listeners.delete(check);
|
|
406
|
+
waitingCalls.add(record);
|
|
407
|
+
check();
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
const call = (proxy, request, callOptions) => {
|
|
411
|
+
const unavailable = terminalError(proxy, proxy.capability);
|
|
412
|
+
if (unavailable) return Promise.reject(unavailable);
|
|
413
|
+
const timeout = timeoutMs(callOptions.timeoutMs, defaultTimeout);
|
|
414
|
+
let prepared;
|
|
415
|
+
try {
|
|
416
|
+
prepared = prepareRequest(proxy.capability, request);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return Promise.reject(error instanceof WebLoomError ? error : frameworkError("request_validation_failed", "validate", contextFor(proxy.capability)));
|
|
419
|
+
}
|
|
420
|
+
const reference = proxy.bound ?? findService(proxy.capability);
|
|
421
|
+
if (!reference) return waitForService(proxy, prepared, callOptions);
|
|
422
|
+
if (!proxy.bound) proxy.bound = reference;
|
|
423
|
+
return startUnary(proxy, prepared, reference, callOptions, timeout);
|
|
424
|
+
};
|
|
425
|
+
const createRejectedSubscription = (error) => {
|
|
426
|
+
let readyReject;
|
|
427
|
+
let closedReject;
|
|
428
|
+
const ready = new Promise((_, reject) => {
|
|
429
|
+
readyReject = reject;
|
|
430
|
+
});
|
|
431
|
+
const closed = new Promise((_, reject) => {
|
|
432
|
+
closedReject = reject;
|
|
433
|
+
});
|
|
434
|
+
void ready.catch(() => void 0);
|
|
435
|
+
void closed.catch(() => void 0);
|
|
436
|
+
readyReject(error);
|
|
437
|
+
closedReject(error);
|
|
438
|
+
return { ready, closed, cancel() {
|
|
439
|
+
} };
|
|
440
|
+
};
|
|
441
|
+
const startStream = (proxy, prepared, reference, optionsForSubscribe, timeout, existingReservation) => {
|
|
442
|
+
const maximumCredit = Math.min(256, limits.maxStreamCredit);
|
|
443
|
+
const window = optionsForSubscribe.initialCredit ?? 16;
|
|
444
|
+
if (!validCredit(window, maximumCredit)) {
|
|
445
|
+
if (existingReservation) releaseReservation(existingReservation);
|
|
446
|
+
return createRejectedSubscription(frameworkError("stream_overflow", "validate", contextFor(proxy.capability, reference)));
|
|
447
|
+
}
|
|
448
|
+
const reservation = existingReservation ?? reserve(prepared.budgetBytes, true);
|
|
449
|
+
if (reservation instanceof WebLoomError) return createRejectedSubscription(reservation);
|
|
450
|
+
let resolveReady;
|
|
451
|
+
let rejectReady;
|
|
452
|
+
let resolveClosed;
|
|
453
|
+
let rejectClosed;
|
|
454
|
+
const ready = new Promise((resolve, reject) => {
|
|
455
|
+
resolveReady = resolve;
|
|
456
|
+
rejectReady = reject;
|
|
457
|
+
});
|
|
458
|
+
const closed = new Promise((resolve, reject) => {
|
|
459
|
+
resolveClosed = resolve;
|
|
460
|
+
rejectClosed = reject;
|
|
461
|
+
});
|
|
462
|
+
void ready.catch(() => void 0);
|
|
463
|
+
void closed.catch(() => void 0);
|
|
464
|
+
const controller = new AbortController();
|
|
465
|
+
const merged = mergeSignals(controller.signal, proxy.scope?.signal, optionsForSubscribe.signal);
|
|
466
|
+
const callId = id("stream");
|
|
467
|
+
const entry = {
|
|
468
|
+
callId,
|
|
469
|
+
capability: proxy.capability,
|
|
470
|
+
reference,
|
|
471
|
+
proxy,
|
|
472
|
+
controller,
|
|
473
|
+
cleanup: merged.dispose,
|
|
474
|
+
reservation,
|
|
475
|
+
timer: void 0,
|
|
476
|
+
settled: false,
|
|
477
|
+
cancelSent: false,
|
|
478
|
+
mode: "stream",
|
|
479
|
+
onNext: optionsForSubscribe.onNext,
|
|
480
|
+
itemParser: proxy.capability.item,
|
|
481
|
+
window,
|
|
482
|
+
credit: window,
|
|
483
|
+
sequence: 1,
|
|
484
|
+
state: "opening",
|
|
485
|
+
doneReceived: false,
|
|
486
|
+
processing: false,
|
|
487
|
+
draining: false,
|
|
488
|
+
readySettled: false,
|
|
489
|
+
closedSettled: false,
|
|
490
|
+
queue: [],
|
|
491
|
+
resolveReady,
|
|
492
|
+
rejectReady,
|
|
493
|
+
resolveClosed,
|
|
494
|
+
rejectClosed
|
|
495
|
+
};
|
|
496
|
+
streams.set(callId, entry);
|
|
497
|
+
pending.set(callId, entry);
|
|
498
|
+
const cancel = () => {
|
|
499
|
+
if (entry.state === "closed" || entry.state === "failed") return;
|
|
500
|
+
terminateStream(entry, frameworkError("request_cancelled", "dispose", contextFor(entry.capability, reference)), true);
|
|
501
|
+
};
|
|
502
|
+
merged.signal.addEventListener("abort", cancel, { once: true });
|
|
503
|
+
entry.timer = setTimeout(() => {
|
|
504
|
+
if (entry.state !== "opening") return;
|
|
505
|
+
terminateStream(entry, frameworkError("call_timeout", "wait", contextFor(entry.capability, reference)), true);
|
|
506
|
+
}, timeout);
|
|
507
|
+
if (merged.signal.aborted) {
|
|
508
|
+
cancel();
|
|
509
|
+
return { ready, closed, cancel };
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
sendWire({ type: RUNTIME_CALL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId, capabilityId: entry.capability.id, contractVersion: entry.capability.version, serviceInstanceId: reference.serviceInstanceId, mode: "stream", timeoutMs: timeout, request: prepared.value, initialCredit: window, ...optionsForSubscribe.operationId !== void 0 ? { operationId: optionsForSubscribe.operationId } : {} }, prepared.transfer);
|
|
513
|
+
} catch {
|
|
514
|
+
terminateStream(entry, frameworkError("request_clone_failed", "dispatch", contextFor(entry.capability, reference)), false);
|
|
515
|
+
}
|
|
516
|
+
return { ready, closed, cancel };
|
|
517
|
+
};
|
|
518
|
+
const waitForStream = (proxy, prepared, optionsForSubscribe) => {
|
|
519
|
+
const timeout = timeoutMs(optionsForSubscribe.timeoutMs, defaultTimeout);
|
|
520
|
+
const deadline = Date.now() + timeout;
|
|
521
|
+
const reserved = reserve(prepared.budgetBytes, true);
|
|
522
|
+
if (reserved instanceof WebLoomError) return createRejectedSubscription(reserved);
|
|
523
|
+
let resolveReady;
|
|
524
|
+
let rejectReady;
|
|
525
|
+
let resolveClosed;
|
|
526
|
+
let rejectClosed;
|
|
527
|
+
const ready = new Promise((resolve, reject) => {
|
|
528
|
+
resolveReady = resolve;
|
|
529
|
+
rejectReady = reject;
|
|
530
|
+
});
|
|
531
|
+
const closed = new Promise((resolve, reject) => {
|
|
532
|
+
resolveClosed = resolve;
|
|
533
|
+
rejectClosed = reject;
|
|
534
|
+
});
|
|
535
|
+
void ready.catch(() => void 0);
|
|
536
|
+
void closed.catch(() => void 0);
|
|
537
|
+
let active;
|
|
538
|
+
const record = {
|
|
539
|
+
proxy,
|
|
540
|
+
prepared,
|
|
541
|
+
reservation: reserved,
|
|
542
|
+
deadline,
|
|
543
|
+
mode: "stream",
|
|
544
|
+
finished: false,
|
|
545
|
+
fail: () => void 0,
|
|
546
|
+
start: () => void 0
|
|
547
|
+
};
|
|
548
|
+
const cleanup = () => {
|
|
549
|
+
if (record.timer !== void 0) clearTimeout(record.timer);
|
|
550
|
+
record.removeListener?.();
|
|
551
|
+
optionsForSubscribe.signal?.removeEventListener("abort", onAbort);
|
|
552
|
+
waitingCalls.delete(record);
|
|
553
|
+
};
|
|
554
|
+
record.fail = (error) => {
|
|
555
|
+
if (record.finished) return;
|
|
556
|
+
record.finished = true;
|
|
557
|
+
cleanup();
|
|
558
|
+
releaseReservation(record.reservation);
|
|
559
|
+
rejectReady(error);
|
|
560
|
+
rejectClosed(error);
|
|
561
|
+
};
|
|
562
|
+
record.start = (reference, remaining) => {
|
|
563
|
+
if (record.finished) return;
|
|
564
|
+
record.finished = true;
|
|
565
|
+
cleanup();
|
|
566
|
+
if (!proxy.bound) proxy.bound = reference;
|
|
567
|
+
active = startStream(proxy, prepared, reference, optionsForSubscribe, remaining, record.reservation);
|
|
568
|
+
active.ready.then(resolveReady, rejectReady);
|
|
569
|
+
active.closed.then(resolveClosed, rejectClosed);
|
|
570
|
+
};
|
|
571
|
+
const onAbort = () => record.fail(frameworkError("request_cancelled", "dispose", contextFor(proxy.capability)));
|
|
572
|
+
const check = () => {
|
|
573
|
+
if (record.finished) return;
|
|
574
|
+
const unavailable = terminalError(proxy, proxy.capability);
|
|
575
|
+
if (unavailable) {
|
|
576
|
+
record.fail(unavailable);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const reference = proxy.bound ?? findService(proxy.capability);
|
|
580
|
+
if (!reference) return;
|
|
581
|
+
const remaining = deadline - Date.now();
|
|
582
|
+
if (remaining <= 0) {
|
|
583
|
+
record.fail(frameworkError("call_timeout", "wait", contextFor(proxy.capability)));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
record.start(reference, remaining);
|
|
587
|
+
};
|
|
588
|
+
record.timer = setTimeout(() => record.fail(frameworkError("call_timeout", "wait", contextFor(proxy.capability))), timeout);
|
|
589
|
+
if (optionsForSubscribe.signal) {
|
|
590
|
+
if (optionsForSubscribe.signal.aborted) {
|
|
591
|
+
onAbort();
|
|
592
|
+
return { ready, closed, cancel() {
|
|
593
|
+
} };
|
|
594
|
+
}
|
|
595
|
+
optionsForSubscribe.signal.addEventListener("abort", onAbort, { once: true });
|
|
596
|
+
}
|
|
597
|
+
listeners.add(check);
|
|
598
|
+
record.removeListener = () => listeners.delete(check);
|
|
599
|
+
waitingCalls.add(record);
|
|
600
|
+
check();
|
|
601
|
+
return { ready, closed, cancel(reason = "stream cancelled") {
|
|
602
|
+
if (active) active.cancel(reason);
|
|
603
|
+
else record.fail(frameworkError("request_cancelled", "dispose", contextFor(proxy.capability)));
|
|
604
|
+
} };
|
|
605
|
+
};
|
|
606
|
+
const subscribe = (proxy, request, optionsForSubscribe) => {
|
|
607
|
+
const unavailable = terminalError(proxy, proxy.capability);
|
|
608
|
+
if (unavailable) return createRejectedSubscription(unavailable);
|
|
609
|
+
if (typeof optionsForSubscribe.onNext !== "function") return createRejectedSubscription(frameworkError("request_validation_failed", "validate", contextFor(proxy.capability)));
|
|
610
|
+
let prepared;
|
|
611
|
+
try {
|
|
612
|
+
prepared = prepareRequest(proxy.capability, request);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
return createRejectedSubscription(error instanceof WebLoomError ? error : frameworkError("request_validation_failed", "validate", contextFor(proxy.capability)));
|
|
615
|
+
}
|
|
616
|
+
const reference = proxy.bound ?? findService(proxy.capability);
|
|
617
|
+
if (!reference) return waitForStream(proxy, prepared, optionsForSubscribe);
|
|
618
|
+
if (!proxy.bound) proxy.bound = reference;
|
|
619
|
+
return startStream(proxy, prepared, reference, optionsForSubscribe, timeoutMs(optionsForSubscribe.timeoutMs, defaultTimeout));
|
|
620
|
+
};
|
|
621
|
+
let drainStream;
|
|
622
|
+
const waitForExecutionSlot = (stream) => {
|
|
623
|
+
if (stream.executionWaiterRemove || stream.state === "failed" || stream.state === "closed" || stream.queue.length === 0) return;
|
|
624
|
+
stream.executionWaiterRemove = budget.registerExecutionWaiter(() => {
|
|
625
|
+
stream.executionWaiterRemove = void 0;
|
|
626
|
+
drainStream(stream);
|
|
627
|
+
});
|
|
628
|
+
};
|
|
629
|
+
drainStream = (stream) => {
|
|
630
|
+
if (stream.draining) return;
|
|
631
|
+
stream.draining = true;
|
|
632
|
+
void (async () => {
|
|
633
|
+
try {
|
|
634
|
+
while (stream.queue.length > 0) {
|
|
635
|
+
if (stream.state === "failed" || stream.state === "closed") break;
|
|
636
|
+
const item = stream.queue.shift();
|
|
637
|
+
if (!item) break;
|
|
638
|
+
if (callbackExecutions.size >= limits.maxExecutionSlotsPerPeer || budget.executionSlots >= limits.maxExecutionSlotsPerRuntime) {
|
|
639
|
+
stream.queue.unshift(item);
|
|
640
|
+
waitForExecutionSlot(stream);
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
stream.executionWaiterRemove?.();
|
|
644
|
+
stream.executionWaiterRemove = void 0;
|
|
645
|
+
const execution = { stream, item, released: false };
|
|
646
|
+
callbackExecutions.add(execution);
|
|
647
|
+
budget.executionSlots += 1;
|
|
648
|
+
stream.processing = true;
|
|
649
|
+
try {
|
|
650
|
+
item.ledger.handoff();
|
|
651
|
+
await stream.onNext(item.value);
|
|
652
|
+
if (stream.state === "active" && !stream.doneReceived) {
|
|
653
|
+
stream.credit += 1;
|
|
654
|
+
try {
|
|
655
|
+
sendWire({ type: RUNTIME_CREDIT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: stream.callId, serviceInstanceId: stream.reference.serviceInstanceId, count: 1 });
|
|
656
|
+
} catch {
|
|
657
|
+
terminateStream(stream, frameworkError("transport_unavailable", "dispatch", contextFor(stream.capability, stream.reference)), true);
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
} catch {
|
|
662
|
+
terminateStream(stream, frameworkError("handler_failed", "execute", contextFor(stream.capability, stream.reference)), true);
|
|
663
|
+
break;
|
|
664
|
+
} finally {
|
|
665
|
+
stream.processing = false;
|
|
666
|
+
releaseCallbackExecution(execution);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} finally {
|
|
670
|
+
stream.processing = false;
|
|
671
|
+
stream.draining = false;
|
|
672
|
+
if (stream.queue.length === 0 || stream.state === "failed" || stream.state === "closed") {
|
|
673
|
+
stream.executionWaiterRemove?.();
|
|
674
|
+
stream.executionWaiterRemove = void 0;
|
|
675
|
+
} else {
|
|
676
|
+
waitForExecutionSlot(stream);
|
|
677
|
+
}
|
|
678
|
+
finishDraining(stream);
|
|
679
|
+
forgetStream(stream);
|
|
680
|
+
}
|
|
681
|
+
})().catch(() => {
|
|
682
|
+
terminateStream(stream, frameworkError("handler_failed", "execute", contextFor(stream.capability, stream.reference)), true);
|
|
683
|
+
});
|
|
684
|
+
};
|
|
685
|
+
const invalidate = (reason = "remote service directory replaced", terminalCause) => {
|
|
686
|
+
if (terminalCause) {
|
|
687
|
+
for (const record of [...waitingCalls]) record.fail(errorWithTerminalCause(terminalCause, record.proxy.capability, record.proxy.bound));
|
|
688
|
+
for (const entry of [...pending.values()]) {
|
|
689
|
+
const error = errorWithTerminalCause(terminalCause, entry.capability, entry.reference);
|
|
690
|
+
if (entry.mode === "stream") terminateStream(entry, error, true);
|
|
691
|
+
else settleUnary(entry, error);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
for (const proxy of proxies.values()) revokeProxy(proxy, reason, terminalCause);
|
|
695
|
+
if (!terminalCause) for (const record of [...waitingCalls]) record.fail(frameworkError("service_stale", "dispose", contextFor(record.proxy.capability, record.proxy.bound)));
|
|
696
|
+
clients.clear();
|
|
697
|
+
if (!terminalCause) for (const entry of [...pending.values()]) {
|
|
698
|
+
if (entry.mode === "stream") terminateStream(entry, frameworkError("service_stale", "dispose", contextFor(entry.capability, entry.reference)), true);
|
|
699
|
+
else settleUnary(entry, frameworkError("service_stale", "dispose", contextFor(entry.capability, entry.reference)));
|
|
700
|
+
}
|
|
701
|
+
currentServices = [];
|
|
702
|
+
currentState = disposed ? "disposed" : "stale";
|
|
703
|
+
emit();
|
|
704
|
+
};
|
|
705
|
+
const failClose = (reason, code = "invalid_message") => {
|
|
706
|
+
if (disposed) return;
|
|
707
|
+
terminalFailure = frameworkError(code, "receive");
|
|
708
|
+
invalidate(reason, terminalFailure);
|
|
709
|
+
try {
|
|
710
|
+
options.transport.close?.();
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
const onMessage = (message, metadata) => {
|
|
715
|
+
const ledger = metadata?.ledger ?? createReceivePortLedger(metadata?.ports, { limits, phase: "receive" });
|
|
716
|
+
if (!ledger.valid) {
|
|
717
|
+
failClose("Invalid received transfer metadata", "transfer_invalid");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
if (!metadata?.decoded) {
|
|
722
|
+
message = codec.decode(message);
|
|
723
|
+
}
|
|
724
|
+
} catch (error) {
|
|
725
|
+
ledger.closeUndelivered();
|
|
726
|
+
if (error instanceof WebLoomError && error.code === "protocol_mismatch") {
|
|
727
|
+
failClose("Remote Runtime protocol mismatch", "protocol_mismatch");
|
|
728
|
+
} else if (error instanceof WebLoomError && error.code === "invalid_snapshot") {
|
|
729
|
+
failClose("Invalid WebLoom runtime message", "invalid_snapshot");
|
|
730
|
+
} else failClose("Invalid WebLoom runtime message", "invalid_message");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
if (message.type === RUNTIME_CALL_TYPE || message.type === RUNTIME_CANCEL_TYPE || message.type === RUNTIME_CREDIT_TYPE) return;
|
|
735
|
+
if (message.type === RUNTIME_SNAPSHOT_TYPE) {
|
|
736
|
+
if (ledger.ports.length > 0) throw frameworkError("transfer_invalid", "receive");
|
|
737
|
+
const result = applySnapshot(message);
|
|
738
|
+
if (!result.accepted && result.reason === "protocol-mismatch") failClose("Remote Runtime protocol mismatch", "protocol_mismatch");
|
|
739
|
+
else if (!result.accepted && result.reason === "invalid-snapshot") failClose("Invalid Runtime snapshot", "invalid_snapshot");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (message.type === RUNTIME_ERROR_TYPE) {
|
|
743
|
+
ledger.closeUndelivered();
|
|
744
|
+
invalidate(message.message);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (message.type === RUNTIME_RESULT_TYPE) {
|
|
748
|
+
const entry = pending.get(message.callId);
|
|
749
|
+
if (!entry || entry.reference.serviceInstanceId !== message.serviceInstanceId) {
|
|
750
|
+
ledger.closeUndelivered();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (entry.mode === "unary") {
|
|
754
|
+
if (!("result" in message)) {
|
|
755
|
+
ledger.closeUndelivered();
|
|
756
|
+
settleUnary(entry, frameworkError("invalid_message", "receive", contextFor(entry.capability, entry.reference)));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
const incoming = prepareIncoming(entry.capability, message.result, "response", ledger);
|
|
761
|
+
ledger.handoff();
|
|
762
|
+
settleUnary(entry, void 0, incoming.value);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
settleUnary(entry, error instanceof WebLoomError ? error : frameworkError("response_validation_failed", "receive", contextFor(entry.capability, entry.reference)));
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const stream = entry;
|
|
769
|
+
if ("streamReady" in message && message.streamReady) {
|
|
770
|
+
if (stream.state !== "opening") {
|
|
771
|
+
ledger.closeUndelivered();
|
|
772
|
+
terminateStream(stream, frameworkError("invalid_message", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (ledger.ports.length > 0) {
|
|
776
|
+
ledger.closeUndelivered();
|
|
777
|
+
terminateStream(stream, frameworkError("transfer_invalid", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
stream.state = "active";
|
|
781
|
+
stream.readySettled = true;
|
|
782
|
+
if (stream.timer !== void 0) clearTimeout(stream.timer);
|
|
783
|
+
releasePendingReservation(stream.reservation);
|
|
784
|
+
stream.resolveReady();
|
|
785
|
+
emit();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if ("done" in message && message.done) {
|
|
789
|
+
if (stream.state !== "active" || stream.doneReceived) {
|
|
790
|
+
ledger.closeUndelivered();
|
|
791
|
+
terminateStream(stream, frameworkError("invalid_message", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (ledger.ports.length > 0) {
|
|
795
|
+
ledger.closeUndelivered();
|
|
796
|
+
terminateStream(stream, frameworkError("transfer_invalid", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
stream.doneReceived = true;
|
|
800
|
+
stream.state = "draining";
|
|
801
|
+
pending.delete(stream.callId);
|
|
802
|
+
releaseStreamReservation(stream.reservation);
|
|
803
|
+
releasePayloadReservation(stream.reservation);
|
|
804
|
+
stream.cleanup?.();
|
|
805
|
+
finishDraining(stream);
|
|
806
|
+
drainStream(stream);
|
|
807
|
+
emit();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
ledger.closeUndelivered();
|
|
811
|
+
terminateStream(stream, frameworkError("invalid_message", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (message.type === RUNTIME_ERROR_MESSAGE_TYPE) {
|
|
815
|
+
const entry = pending.get(message.callId);
|
|
816
|
+
if (!entry || entry.reference.serviceInstanceId !== message.serviceInstanceId) {
|
|
817
|
+
ledger.closeUndelivered();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
ledger.closeUndelivered();
|
|
821
|
+
const error = errorFromWire(message);
|
|
822
|
+
if (entry.mode === "stream") terminateStream(entry, error, false);
|
|
823
|
+
else settleUnary(entry, error);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (message.type === RUNTIME_NEXT_TYPE) {
|
|
827
|
+
const entry = pending.get(message.callId);
|
|
828
|
+
if (!entry || entry.mode !== "stream" || entry.reference.serviceInstanceId !== message.serviceInstanceId) {
|
|
829
|
+
ledger.closeUndelivered();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const stream = entry;
|
|
833
|
+
if (stream.state !== "active" || stream.doneReceived) {
|
|
834
|
+
ledger.closeUndelivered();
|
|
835
|
+
terminateStream(stream, frameworkError("invalid_message", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (stream.credit < 1) {
|
|
839
|
+
ledger.closeUndelivered();
|
|
840
|
+
terminateStream(stream, frameworkError("stream_overflow", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (message.sequence !== stream.sequence) {
|
|
844
|
+
ledger.closeUndelivered();
|
|
845
|
+
terminateStream(stream, frameworkError("stream_overflow", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
let incoming;
|
|
849
|
+
try {
|
|
850
|
+
incoming = prepareIncoming(stream.capability, message.item, "item", ledger);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
terminateStream(stream, error instanceof WebLoomError ? error : frameworkError("response_validation_failed", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const { value, stats } = incoming;
|
|
856
|
+
if (peerRetainedPayloadBytes > limits.maxRetainedPayloadBytesPerPeer - stats.budgetBytes || budget.retainedPayloadBytes > limits.maxRetainedPayloadBytesPerRuntime - stats.budgetBytes || stream.queue.length >= stream.window) {
|
|
857
|
+
ledger.closeUndelivered();
|
|
858
|
+
terminateStream(stream, frameworkError("resource_limit_exceeded", "receive", contextFor(stream.capability, stream.reference)), true);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
stream.credit -= 1;
|
|
862
|
+
stream.sequence += 1;
|
|
863
|
+
stream.queue.push({ value, ledger, budgetBytes: stats.budgetBytes });
|
|
864
|
+
peerRetainedPayloadBytes += stats.budgetBytes;
|
|
865
|
+
budget.retainedPayloadBytes += stats.budgetBytes;
|
|
866
|
+
drainStream(stream);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
ledger.closeUndelivered();
|
|
870
|
+
} catch (error) {
|
|
871
|
+
ledger.closeUndelivered();
|
|
872
|
+
if (error instanceof WebLoomError && error.code === "protocol_mismatch") failClose("Remote Runtime protocol mismatch", "protocol_mismatch");
|
|
873
|
+
else if (error instanceof WebLoomError && error.code === "invalid_snapshot") failClose("Invalid WebLoom runtime message", "invalid_snapshot");
|
|
874
|
+
else if (error instanceof WebLoomError && error.code === "transfer_invalid") failClose("Invalid WebLoom runtime message", "transfer_invalid");
|
|
875
|
+
else failClose("Invalid WebLoom runtime message", "invalid_message");
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
const removeTransport = options.transport.subscribe((message, metadata) => onMessage(message, metadata));
|
|
879
|
+
const removeTransportError = options.transport.subscribeError?.((error, metadata) => {
|
|
880
|
+
metadata?.ledger?.closeUndelivered();
|
|
881
|
+
if (error instanceof WebLoomError && error.code === "protocol_mismatch") failClose("Remote Runtime protocol mismatch", "protocol_mismatch");
|
|
882
|
+
else if (error instanceof WebLoomError && error.code === "transfer_invalid") failClose("Invalid received transfer metadata", "transfer_invalid");
|
|
883
|
+
else failClose("Invalid WebLoom runtime message", "invalid_message");
|
|
884
|
+
});
|
|
885
|
+
function applySnapshot(snapshot) {
|
|
886
|
+
if (disposed) return { accepted: false, reason: "disposed" };
|
|
887
|
+
if (!snapshot || typeof snapshot !== "object") return { accepted: false, reason: "invalid-snapshot" };
|
|
888
|
+
if (snapshot.protocolVersion !== RUNTIME_PROTOCOL_VERSION) return { accepted: false, reason: "protocol-mismatch" };
|
|
889
|
+
try {
|
|
890
|
+
if (snapshot.units.length > limits.maxSnapshotUnits || snapshot.services.length > limits.maxSnapshotServices) return { accepted: false, reason: "invalid-snapshot" };
|
|
891
|
+
codec.encode({ ...snapshot, type: RUNTIME_SNAPSHOT_TYPE });
|
|
892
|
+
validateDto(snapshot, { limits: { maxDepth: limits.maxDtoDepth, maxNodes: limits.maxDtoNodes, maxEdges: limits.maxDtoEdges, maxBudgetBytes: limits.maxMessageBudgetBytes }, phase: "receive" });
|
|
893
|
+
} catch {
|
|
894
|
+
return { accepted: false, reason: "invalid-snapshot" };
|
|
895
|
+
}
|
|
896
|
+
if (options.remoteRuntimeId !== void 0 && snapshot.runtimeId !== options.remoteRuntimeId) return { accepted: false, reason: "invalid-snapshot" };
|
|
897
|
+
if (options.remoteRuntimeKind !== void 0 && snapshot.runtimeKind !== options.remoteRuntimeKind) return { accepted: false, reason: "invalid-snapshot" };
|
|
898
|
+
if (remoteRuntimeId !== void 0 && snapshot.runtimeId !== remoteRuntimeId) return { accepted: false, reason: "invalid-snapshot" };
|
|
899
|
+
if (remoteRuntimeKind !== void 0 && snapshot.runtimeKind !== remoteRuntimeKind) return { accepted: false, reason: "invalid-snapshot" };
|
|
900
|
+
let fingerprint;
|
|
901
|
+
try {
|
|
902
|
+
fingerprint = JSON.stringify(snapshot);
|
|
903
|
+
} catch {
|
|
904
|
+
return { accepted: false, reason: "invalid-snapshot" };
|
|
905
|
+
}
|
|
906
|
+
if (remoteRuntimeInstanceId !== void 0 && remoteRuntimeInstanceId === snapshot.runtimeInstanceId && snapshot.revision <= revision) {
|
|
907
|
+
if (snapshot.revision === revision && fingerprint === appliedSnapshotFingerprint) return { accepted: true, state: currentState === "disposed" ? "stale" : currentState, revision };
|
|
908
|
+
return { accepted: false, reason: "stale-revision", receivedRevision: snapshot.revision };
|
|
909
|
+
}
|
|
910
|
+
const services = [];
|
|
911
|
+
const keys = /* @__PURE__ */ new Set();
|
|
912
|
+
try {
|
|
913
|
+
for (const service of snapshot.state === "ready" ? snapshot.services : []) {
|
|
914
|
+
const key = serviceKey(service);
|
|
915
|
+
if (keys.has(key)) return { accepted: false, reason: "invalid-snapshot" };
|
|
916
|
+
keys.add(key);
|
|
917
|
+
services.push(Object.freeze({ kind: service.kind, capabilityId: service.capabilityId, contractVersion: service.contractVersion, runtime: snapshot.runtimeKind, runtimeInstanceId: snapshot.runtimeInstanceId, serviceInstanceId: service.serviceInstanceId, attributes: cloneFrozenAttributes(service.attributes), ...service.grantId !== void 0 ? { grantId: service.grantId } : {}, ...service.authorizationRevision !== void 0 ? { authorizationRevision: service.authorizationRevision } : {} }));
|
|
918
|
+
}
|
|
919
|
+
} catch {
|
|
920
|
+
return { accepted: false, reason: "invalid-snapshot" };
|
|
921
|
+
}
|
|
922
|
+
const newRuntime = remoteRuntimeInstanceId !== void 0 && remoteRuntimeInstanceId !== snapshot.runtimeInstanceId;
|
|
923
|
+
const replaced = !newRuntime && remoteRuntimeInstanceId === snapshot.runtimeInstanceId && currentServices.some((current) => {
|
|
924
|
+
const next = services.find((service) => serviceKey(service) === serviceKey(current));
|
|
925
|
+
return next !== void 0 && next.serviceInstanceId !== current.serviceInstanceId;
|
|
926
|
+
});
|
|
927
|
+
if (newRuntime || replaced) invalidate(newRuntime ? "Remote Runtime instance changed" : "Remote Runtime service exposure replaced");
|
|
928
|
+
if (!newRuntime && !replaced) {
|
|
929
|
+
for (const proxy of proxies.values()) {
|
|
930
|
+
if (proxy.bound && !services.some((service) => serviceKey(service) === serviceKey(proxy.bound) && service.serviceInstanceId === proxy.bound?.serviceInstanceId)) {
|
|
931
|
+
revokeProxy(proxy, "Remote Runtime service exposure was removed");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
remoteRuntimeId = snapshot.runtimeId;
|
|
936
|
+
remoteRuntimeKind = snapshot.runtimeKind;
|
|
937
|
+
remoteRuntimeInstanceId = snapshot.runtimeInstanceId;
|
|
938
|
+
revision = snapshot.revision;
|
|
939
|
+
appliedSnapshotFingerprint = fingerprint;
|
|
940
|
+
currentServices = Object.freeze(services);
|
|
941
|
+
currentState = snapshot.state === "ready" ? "ready" : snapshot.state === "starting" ? "empty" : snapshot.state === "disposed" ? "disposed" : "stale";
|
|
942
|
+
if (currentState !== "ready" && snapshot.state !== "starting") for (const proxy of proxies.values()) revokeProxy(proxy, `Remote Runtime is ${snapshot.state}`);
|
|
943
|
+
emit();
|
|
944
|
+
return { accepted: true, state: currentState === "disposed" ? "stale" : currentState, revision };
|
|
945
|
+
}
|
|
946
|
+
const bridge = {
|
|
947
|
+
get state() {
|
|
948
|
+
return currentState;
|
|
949
|
+
},
|
|
950
|
+
get runtimeInstanceId() {
|
|
951
|
+
return remoteRuntimeInstanceId;
|
|
952
|
+
},
|
|
953
|
+
get runtimeKind() {
|
|
954
|
+
return remoteRuntimeKind;
|
|
955
|
+
},
|
|
956
|
+
getClient(capability, scope) {
|
|
957
|
+
const key = `${capability.kind}\0${capability.id}\0${capability.version}\0${scope?.identity.scopeId ?? "root"}`;
|
|
958
|
+
let proxy = proxies.get(key);
|
|
959
|
+
if (!proxy || proxy.revoked) {
|
|
960
|
+
proxy = { capability, scope, cacheKey: key, revoked: false, reason: "Capability proxy is revoked" };
|
|
961
|
+
proxies.set(key, proxy);
|
|
962
|
+
scope?.onRevoke((reason) => revokeProxy(proxy, reason));
|
|
963
|
+
}
|
|
964
|
+
const cached = clients.get(key);
|
|
965
|
+
if (cached && !proxy.revoked) return cached;
|
|
966
|
+
const client = capability.kind === "rpc" ? { call: (request, optionsForCall) => call(proxy, request, optionsForCall ?? {}) } : { subscribe: (request, optionsForSubscribe) => subscribe(proxy, request, optionsForSubscribe) };
|
|
967
|
+
clients.set(key, client);
|
|
968
|
+
return client;
|
|
969
|
+
},
|
|
970
|
+
applySnapshot,
|
|
971
|
+
invalidate,
|
|
972
|
+
disconnect(reason = "Runtime disconnected") {
|
|
973
|
+
invalidate(reason);
|
|
974
|
+
currentState = "stale";
|
|
975
|
+
},
|
|
976
|
+
dispose(reason = "Runtime bridge disposed") {
|
|
977
|
+
if (disposed) return;
|
|
978
|
+
disposed = true;
|
|
979
|
+
invalidate(reason);
|
|
980
|
+
removeTransport();
|
|
981
|
+
removeTransportError?.();
|
|
982
|
+
options.transport.close?.();
|
|
983
|
+
currentState = "disposed";
|
|
984
|
+
emit();
|
|
985
|
+
},
|
|
986
|
+
subscribe(listener) {
|
|
987
|
+
listeners.add(listener);
|
|
988
|
+
return () => listeners.delete(listener);
|
|
989
|
+
},
|
|
990
|
+
services() {
|
|
991
|
+
return currentServices;
|
|
992
|
+
},
|
|
993
|
+
get pendingCallCount() {
|
|
994
|
+
return pendingCount;
|
|
995
|
+
},
|
|
996
|
+
get activeStreamCount() {
|
|
997
|
+
return [...streams.values()].filter((entry) => entry.state === "opening" || entry.state === "active").length;
|
|
998
|
+
},
|
|
999
|
+
get retainedPayloadBytes() {
|
|
1000
|
+
return budget.retainedPayloadBytes;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
return bridge;
|
|
1004
|
+
}
|
|
1005
|
+
function validateTransferables(value, transfer, limits) {
|
|
1006
|
+
return validateTransferList(value, transfer, { limits: limits ?? DEFAULT_RUNTIME_LIMITS, phase: "validate" });
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/transport/messagePortServiceTransport.ts
|
|
1010
|
+
function createMessagePortRuntimeTransport(port, options = {}) {
|
|
1011
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1012
|
+
const errorListeners = /* @__PURE__ */ new Set();
|
|
1013
|
+
const codec = createRuntimeMessageCodec();
|
|
1014
|
+
const limits = normalizeRuntimeLimits(options.limits);
|
|
1015
|
+
const onMessage = (event) => {
|
|
1016
|
+
const ledger = createReceivePortLedger(event.ports ?? [], { limits, phase: "receive" });
|
|
1017
|
+
const metadata = Object.freeze({
|
|
1018
|
+
ports: ledger.ports,
|
|
1019
|
+
ledger,
|
|
1020
|
+
decoded: true
|
|
1021
|
+
});
|
|
1022
|
+
if (!ledger.valid) {
|
|
1023
|
+
const error = new WebLoomError("transfer_invalid", "Invalid received transfer metadata", "receive");
|
|
1024
|
+
for (const listener of [...errorListeners]) {
|
|
1025
|
+
try {
|
|
1026
|
+
listener(error, metadata);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
let message;
|
|
1033
|
+
try {
|
|
1034
|
+
message = codec.decode(event.data);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
ledger.closeUndelivered();
|
|
1037
|
+
for (const listener of [...errorListeners]) {
|
|
1038
|
+
try {
|
|
1039
|
+
listener(error, metadata);
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
for (const listener of [...listeners]) {
|
|
1046
|
+
try {
|
|
1047
|
+
listener(message, metadata);
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
port.addEventListener("message", onMessage);
|
|
1053
|
+
port.start?.();
|
|
1054
|
+
let closed = false;
|
|
1055
|
+
return {
|
|
1056
|
+
send(message, transfer) {
|
|
1057
|
+
if (closed) throw new Error("MessagePort transport is closed");
|
|
1058
|
+
port.postMessage(message, transfer);
|
|
1059
|
+
},
|
|
1060
|
+
subscribe(listener) {
|
|
1061
|
+
listeners.add(listener);
|
|
1062
|
+
return () => listeners.delete(listener);
|
|
1063
|
+
},
|
|
1064
|
+
subscribeError(listener) {
|
|
1065
|
+
errorListeners.add(listener);
|
|
1066
|
+
return () => errorListeners.delete(listener);
|
|
1067
|
+
},
|
|
1068
|
+
close() {
|
|
1069
|
+
if (closed) return;
|
|
1070
|
+
closed = true;
|
|
1071
|
+
port.removeEventListener("message", onMessage);
|
|
1072
|
+
port.close?.();
|
|
1073
|
+
listeners.clear();
|
|
1074
|
+
errorListeners.clear();
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/transport/messagePortServiceProvider.ts
|
|
1080
|
+
function safeErrorCode(error, fallback) {
|
|
1081
|
+
return error instanceof WebLoomError ? error.code : fallback;
|
|
1082
|
+
}
|
|
1083
|
+
function post(transport, codec, message, transfer = []) {
|
|
1084
|
+
try {
|
|
1085
|
+
transport.send(codec.encode(message), transfer);
|
|
1086
|
+
return true;
|
|
1087
|
+
} catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function safeErrorMessage(code) {
|
|
1092
|
+
const messages = {
|
|
1093
|
+
capability_unavailable: "Capability is unavailable",
|
|
1094
|
+
permission_denied: "Capability authorization denied",
|
|
1095
|
+
service_stale: "Runtime service exposure is stale",
|
|
1096
|
+
request_validation_failed: "Capability request failed validation",
|
|
1097
|
+
response_validation_failed: "Capability response failed validation",
|
|
1098
|
+
request_clone_failed: "Capability request could not be delivered",
|
|
1099
|
+
response_clone_failed: "Capability response could not be delivered",
|
|
1100
|
+
transfer_invalid: "Capability transfer declaration is invalid",
|
|
1101
|
+
handler_failed: "Remote capability operation failed",
|
|
1102
|
+
call_timeout: "Capability call timed out",
|
|
1103
|
+
request_cancelled: "Capability call was cancelled",
|
|
1104
|
+
service_revoked: "Capability exposure was revoked",
|
|
1105
|
+
transport_unavailable: "Runtime transport is unavailable",
|
|
1106
|
+
stream_overflow: "Stream credit or sequence is invalid",
|
|
1107
|
+
resource_limit_exceeded: "Runtime resource limit exceeded",
|
|
1108
|
+
invalid_message: "Invalid WebLoom runtime message"
|
|
1109
|
+
};
|
|
1110
|
+
return messages[code] ?? "Remote capability operation failed";
|
|
1111
|
+
}
|
|
1112
|
+
function createMessagePortServiceProvider(options) {
|
|
1113
|
+
const codec = createRuntimeMessageCodec();
|
|
1114
|
+
const limits = normalizeRuntimeLimits(options.limits);
|
|
1115
|
+
const budget = options.budget ?? createRuntimeBudget(limits);
|
|
1116
|
+
const configuredTransport = options.transport ?? (options.port ? createMessagePortRuntimeTransport(options.port, { limits }) : void 0);
|
|
1117
|
+
if (!configuredTransport) throw new TypeError("MessagePort service provider requires transport or port");
|
|
1118
|
+
const transport = configuredTransport;
|
|
1119
|
+
const activeCalls = /* @__PURE__ */ new Map();
|
|
1120
|
+
const streams = /* @__PURE__ */ new Map();
|
|
1121
|
+
const executionSlots = /* @__PURE__ */ new Map();
|
|
1122
|
+
let peerRetainedPayloadBytes = 0;
|
|
1123
|
+
let disposed = false;
|
|
1124
|
+
const serviceFor = (message) => options.services().find((service) => service.kind === (message.mode === "stream" ? "stream" : "rpc") && service.capabilityId === message.capabilityId && service.contractVersion === message.contractVersion && service.serviceInstanceId === message.serviceInstanceId);
|
|
1125
|
+
const serviceErrorCode = (message) => {
|
|
1126
|
+
const visibleContract = options.services().some((service) => service.kind === (message.mode === "stream" ? "stream" : "rpc") && service.capabilityId === message.capabilityId && service.contractVersion === message.contractVersion);
|
|
1127
|
+
return visibleContract ? "service_stale" : "capability_unavailable";
|
|
1128
|
+
};
|
|
1129
|
+
const sendError = (call, code, phase) => {
|
|
1130
|
+
post(transport, codec, {
|
|
1131
|
+
type: RUNTIME_ERROR_MESSAGE_TYPE,
|
|
1132
|
+
protocolVersion: RUNTIME_PROTOCOL_VERSION,
|
|
1133
|
+
callId: call.callId,
|
|
1134
|
+
serviceInstanceId: call.serviceInstanceId,
|
|
1135
|
+
error: { code, message: safeErrorMessage(code), phase }
|
|
1136
|
+
});
|
|
1137
|
+
};
|
|
1138
|
+
const releaseExecution = (active) => {
|
|
1139
|
+
if (active.executionReleased) return;
|
|
1140
|
+
active.executionReleased = true;
|
|
1141
|
+
if (executionSlots.get(active.call.callId) === active) executionSlots.delete(active.call.callId);
|
|
1142
|
+
budget.releaseExecutionSlot();
|
|
1143
|
+
peerRetainedPayloadBytes = Math.max(0, peerRetainedPayloadBytes - active.requestBytes);
|
|
1144
|
+
budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - active.requestBytes);
|
|
1145
|
+
active.removePeerRevoke?.();
|
|
1146
|
+
active.removePeerRevoke = void 0;
|
|
1147
|
+
};
|
|
1148
|
+
const releaseStreamReservation = (active) => {
|
|
1149
|
+
if (!active.streamReserved) return;
|
|
1150
|
+
active.streamReserved = false;
|
|
1151
|
+
budget.activeStreams = Math.max(0, budget.activeStreams - 1);
|
|
1152
|
+
};
|
|
1153
|
+
const releasePending = (active) => {
|
|
1154
|
+
if (!active.pending) return;
|
|
1155
|
+
active.pending = false;
|
|
1156
|
+
if (activeCalls.get(active.call.callId) === active) activeCalls.delete(active.call.callId);
|
|
1157
|
+
if (active.deadlineTimer !== void 0) {
|
|
1158
|
+
clearTimeout(active.deadlineTimer);
|
|
1159
|
+
active.deadlineTimer = void 0;
|
|
1160
|
+
}
|
|
1161
|
+
budget.pendingCalls = Math.max(0, budget.pendingCalls - 1);
|
|
1162
|
+
};
|
|
1163
|
+
const finishPending = (active) => {
|
|
1164
|
+
releasePending(active);
|
|
1165
|
+
};
|
|
1166
|
+
const maybeReleaseStreamExecution = (stream) => {
|
|
1167
|
+
if (stream.pumpDone && stream.returnDone) releaseExecution(stream.active);
|
|
1168
|
+
};
|
|
1169
|
+
const returnIterator = (stream) => {
|
|
1170
|
+
if (stream.returnStarted) return;
|
|
1171
|
+
stream.returnStarted = true;
|
|
1172
|
+
const candidate = stream.iterator?.return;
|
|
1173
|
+
if (!candidate) {
|
|
1174
|
+
stream.returnDone = true;
|
|
1175
|
+
maybeReleaseStreamExecution(stream);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
let result;
|
|
1179
|
+
try {
|
|
1180
|
+
result = candidate.call(stream.iterator);
|
|
1181
|
+
} catch {
|
|
1182
|
+
stream.returnDone = true;
|
|
1183
|
+
maybeReleaseStreamExecution(stream);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
void Promise.resolve(result).then(() => {
|
|
1187
|
+
stream.returnDone = true;
|
|
1188
|
+
maybeReleaseStreamExecution(stream);
|
|
1189
|
+
}, () => {
|
|
1190
|
+
stream.returnDone = true;
|
|
1191
|
+
maybeReleaseStreamExecution(stream);
|
|
1192
|
+
});
|
|
1193
|
+
};
|
|
1194
|
+
const closeStream = (stream, normalDone = false) => {
|
|
1195
|
+
if (stream.closed) return;
|
|
1196
|
+
stream.closed = true;
|
|
1197
|
+
stream.active.cancelled = true;
|
|
1198
|
+
finishPending(stream.active);
|
|
1199
|
+
streams.delete(stream.call.callId);
|
|
1200
|
+
releaseStreamReservation(stream.active);
|
|
1201
|
+
if (normalDone) {
|
|
1202
|
+
stream.iteratorDone = true;
|
|
1203
|
+
stream.returnDone = true;
|
|
1204
|
+
maybeReleaseStreamExecution(stream);
|
|
1205
|
+
} else {
|
|
1206
|
+
if (!stream.running) stream.pumpDone = true;
|
|
1207
|
+
returnIterator(stream);
|
|
1208
|
+
maybeReleaseStreamExecution(stream);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
const closeLateIterable = async (value) => {
|
|
1212
|
+
try {
|
|
1213
|
+
if (!value || typeof value[Symbol.asyncIterator] !== "function") return;
|
|
1214
|
+
const iterator = value[Symbol.asyncIterator]();
|
|
1215
|
+
const close = iterator.return;
|
|
1216
|
+
if (close) await Promise.resolve(close.call(iterator));
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
const cancelActive = (active, errorCode = "request_cancelled") => {
|
|
1221
|
+
if (active.cancelled) return;
|
|
1222
|
+
active.frameworkSettled = true;
|
|
1223
|
+
active.cancelled = true;
|
|
1224
|
+
try {
|
|
1225
|
+
active.controller.abort();
|
|
1226
|
+
} catch {
|
|
1227
|
+
}
|
|
1228
|
+
const stream = streams.get(active.call.callId);
|
|
1229
|
+
if (stream) closeStream(stream);
|
|
1230
|
+
else finishPending(active);
|
|
1231
|
+
};
|
|
1232
|
+
const reserve = (requestBytes, stream) => {
|
|
1233
|
+
if (activeCalls.size >= limits.maxPendingCallsPerPeer || budget.pendingCalls >= limits.maxPendingCallsPerRuntime) return new WebLoomError("resource_limit_exceeded", safeErrorMessage("resource_limit_exceeded"), "dispatch");
|
|
1234
|
+
if (executionSlots.size >= limits.maxExecutionSlotsPerPeer || budget.executionSlots >= limits.maxExecutionSlotsPerRuntime) return new WebLoomError("resource_limit_exceeded", safeErrorMessage("resource_limit_exceeded"), "dispatch");
|
|
1235
|
+
if (stream && (streams.size >= limits.maxActiveStreamsPerPeer || budget.activeStreams >= limits.maxActiveStreamsPerRuntime)) return new WebLoomError("resource_limit_exceeded", safeErrorMessage("resource_limit_exceeded"), "dispatch");
|
|
1236
|
+
if (requestBytes > limits.maxMessageBudgetBytes || peerRetainedPayloadBytes > limits.maxRetainedPayloadBytesPerPeer - requestBytes || budget.retainedPayloadBytes > limits.maxRetainedPayloadBytesPerRuntime - requestBytes) return new WebLoomError("resource_limit_exceeded", safeErrorMessage("resource_limit_exceeded"), "dispatch");
|
|
1237
|
+
budget.pendingCalls += 1;
|
|
1238
|
+
if (stream) budget.activeStreams += 1;
|
|
1239
|
+
budget.executionSlots += 1;
|
|
1240
|
+
peerRetainedPayloadBytes += requestBytes;
|
|
1241
|
+
budget.retainedPayloadBytes += requestBytes;
|
|
1242
|
+
return void 0;
|
|
1243
|
+
};
|
|
1244
|
+
const prepareRequest = (call, ledger) => {
|
|
1245
|
+
try {
|
|
1246
|
+
validateRawDto(call.request, limits, "receive");
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
ledger.closeUndelivered();
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
let prepared;
|
|
1252
|
+
try {
|
|
1253
|
+
prepared = options.prepareRequest?.(call, ledger) ?? { value: call.request };
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
ledger.closeUndelivered();
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
let transfer;
|
|
1259
|
+
try {
|
|
1260
|
+
transfer = validateTransferList(prepared.value, prepared.transfer, { limits, phase: "receive" });
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
ledger.closeUndelivered();
|
|
1263
|
+
throw error;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
assertReceivedPortSet(ledger, transfer, { limits, phase: "receive" });
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
ledger.closeUndelivered();
|
|
1269
|
+
throw error;
|
|
1270
|
+
}
|
|
1271
|
+
const stats = validateDto(prepared.value, { limits: { maxDepth: limits.maxDtoDepth, maxNodes: limits.maxDtoNodes, maxEdges: limits.maxDtoEdges, maxBudgetBytes: limits.maxMessageBudgetBytes }, transferables: new Set(transfer), phase: "receive" });
|
|
1272
|
+
return { value: prepared.value, budgetBytes: stats.budgetBytes };
|
|
1273
|
+
};
|
|
1274
|
+
const prepareOutput = (prepared, ledger) => {
|
|
1275
|
+
try {
|
|
1276
|
+
const transfer = validateTransferList(prepared.value, prepared.transfer, { limits, phase: "receive" });
|
|
1277
|
+
if (ledger) ;
|
|
1278
|
+
validateDto(prepared.value, { limits: { maxDepth: limits.maxDtoDepth, maxNodes: limits.maxDtoNodes, maxEdges: limits.maxDtoEdges, maxBudgetBytes: limits.maxMessageBudgetBytes }, transferables: new Set(transfer), phase: "receive" });
|
|
1279
|
+
return { value: prepared.value, transfer };
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
throw error;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
const pump = async (entry) => {
|
|
1285
|
+
if (entry.running || entry.closed || !entry.iterator) return;
|
|
1286
|
+
entry.running = true;
|
|
1287
|
+
try {
|
|
1288
|
+
while (!entry.closed && !entry.active.cancelled && entry.credit > 0) {
|
|
1289
|
+
let next;
|
|
1290
|
+
try {
|
|
1291
|
+
next = await entry.iterator.next();
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (!entry.closed && !entry.active.cancelled) sendError(entry.call, safeErrorCode(error, "handler_failed"), "execute");
|
|
1294
|
+
closeStream(entry);
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
if (entry.closed || entry.active.cancelled) break;
|
|
1298
|
+
if (next.done) {
|
|
1299
|
+
entry.iteratorDone = true;
|
|
1300
|
+
if (post(transport, codec, { type: RUNTIME_RESULT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: entry.call.callId, serviceInstanceId: entry.reference.serviceInstanceId, done: true })) closeStream(entry, true);
|
|
1301
|
+
else {
|
|
1302
|
+
sendError(entry.call, "transport_unavailable", "receive");
|
|
1303
|
+
closeStream(entry, true);
|
|
1304
|
+
}
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
const prepared = options.prepareItem?.(next.value, entry.call) ?? { value: next.value };
|
|
1309
|
+
const output = prepareOutput(prepared);
|
|
1310
|
+
if (!post(transport, codec, { type: RUNTIME_NEXT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: entry.call.callId, serviceInstanceId: entry.reference.serviceInstanceId, sequence: entry.sequence, item: output.value }, output.transfer)) {
|
|
1311
|
+
sendError(entry.call, "response_clone_failed", "receive");
|
|
1312
|
+
closeStream(entry);
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
entry.credit -= 1;
|
|
1316
|
+
entry.sequence += 1;
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
if (!entry.closed && !entry.active.cancelled) sendError(entry.call, safeErrorCode(error, "response_validation_failed"), "receive");
|
|
1319
|
+
closeStream(entry);
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
} finally {
|
|
1324
|
+
entry.running = false;
|
|
1325
|
+
entry.pumpDone = true;
|
|
1326
|
+
maybeReleaseStreamExecution(entry);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
const onMessage = (message, metadata) => {
|
|
1330
|
+
const ledger = metadata?.ledger ?? createReceivePortLedger(metadata?.ports, { limits, phase: "receive" });
|
|
1331
|
+
if (!ledger.valid) {
|
|
1332
|
+
failClose();
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (message.type === RUNTIME_CANCEL_TYPE) {
|
|
1336
|
+
ledger.closeUndelivered();
|
|
1337
|
+
const active2 = activeCalls.get(message.callId) ?? streams.get(message.callId)?.active;
|
|
1338
|
+
if (!active2 || active2.reference.serviceInstanceId !== message.serviceInstanceId) return;
|
|
1339
|
+
cancelActive(active2);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (message.type === RUNTIME_CREDIT_TYPE) {
|
|
1343
|
+
ledger.closeUndelivered();
|
|
1344
|
+
const stream = streams.get(message.callId);
|
|
1345
|
+
const valid = stream !== void 0 && stream.reference.serviceInstanceId === message.serviceInstanceId && Number.isSafeInteger(message.count) && message.count >= 1 && message.count <= limits.maxStreamCredit && stream.credit + message.count <= stream.window;
|
|
1346
|
+
if (!valid) {
|
|
1347
|
+
if (stream && !stream.active.cancelled) sendError(stream.call, "stream_overflow", "receive");
|
|
1348
|
+
if (stream) closeStream(stream);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
stream.credit += message.count;
|
|
1352
|
+
void pump(stream);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (message.type !== RUNTIME_CALL_TYPE) return;
|
|
1356
|
+
const reference = serviceFor(message);
|
|
1357
|
+
if (!reference) {
|
|
1358
|
+
ledger.closeUndelivered();
|
|
1359
|
+
sendError(message, serviceErrorCode(message), "dispatch");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (activeCalls.has(message.callId) || executionSlots.has(message.callId)) {
|
|
1363
|
+
ledger.closeUndelivered();
|
|
1364
|
+
sendError(message, "invalid_message", "dispatch");
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (message.grantId !== void 0 && message.grantId !== reference.grantId) {
|
|
1368
|
+
ledger.closeUndelivered();
|
|
1369
|
+
sendError(message, "permission_denied", "dispatch");
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const initialCredit = message.initialCredit;
|
|
1373
|
+
if (message.mode === "stream" && (!Number.isSafeInteger(initialCredit) || initialCredit < 1 || initialCredit > limits.maxStreamCredit)) {
|
|
1374
|
+
ledger.closeUndelivered();
|
|
1375
|
+
sendError(message, "stream_overflow", "validate");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
let request;
|
|
1379
|
+
try {
|
|
1380
|
+
request = prepareRequest(message, ledger);
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
sendError(message, safeErrorCode(error, "request_validation_failed"), error instanceof WebLoomError ? error.phase : "receive");
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const quotaError = reserve(request.budgetBytes, message.mode === "stream");
|
|
1386
|
+
if (quotaError) {
|
|
1387
|
+
ledger.closeUndelivered();
|
|
1388
|
+
sendError(message, quotaError.code, "dispatch");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
ledger.handoff();
|
|
1392
|
+
const controller = new AbortController();
|
|
1393
|
+
const active = { call: { ...message, request: request.value }, controller, reference, requestBytes: request.budgetBytes, deadlineAt: Date.now() + message.timeoutMs, pending: true, streamReserved: message.mode === "stream", cancelled: false, frameworkSettled: false, executionReleased: false };
|
|
1394
|
+
activeCalls.set(message.callId, active);
|
|
1395
|
+
executionSlots.set(message.callId, active);
|
|
1396
|
+
if (options.peerScope) active.removePeerRevoke = options.peerScope.onRevoke(() => cancelActive(active, "service_revoked"));
|
|
1397
|
+
active.deadlineTimer = setTimeout(() => cancelActive(active, "call_timeout"), message.timeoutMs);
|
|
1398
|
+
const callPeer = options.peerForCall?.(active.call, reference) ?? options.peer;
|
|
1399
|
+
void (async () => {
|
|
1400
|
+
let stream;
|
|
1401
|
+
try {
|
|
1402
|
+
if (active.cancelled || disposed) {
|
|
1403
|
+
releaseExecution(active);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const result = await options.handleCall({ message: active.call, request: request.value, reference, signal: controller.signal, deadlineAt: active.deadlineAt, peer: callPeer });
|
|
1407
|
+
if (active.cancelled || disposed || executionSlots.get(message.callId) !== active) {
|
|
1408
|
+
if (message.mode === "stream") await closeLateIterable(result);
|
|
1409
|
+
releaseExecution(active);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (message.mode === "unary") {
|
|
1413
|
+
try {
|
|
1414
|
+
const output = prepareOutput(options.prepareResult?.(result, active.call) ?? { value: result });
|
|
1415
|
+
if (!active.cancelled && !post(transport, codec, { type: RUNTIME_RESULT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: message.callId, serviceInstanceId: reference.serviceInstanceId, result: output.value }, output.transfer)) sendError(message, "response_clone_failed", "receive");
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
if (!active.cancelled) sendError(message, safeErrorCode(error, "response_validation_failed"), "receive");
|
|
1418
|
+
}
|
|
1419
|
+
finishPending(active);
|
|
1420
|
+
releaseExecution(active);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (!result || typeof result[Symbol.asyncIterator] !== "function") throw new WebLoomError("handler_failed", "Stream handler did not return AsyncIterable", "execute");
|
|
1424
|
+
const iterator = result[Symbol.asyncIterator]();
|
|
1425
|
+
stream = { active, call: active.call, controller, reference, window: message.initialCredit ?? 16, iterator, credit: message.initialCredit ?? 16, sequence: 1, running: false, closed: false, iteratorDone: false, returnStarted: false, returnDone: false, pumpDone: false };
|
|
1426
|
+
streams.set(message.callId, stream);
|
|
1427
|
+
if (!post(transport, codec, { type: RUNTIME_RESULT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: message.callId, serviceInstanceId: reference.serviceInstanceId, streamReady: true })) {
|
|
1428
|
+
sendError(message, "transport_unavailable", "dispatch");
|
|
1429
|
+
closeStream(stream);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
releasePending(active);
|
|
1433
|
+
void pump(stream);
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
if (!active.cancelled && !disposed) sendError(message, safeErrorCode(error, "handler_failed"), error instanceof WebLoomError ? error.phase : "execute");
|
|
1436
|
+
if (stream) closeStream(stream);
|
|
1437
|
+
else {
|
|
1438
|
+
finishPending(active);
|
|
1439
|
+
releaseExecution(active);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
})().catch(() => {
|
|
1443
|
+
const lateStream = streams.get(active.call.callId);
|
|
1444
|
+
if (lateStream) closeStream(lateStream);
|
|
1445
|
+
else {
|
|
1446
|
+
finishPending(active);
|
|
1447
|
+
releaseExecution(active);
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
};
|
|
1451
|
+
function dispose() {
|
|
1452
|
+
if (disposed) return;
|
|
1453
|
+
disposed = true;
|
|
1454
|
+
for (const active of [...activeCalls.values()]) cancelActive(active, "service_revoked");
|
|
1455
|
+
for (const stream of [...streams.values()]) closeStream(stream);
|
|
1456
|
+
removeTransport();
|
|
1457
|
+
}
|
|
1458
|
+
function failClose() {
|
|
1459
|
+
dispose();
|
|
1460
|
+
try {
|
|
1461
|
+
transport.close?.();
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const removeTransport = transport.subscribe(onMessage);
|
|
1466
|
+
return {
|
|
1467
|
+
setServices() {
|
|
1468
|
+
},
|
|
1469
|
+
dispose,
|
|
1470
|
+
pendingCount() {
|
|
1471
|
+
return activeCalls.size;
|
|
1472
|
+
},
|
|
1473
|
+
executionCount() {
|
|
1474
|
+
return executionSlots.size;
|
|
1475
|
+
},
|
|
1476
|
+
nonCooperativeExecutionCount() {
|
|
1477
|
+
return [...executionSlots.values()].filter((active) => active.frameworkSettled).length;
|
|
1478
|
+
},
|
|
1479
|
+
retainedPayloadBytes() {
|
|
1480
|
+
return budget.retainedPayloadBytes;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/runtime/peerView.ts
|
|
1486
|
+
function createPeerScopeView(scope) {
|
|
1487
|
+
return Object.freeze({
|
|
1488
|
+
get state() {
|
|
1489
|
+
return scope.state;
|
|
1490
|
+
},
|
|
1491
|
+
signal: scope.signal,
|
|
1492
|
+
onRevoke(listener) {
|
|
1493
|
+
return scope.onRevoke(listener);
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
function createCapabilityPeerView(options) {
|
|
1498
|
+
const allowed = options.allowed === void 0 ? void 0 : new Set(options.allowed.map((descriptor) => capabilityKey(descriptor)));
|
|
1499
|
+
return Object.freeze({
|
|
1500
|
+
peerId: options.peerId,
|
|
1501
|
+
get runtime() {
|
|
1502
|
+
return options.bridge.runtimeKind;
|
|
1503
|
+
},
|
|
1504
|
+
get runtimeInstanceId() {
|
|
1505
|
+
return options.bridge.runtimeInstanceId;
|
|
1506
|
+
},
|
|
1507
|
+
scope: options.scope,
|
|
1508
|
+
capability(capability) {
|
|
1509
|
+
if (allowed && !allowed.has(capabilityKey(capability))) {
|
|
1510
|
+
throw new WebLoomError("capability_unavailable", "Capability is unavailable", "dispatch", { capabilityId: capability.id });
|
|
1511
|
+
}
|
|
1512
|
+
return options.bridge.getClient(capability, options.capabilityScope);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/runtime/connectSharedWorker.ts
|
|
1518
|
+
var handleBridges = /* @__PURE__ */ new WeakMap();
|
|
1519
|
+
function makeWorker(options) {
|
|
1520
|
+
const workerOptions = { type: "module", ...options.name !== void 0 ? { name: options.name } : {}, ...options.credentials !== void 0 ? { credentials: options.credentials } : {} };
|
|
1521
|
+
if (options.workerFactory) return options.workerFactory(options.url, workerOptions);
|
|
1522
|
+
const WorkerConstructor = globalThis.SharedWorker;
|
|
1523
|
+
if (!WorkerConstructor) throw new RuntimeUnavailableError("SharedWorker is not supported by this browser");
|
|
1524
|
+
return new WorkerConstructor(options.url, workerOptions);
|
|
1525
|
+
}
|
|
1526
|
+
function snapshotForClient(app, runtimeId, runtimeInstanceId, exposed, revision) {
|
|
1527
|
+
const allowed = new Set(exposed.map((capability) => capabilityKey(capability)));
|
|
1528
|
+
const state = app.state();
|
|
1529
|
+
const runtimeState = state.state === "disconnected" ? "failed" : state.state;
|
|
1530
|
+
return {
|
|
1531
|
+
type: RUNTIME_SNAPSHOT_TYPE,
|
|
1532
|
+
protocolVersion: RUNTIME_PROTOCOL_VERSION,
|
|
1533
|
+
runtimeId,
|
|
1534
|
+
runtimeKind: "window-main",
|
|
1535
|
+
runtimeInstanceId,
|
|
1536
|
+
revision,
|
|
1537
|
+
state: runtimeState,
|
|
1538
|
+
units: state.units,
|
|
1539
|
+
services: runtimeState === "ready" ? state.services.filter((service) => allowed.has(capabilityKey({ kind: service.kind, id: service.capabilityId, version: service.contractVersion }))).map((service) => ({ kind: service.kind, capabilityId: service.capabilityId, contractVersion: service.contractVersion, serviceInstanceId: service.serviceInstanceId, attributes: cloneFrozenAttributes(service.attributes), ...service.grantId !== void 0 ? { grantId: service.grantId } : {}, ...service.authorizationRevision !== void 0 ? { authorizationRevision: service.authorizationRevision } : {} })) : []
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function connectInternal(options) {
|
|
1543
|
+
if (!options || typeof options.id !== "string" || options.id.trim() === "") throw new TypeError("SharedWorker runtime id must be a non-empty string");
|
|
1544
|
+
const requestedClientHost = options.client ? hostForWindowApp(options.client.app) : void 0;
|
|
1545
|
+
const exposed = options.client?.expose ?? [];
|
|
1546
|
+
for (const capability of exposed) {
|
|
1547
|
+
if (capability.kind !== "rpc" && capability.kind !== "stream") throw new TypeError(`Capability "${capability.id ?? "unknown"}" is not remotely exposable`);
|
|
1548
|
+
if (!requestedClientHost?.capabilities.registration(capability)) throw new TypeError(`Capability "${capability.id}" is not registered by the WindowApp`);
|
|
1549
|
+
}
|
|
1550
|
+
const worker = makeWorker(options);
|
|
1551
|
+
const port = worker.port;
|
|
1552
|
+
if (!port) throw new RuntimeUnavailableError("SharedWorker did not expose a MessagePort");
|
|
1553
|
+
const limits = normalizeRuntimeLimits(options.limits);
|
|
1554
|
+
const transport = createMessagePortRuntimeTransport({
|
|
1555
|
+
addEventListener(type, listener) {
|
|
1556
|
+
port.addEventListener(type, listener);
|
|
1557
|
+
},
|
|
1558
|
+
removeEventListener(type, listener) {
|
|
1559
|
+
port.removeEventListener(type, listener);
|
|
1560
|
+
},
|
|
1561
|
+
postMessage(messageValue, transfer) {
|
|
1562
|
+
port.postMessage(messageValue, transfer ? [...transfer] : void 0);
|
|
1563
|
+
},
|
|
1564
|
+
start() {
|
|
1565
|
+
port.start();
|
|
1566
|
+
},
|
|
1567
|
+
close() {
|
|
1568
|
+
try {
|
|
1569
|
+
port.close();
|
|
1570
|
+
} catch {
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}, { limits });
|
|
1574
|
+
const outboundBudget = createRuntimeBudget(limits);
|
|
1575
|
+
const inboundBudget = createRuntimeBudget(limits);
|
|
1576
|
+
const bridge = createCapabilityBridge({ transport, remoteRuntimeKind: "shared-worker", remoteRuntimeId: options.id, defaultCallTimeoutMs: options.defaultCallTimeoutMs, limits, budget: outboundBudget });
|
|
1577
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1578
|
+
const workerInstanceId = { value: "" };
|
|
1579
|
+
const localRuntimeInstanceId = options.client?.app.runtimeInstanceId ?? `window:${Date.now().toString(36)}`;
|
|
1580
|
+
let revision = 0;
|
|
1581
|
+
let disposed = false;
|
|
1582
|
+
let current = Object.freeze({ protocolVersion: RUNTIME_PROTOCOL_VERSION, runtimeId: options.id, runtimeKind: "shared-worker", runtimeInstanceId: "", state: "starting", revision: 0, units: [], services: [] });
|
|
1583
|
+
const clientHost = requestedClientHost;
|
|
1584
|
+
const clientPeerScope = options.client && clientHost ? clientHost.rootScope.child("peer", { attributes: { peerId: `peer:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}` } }) : void 0;
|
|
1585
|
+
const clientPeerScopeView = clientPeerScope ? createPeerScopeView(clientPeerScope) : void 0;
|
|
1586
|
+
const clientPeer = clientPeerScope && clientPeerScopeView ? createCapabilityPeerView({
|
|
1587
|
+
peerId: clientPeerScope.identity.attributes.peerId,
|
|
1588
|
+
scope: clientPeerScopeView,
|
|
1589
|
+
bridge,
|
|
1590
|
+
capabilityScope: clientPeerScope
|
|
1591
|
+
}) : void 0;
|
|
1592
|
+
const provider = options.client && clientHost ? createMessagePortServiceProvider({
|
|
1593
|
+
transport,
|
|
1594
|
+
peerScope: clientPeerScope,
|
|
1595
|
+
peer: clientPeer,
|
|
1596
|
+
budget: inboundBudget,
|
|
1597
|
+
limits,
|
|
1598
|
+
services: () => clientHost.serviceReferences().filter((service) => exposed.some((capability) => capabilityKey(capability) === capabilityKey({ kind: service.kind, id: service.capabilityId, version: service.contractVersion }))),
|
|
1599
|
+
peerForCall: (_call, reference) => {
|
|
1600
|
+
const registration = clientHost.capabilities.registration({ kind: reference.kind, id: reference.capabilityId, version: reference.contractVersion });
|
|
1601
|
+
return clientPeerScope && clientPeerScopeView ? createCapabilityPeerView({
|
|
1602
|
+
peerId: clientPeerScope.identity.attributes.peerId,
|
|
1603
|
+
scope: clientPeerScopeView,
|
|
1604
|
+
bridge,
|
|
1605
|
+
allowed: registration?.peerDependencies ?? [],
|
|
1606
|
+
capabilityScope: clientPeerScope
|
|
1607
|
+
}) : void 0;
|
|
1608
|
+
},
|
|
1609
|
+
prepareRequest: (call) => {
|
|
1610
|
+
const registration = clientHost.capabilities.registration({ kind: call.mode === "stream" ? "stream" : "rpc", id: call.capabilityId, version: call.contractVersion });
|
|
1611
|
+
if (!registration || registration.capability.kind !== (call.mode === "stream" ? "stream" : "rpc")) throw new WebLoomError("service_stale", "Window service exposure is stale", "dispatch");
|
|
1612
|
+
const capability = registration.capability;
|
|
1613
|
+
let value;
|
|
1614
|
+
try {
|
|
1615
|
+
value = capability.request.parse(call.request);
|
|
1616
|
+
} catch {
|
|
1617
|
+
throw new WebLoomError("request_validation_failed", "Capability request failed validation", "receive");
|
|
1618
|
+
}
|
|
1619
|
+
return { value, transfer: capability.transfer?.request?.(value) };
|
|
1620
|
+
},
|
|
1621
|
+
handleCall: async ({ request, reference, signal, deadlineAt, peer }) => {
|
|
1622
|
+
const registration = clientHost.capabilities.registration({ kind: reference.kind, id: reference.capabilityId, version: reference.contractVersion });
|
|
1623
|
+
if (!registration) throw new WebLoomError("service_stale", "Window exposure is no longer available", "dispatch");
|
|
1624
|
+
const handler = registration.handler;
|
|
1625
|
+
if (!handler) throw new WebLoomError("service_stale", "Window service handler is unavailable", "dispatch");
|
|
1626
|
+
return handler(request, { signal, deadlineAt, reference, origin: "remote", peer });
|
|
1627
|
+
},
|
|
1628
|
+
prepareResult: (value, call) => {
|
|
1629
|
+
const registration = clientHost.capabilities.registration({ kind: "rpc", id: call.capabilityId, version: call.contractVersion });
|
|
1630
|
+
if (!registration || registration.capability.kind !== "rpc") throw new Error("Window RPC registration disappeared");
|
|
1631
|
+
const rpc = registration.capability;
|
|
1632
|
+
const parsed = rpc.response.parse(value);
|
|
1633
|
+
return { value: parsed, transfer: rpc.transfer?.response?.(parsed) };
|
|
1634
|
+
},
|
|
1635
|
+
prepareItem: (value, call) => {
|
|
1636
|
+
const registration = clientHost.capabilities.registration({ kind: "stream", id: call.capabilityId, version: call.contractVersion });
|
|
1637
|
+
if (!registration || registration.capability.kind !== "stream") throw new Error("Window stream registration disappeared");
|
|
1638
|
+
const stream = registration.capability;
|
|
1639
|
+
const parsed = stream.item.parse(value);
|
|
1640
|
+
return { value: parsed, transfer: stream.transfer?.item?.(parsed) };
|
|
1641
|
+
}
|
|
1642
|
+
}) : void 0;
|
|
1643
|
+
const emit = (next) => {
|
|
1644
|
+
current = Object.freeze({ ...next, units: Object.freeze([...next.units]), services: Object.freeze([...next.services]) });
|
|
1645
|
+
for (const listener of [...listeners]) {
|
|
1646
|
+
try {
|
|
1647
|
+
listener(current);
|
|
1648
|
+
} catch {
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
const sendClientSnapshot = () => {
|
|
1653
|
+
if (!options.client || disposed) return;
|
|
1654
|
+
revision += 1;
|
|
1655
|
+
try {
|
|
1656
|
+
transport.send(snapshotForClient(options.client.app, `${options.client.app.runtimeId}`, localRuntimeInstanceId, exposed, revision));
|
|
1657
|
+
} catch {
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
const removeRaw = transport.subscribe((messageValue) => {
|
|
1661
|
+
if (messageValue.type === RUNTIME_SNAPSHOT_TYPE && messageValue.runtimeKind === "shared-worker") {
|
|
1662
|
+
const applied = bridge.applySnapshot(messageValue);
|
|
1663
|
+
if (applied.accepted) {
|
|
1664
|
+
workerInstanceId.value = messageValue.runtimeInstanceId;
|
|
1665
|
+
emit({ protocolVersion: RUNTIME_PROTOCOL_VERSION, runtimeId: messageValue.runtimeId, runtimeKind: messageValue.runtimeKind, runtimeInstanceId: messageValue.runtimeInstanceId, state: messageValue.state, revision: messageValue.revision, units: messageValue.units, services: messageValue.services });
|
|
1666
|
+
}
|
|
1667
|
+
} else if (messageValue.type === RUNTIME_ERROR_TYPE) {
|
|
1668
|
+
bridge.invalidate(messageValue.message);
|
|
1669
|
+
emit({ ...current, state: "failed", error: messageValue.message, units: [], services: [] });
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
const onError = () => {
|
|
1673
|
+
if (!disposed) {
|
|
1674
|
+
clientPeerScope?.revoke("SharedWorker disconnected");
|
|
1675
|
+
provider?.dispose();
|
|
1676
|
+
bridge.disconnect("SharedWorker disconnected");
|
|
1677
|
+
emit({ ...current, state: "disconnected", runtimeInstanceId: "", revision: 0, units: [], services: [], error: "SharedWorker disconnected" });
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
port.addEventListener("messageerror", onError);
|
|
1681
|
+
if (worker.addEventListener) worker.addEventListener("error", onError);
|
|
1682
|
+
else worker.onerror = onError;
|
|
1683
|
+
sendClientSnapshot();
|
|
1684
|
+
const removeClient = options.client?.app.subscribe(() => sendClientSnapshot());
|
|
1685
|
+
const handle = {
|
|
1686
|
+
runtimeKind: "shared-worker",
|
|
1687
|
+
runtimeId: options.id,
|
|
1688
|
+
get runtimeInstanceId() {
|
|
1689
|
+
return workerInstanceId.value;
|
|
1690
|
+
},
|
|
1691
|
+
state: () => current,
|
|
1692
|
+
capability(capability) {
|
|
1693
|
+
return bridge.getClient(capability);
|
|
1694
|
+
},
|
|
1695
|
+
optionalCapability(capability) {
|
|
1696
|
+
return bridge.services().some((service) => service.kind === capability.kind && service.capabilityId === capability.id && service.contractVersion === capability.version) ? bridge.getClient(capability) : void 0;
|
|
1697
|
+
},
|
|
1698
|
+
inspect: () => Object.freeze({ ...current, pendingCallCount: bridge.pendingCallCount, activeStreamCount: bridge.activeStreamCount, peerCount: current.state === "ready" ? 1 : 0 }),
|
|
1699
|
+
subscribe(listener) {
|
|
1700
|
+
listeners.add(listener);
|
|
1701
|
+
listener(current);
|
|
1702
|
+
return () => listeners.delete(listener);
|
|
1703
|
+
},
|
|
1704
|
+
dispose(reason = "SharedWorker connection disposed") {
|
|
1705
|
+
if (disposed) return Promise.resolve();
|
|
1706
|
+
disposed = true;
|
|
1707
|
+
emit({ ...current, state: "stopping" });
|
|
1708
|
+
removeClient?.();
|
|
1709
|
+
removeRaw();
|
|
1710
|
+
clientPeerScope?.revoke(reason);
|
|
1711
|
+
provider?.dispose();
|
|
1712
|
+
bridge.dispose(reason);
|
|
1713
|
+
port.removeEventListener("messageerror", onError);
|
|
1714
|
+
worker.removeEventListener?.("error", onError);
|
|
1715
|
+
transport.close?.();
|
|
1716
|
+
emit({ ...current, state: "disposed", runtimeInstanceId: "", revision: 0, units: [], services: [] });
|
|
1717
|
+
return clientPeerScope ? clientPeerScope.dispose({ reason }).then(() => void 0) : Promise.resolve();
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
handleBridges.set(handle, bridge);
|
|
1721
|
+
return handle;
|
|
1722
|
+
}
|
|
1723
|
+
function connectSharedWorker(options) {
|
|
1724
|
+
return connectInternal(options);
|
|
1725
|
+
}
|
|
1726
|
+
function connectSharedWorkerForTesting(options, workerFactory) {
|
|
1727
|
+
return connectInternal({ ...options, workerFactory });
|
|
1728
|
+
}
|
|
1729
|
+
function bridgeForRuntimeHandle(handle) {
|
|
1730
|
+
const bridge = handleBridges.get(handle);
|
|
1731
|
+
if (!bridge) throw new Error("RuntimeHandle is not owned by WebLoom");
|
|
1732
|
+
return bridge;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
export { bridgeForRuntimeHandle, connectSharedWorker, connectSharedWorkerForTesting, createCapabilityBridge, createCapabilityPeerView, createMessagePortRuntimeTransport, createMessagePortServiceProvider, createPeerScopeView, validateTransferables };
|
|
1736
|
+
//# sourceMappingURL=chunk-RAQOFUZY.js.map
|
|
1737
|
+
//# sourceMappingURL=chunk-RAQOFUZY.js.map
|