webloom-framework 0.4.1 → 0.4.3
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 +11 -1
- package/dist/advanced.d.ts +19 -7
- package/dist/advanced.js +4 -4
- package/dist/advanced.js.map +1 -1
- package/dist/{chunk-SX46RHDI.js → chunk-UZAO6ORB.js} +205 -41
- package/dist/chunk-UZAO6ORB.js.map +1 -0
- package/dist/{chunk-ANA6GBEI.js → chunk-YIIDRFHK.js} +553 -47
- package/dist/chunk-YIIDRFHK.js.map +1 -0
- package/dist/{chunk-HJKPKWI7.js → chunk-ZP5QYTXX.js} +19 -4
- package/dist/chunk-ZP5QYTXX.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/{messageBus-CtrwkjrO.d.ts → messageBus-DigYfj74.d.ts} +1 -1
- package/dist/{messagePortServiceTransport-BYprNvQY.d.ts → messagePortServiceTransport-B9-24RK6.d.ts} +95 -4
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{runtimeTypes-DquUCHz-.d.ts → runtimeTypes-50LVQs0h.d.ts} +55 -2
- package/dist/{sharedWorkerHost-KI7TIGdX.d.ts → sharedWorkerHost-DikS18hA.d.ts} +88 -2
- package/dist/testing.d.ts +6 -6
- package/dist/testing.js +4 -4
- package/dist/{windowRuntime-BKkLPsAS.d.ts → windowRuntime-e1Tn6SnN.d.ts} +2 -2
- package/docs/api.md +28 -1
- package/docs/proposals/webloom-v4/requirements.md +1 -1
- package/docs/proposals/webloom-v4/verification.md +26 -11
- package/package.json +1 -1
- package/dist/chunk-ANA6GBEI.js.map +0 -1
- package/dist/chunk-HJKPKWI7.js.map +0 -1
- package/dist/chunk-SX46RHDI.js.map +0 -1
|
@@ -1,4 +1,171 @@
|
|
|
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,
|
|
1
|
+
import { createRuntimeMessageCodec, normalizeRuntimeLimits, createRuntimeBudget, WebLoomError, validateTransferList, DEFAULT_RUNTIME_LIMITS, capabilityKey, RUNTIME_PROTOCOL_VERSION, RUNTIME_CLOSE_TYPE, createReceivePortLedger, RUNTIME_CLOSE_ACK_TYPE, 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, validateDto, cloneFrozenAttributes, hostForWindowApp, RuntimeUnavailableError, validateRawDto, validateTransferListWithStats, assertReceivedPortSet } from './chunk-ZP5QYTXX.js';
|
|
2
|
+
|
|
3
|
+
// src/runtime/runtimeSession.ts
|
|
4
|
+
var DEFAULT_RUNTIME_DRAIN_TIMEOUT_MS = 2e3;
|
|
5
|
+
function validText(value) {
|
|
6
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256;
|
|
7
|
+
}
|
|
8
|
+
function isRuntimeEndpointBinding(value) {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
10
|
+
const candidate = value;
|
|
11
|
+
return validText(candidate.runtimeInstanceId) && validText(candidate.connectionId);
|
|
12
|
+
}
|
|
13
|
+
function sameRuntimeEndpointBinding(left, right) {
|
|
14
|
+
return left !== void 0 && right !== void 0 && left.runtimeInstanceId === right.runtimeInstanceId && left.connectionId === right.connectionId;
|
|
15
|
+
}
|
|
16
|
+
function copyBinding(value) {
|
|
17
|
+
return Object.freeze({ runtimeInstanceId: value.runtimeInstanceId, connectionId: value.connectionId });
|
|
18
|
+
}
|
|
19
|
+
function createRuntimeEndpointBinding(runtimeInstanceId) {
|
|
20
|
+
let connectionId;
|
|
21
|
+
try {
|
|
22
|
+
connectionId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? `connection:${crypto.randomUUID()}` : `connection:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
23
|
+
} catch {
|
|
24
|
+
connectionId = `connection:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
25
|
+
}
|
|
26
|
+
if (!validText(runtimeInstanceId)) throw new TypeError("Runtime endpoint requires a valid runtimeInstanceId");
|
|
27
|
+
return copyBinding({ runtimeInstanceId, connectionId });
|
|
28
|
+
}
|
|
29
|
+
function createRuntimeEndpointSession(binding, options = {}) {
|
|
30
|
+
if (!isRuntimeEndpointBinding(binding)) throw new TypeError("Runtime endpoint binding is invalid");
|
|
31
|
+
const localBinding = copyBinding(binding);
|
|
32
|
+
const beginCloseListeners = /* @__PURE__ */ new Set();
|
|
33
|
+
const closedListeners = /* @__PURE__ */ new Set();
|
|
34
|
+
const participants = /* @__PURE__ */ new Set();
|
|
35
|
+
const defaultTimeout = options.defaultDrainTimeoutMs ?? DEFAULT_RUNTIME_DRAIN_TIMEOUT_MS;
|
|
36
|
+
if (!Number.isFinite(defaultTimeout) || defaultTimeout < 1 || defaultTimeout > 3e5) {
|
|
37
|
+
throw new TypeError("defaultDrainTimeoutMs must be a finite number from 1 to 300000");
|
|
38
|
+
}
|
|
39
|
+
let state = "active";
|
|
40
|
+
let remoteBindingValue;
|
|
41
|
+
let closeReasonValue;
|
|
42
|
+
let drainPromise;
|
|
43
|
+
const session = {
|
|
44
|
+
binding: localBinding,
|
|
45
|
+
maxDrainTimeoutMs: defaultTimeout,
|
|
46
|
+
get remoteBinding() {
|
|
47
|
+
return remoteBindingValue;
|
|
48
|
+
},
|
|
49
|
+
get state() {
|
|
50
|
+
return state;
|
|
51
|
+
},
|
|
52
|
+
get closeReason() {
|
|
53
|
+
return closeReasonValue;
|
|
54
|
+
},
|
|
55
|
+
acceptRemoteBinding(value) {
|
|
56
|
+
if (!isRuntimeEndpointBinding(value) || state === "closed") return false;
|
|
57
|
+
const next = copyBinding(value);
|
|
58
|
+
if (remoteBindingValue === void 0) {
|
|
59
|
+
remoteBindingValue = next;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return sameRuntimeEndpointBinding(remoteBindingValue, next);
|
|
63
|
+
},
|
|
64
|
+
beginClose(reason = "Runtime endpoint closing") {
|
|
65
|
+
if (state !== "active") return;
|
|
66
|
+
state = "closing";
|
|
67
|
+
closeReasonValue = reason.slice(0, 256);
|
|
68
|
+
for (const listener of [...beginCloseListeners]) {
|
|
69
|
+
try {
|
|
70
|
+
listener(closeReasonValue);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
drain(timeoutMs2 = defaultTimeout) {
|
|
76
|
+
if (!Number.isFinite(timeoutMs2) || timeoutMs2 < 1 || timeoutMs2 > 3e5) {
|
|
77
|
+
return Promise.reject(new TypeError("drain timeoutMs must be a finite number from 1 to 300000"));
|
|
78
|
+
}
|
|
79
|
+
if (drainPromise) return drainPromise;
|
|
80
|
+
const currentParticipants = [...participants];
|
|
81
|
+
if (state === "closed") {
|
|
82
|
+
const pendingExecutions = currentParticipants.reduce((total, participant) => {
|
|
83
|
+
try {
|
|
84
|
+
return total + Math.max(0, participant.pending());
|
|
85
|
+
} catch {
|
|
86
|
+
return total;
|
|
87
|
+
}
|
|
88
|
+
}, 0);
|
|
89
|
+
const result = {
|
|
90
|
+
state,
|
|
91
|
+
drained: pendingExecutions === 0,
|
|
92
|
+
timedOut: pendingExecutions !== 0,
|
|
93
|
+
pendingExecutions
|
|
94
|
+
};
|
|
95
|
+
drainPromise = Promise.resolve(result);
|
|
96
|
+
return drainPromise;
|
|
97
|
+
}
|
|
98
|
+
if (state === "active") session.beginClose("Runtime endpoint drain requested");
|
|
99
|
+
drainPromise = new Promise((resolve) => {
|
|
100
|
+
let settled = false;
|
|
101
|
+
const settle = (timedOut, failed) => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
settled = true;
|
|
104
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
105
|
+
const pendingExecutions = currentParticipants.reduce((total, participant) => {
|
|
106
|
+
try {
|
|
107
|
+
return total + Math.max(0, participant.pending());
|
|
108
|
+
} catch {
|
|
109
|
+
return total;
|
|
110
|
+
}
|
|
111
|
+
}, 0);
|
|
112
|
+
resolve({ state, drained: !timedOut && !failed && pendingExecutions === 0, timedOut, pendingExecutions });
|
|
113
|
+
};
|
|
114
|
+
const effectiveTimeout = Math.min(timeoutMs2, defaultTimeout);
|
|
115
|
+
const timer = setTimeout(() => settle(true, false), effectiveTimeout);
|
|
116
|
+
void Promise.allSettled(currentParticipants.map((participant) => {
|
|
117
|
+
try {
|
|
118
|
+
return participant.drain();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return Promise.reject(error);
|
|
121
|
+
}
|
|
122
|
+
})).then((results) => settle(false, results.some((result) => result.status === "rejected")));
|
|
123
|
+
});
|
|
124
|
+
return drainPromise;
|
|
125
|
+
},
|
|
126
|
+
close() {
|
|
127
|
+
if (state === "closed") return;
|
|
128
|
+
if (state === "active") session.beginClose("Runtime endpoint physically closed");
|
|
129
|
+
state = "closed";
|
|
130
|
+
for (const listener of [...closedListeners]) {
|
|
131
|
+
try {
|
|
132
|
+
listener();
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
beginCloseListeners.clear();
|
|
137
|
+
closedListeners.clear();
|
|
138
|
+
},
|
|
139
|
+
onBeginClose(listener) {
|
|
140
|
+
if (state !== "active") {
|
|
141
|
+
try {
|
|
142
|
+
listener(closeReasonValue ?? "Runtime endpoint closing");
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
return () => void 0;
|
|
146
|
+
}
|
|
147
|
+
beginCloseListeners.add(listener);
|
|
148
|
+
return () => beginCloseListeners.delete(listener);
|
|
149
|
+
},
|
|
150
|
+
onClosed(listener) {
|
|
151
|
+
if (state === "closed") {
|
|
152
|
+
try {
|
|
153
|
+
listener();
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
return () => void 0;
|
|
157
|
+
}
|
|
158
|
+
closedListeners.add(listener);
|
|
159
|
+
return () => closedListeners.delete(listener);
|
|
160
|
+
},
|
|
161
|
+
registerDrainParticipant(participant) {
|
|
162
|
+
if (state === "closed") return () => void 0;
|
|
163
|
+
participants.add(participant);
|
|
164
|
+
return () => participants.delete(participant);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
return session;
|
|
168
|
+
}
|
|
2
169
|
|
|
3
170
|
// src/transport/serviceBridge.ts
|
|
4
171
|
function id(prefix) {
|
|
@@ -73,11 +240,16 @@ function createCapabilityBridge(options) {
|
|
|
73
240
|
const codec = createRuntimeMessageCodec();
|
|
74
241
|
const limits = normalizeRuntimeLimits(options.limits);
|
|
75
242
|
const budget = options.budget ?? createRuntimeBudget(limits);
|
|
243
|
+
const session = options.session ?? createRuntimeEndpointSession(options.binding ?? createRuntimeEndpointBinding(`bridge:${id("runtime")}`), { defaultDrainTimeoutMs: options.drainTimeoutMs });
|
|
244
|
+
if (options.binding && !sameRuntimeEndpointBinding(options.binding, session.binding)) throw new TypeError("Capability bridge binding disagrees with endpoint session");
|
|
245
|
+
const localBinding = session.binding;
|
|
246
|
+
const compatibilityControlBinding = createRuntimeEndpointBinding(options.remoteRuntimeId ?? "legacy-remote");
|
|
76
247
|
const listeners = /* @__PURE__ */ new Set();
|
|
77
248
|
const pending = /* @__PURE__ */ new Map();
|
|
78
249
|
const streams = /* @__PURE__ */ new Map();
|
|
79
250
|
const waitingCalls = /* @__PURE__ */ new Set();
|
|
80
251
|
const callbackExecutions = /* @__PURE__ */ new Set();
|
|
252
|
+
const executionDrainWaiters = /* @__PURE__ */ new Set();
|
|
81
253
|
const proxies = /* @__PURE__ */ new Map();
|
|
82
254
|
const clients = /* @__PURE__ */ new Map();
|
|
83
255
|
let pendingCount = 0;
|
|
@@ -91,6 +263,11 @@ function createCapabilityBridge(options) {
|
|
|
91
263
|
let appliedSnapshotFingerprint;
|
|
92
264
|
let currentServices = [];
|
|
93
265
|
let disposed = false;
|
|
266
|
+
let closeMessageSent = false;
|
|
267
|
+
let closeAckWaiter;
|
|
268
|
+
let receivedCloseAck;
|
|
269
|
+
let closeAckSent = false;
|
|
270
|
+
let drainPromise;
|
|
94
271
|
let reservedStreamCount = 0;
|
|
95
272
|
const defaultTimeout = timeoutMs(options.defaultCallTimeoutMs, 3e4);
|
|
96
273
|
const emit = () => {
|
|
@@ -143,6 +320,10 @@ function createCapabilityBridge(options) {
|
|
|
143
320
|
peerRetainedPayloadBytes = Math.max(0, peerRetainedPayloadBytes - record.item.budgetBytes);
|
|
144
321
|
budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - record.item.budgetBytes);
|
|
145
322
|
budget.releaseExecutionSlot();
|
|
323
|
+
if (callbackExecutions.size === 0) {
|
|
324
|
+
for (const resolve of [...executionDrainWaiters]) resolve();
|
|
325
|
+
executionDrainWaiters.clear();
|
|
326
|
+
}
|
|
146
327
|
emit();
|
|
147
328
|
};
|
|
148
329
|
const releaseQueueItem = (item) => {
|
|
@@ -165,7 +346,7 @@ function createCapabilityBridge(options) {
|
|
|
165
346
|
if (entry.cancelSent) return;
|
|
166
347
|
entry.cancelSent = true;
|
|
167
348
|
try {
|
|
168
|
-
options.transport.send({ type: RUNTIME_CANCEL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: entry.callId, serviceInstanceId: entry.reference.serviceInstanceId });
|
|
349
|
+
options.transport.send({ type: RUNTIME_CANCEL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: entry.callId, serviceInstanceId: entry.reference.serviceInstanceId });
|
|
169
350
|
} catch {
|
|
170
351
|
}
|
|
171
352
|
try {
|
|
@@ -244,6 +425,7 @@ function createCapabilityBridge(options) {
|
|
|
244
425
|
const findService = (capability) => currentServices.find((service) => serviceMatches(service, capability));
|
|
245
426
|
const terminalError = (proxy, capability) => {
|
|
246
427
|
if (terminalFailure && !disposed) return frameworkError(terminalFailure.code, terminalFailure.phase, contextFor(capability, proxy.bound));
|
|
428
|
+
if (session.state !== "active") return frameworkError("service_revoked", "dispose", contextFor(capability, proxy.bound));
|
|
247
429
|
if (disposed || proxy.revoked) return frameworkError("service_revoked", "dispatch", contextFor(capability, proxy.bound));
|
|
248
430
|
if (currentState === "stale" || currentState === "disposed") return frameworkError("service_revoked", "dispatch", contextFor(capability, proxy.bound));
|
|
249
431
|
if (proxy.bound && !currentServices.some((service) => serviceKey(service) === serviceKey(proxy.bound) && service.serviceInstanceId === proxy.bound?.serviceInstanceId)) {
|
|
@@ -334,7 +516,7 @@ function createCapabilityBridge(options) {
|
|
|
334
516
|
return;
|
|
335
517
|
}
|
|
336
518
|
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);
|
|
519
|
+
sendWire({ type: RUNTIME_CALL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId, capabilityId: entry.capability.id, contractVersion: entry.capability.version, serviceInstanceId: reference.serviceInstanceId, mode: "unary", timeoutMs: timeout, request: prepared.value, ...reference.grantId !== void 0 ? { grantId: reference.grantId } : {}, ...callOptions.operationId !== void 0 ? { operationId: callOptions.operationId } : {} }, prepared.transfer);
|
|
338
520
|
} catch {
|
|
339
521
|
settleUnary(entry, frameworkError("request_clone_failed", "dispatch", contextFor(entry.capability, reference)));
|
|
340
522
|
}
|
|
@@ -509,7 +691,7 @@ function createCapabilityBridge(options) {
|
|
|
509
691
|
return { ready, closed, cancel };
|
|
510
692
|
}
|
|
511
693
|
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);
|
|
694
|
+
sendWire({ type: RUNTIME_CALL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId, capabilityId: entry.capability.id, contractVersion: entry.capability.version, serviceInstanceId: reference.serviceInstanceId, mode: "stream", timeoutMs: timeout, request: prepared.value, initialCredit: window, ...reference.grantId !== void 0 ? { grantId: reference.grantId } : {}, ...optionsForSubscribe.operationId !== void 0 ? { operationId: optionsForSubscribe.operationId } : {} }, prepared.transfer);
|
|
513
695
|
} catch {
|
|
514
696
|
terminateStream(entry, frameworkError("request_clone_failed", "dispatch", contextFor(entry.capability, reference)), false);
|
|
515
697
|
}
|
|
@@ -652,7 +834,7 @@ function createCapabilityBridge(options) {
|
|
|
652
834
|
if (stream.state === "active" && !stream.doneReceived) {
|
|
653
835
|
stream.credit += 1;
|
|
654
836
|
try {
|
|
655
|
-
sendWire({ type: RUNTIME_CREDIT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: stream.callId, serviceInstanceId: stream.reference.serviceInstanceId, count: 1 });
|
|
837
|
+
sendWire({ type: RUNTIME_CREDIT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: stream.callId, serviceInstanceId: stream.reference.serviceInstanceId, count: 1 });
|
|
656
838
|
} catch {
|
|
657
839
|
terminateStream(stream, frameworkError("transport_unavailable", "dispatch", contextFor(stream.capability, stream.reference)), true);
|
|
658
840
|
break;
|
|
@@ -682,6 +864,14 @@ function createCapabilityBridge(options) {
|
|
|
682
864
|
terminateStream(stream, frameworkError("handler_failed", "execute", contextFor(stream.capability, stream.reference)), true);
|
|
683
865
|
});
|
|
684
866
|
};
|
|
867
|
+
const drainCallbackExecutions = () => {
|
|
868
|
+
if (callbackExecutions.size === 0) return Promise.resolve();
|
|
869
|
+
return new Promise((resolve) => executionDrainWaiters.add(resolve));
|
|
870
|
+
};
|
|
871
|
+
session.registerDrainParticipant({
|
|
872
|
+
drain: drainCallbackExecutions,
|
|
873
|
+
pending: () => callbackExecutions.size
|
|
874
|
+
});
|
|
685
875
|
const invalidate = (reason = "remote service directory replaced", terminalCause) => {
|
|
686
876
|
if (terminalCause) {
|
|
687
877
|
for (const record of [...waitingCalls]) record.fail(errorWithTerminalCause(terminalCause, record.proxy.capability, record.proxy.bound));
|
|
@@ -706,10 +896,81 @@ function createCapabilityBridge(options) {
|
|
|
706
896
|
if (disposed) return;
|
|
707
897
|
terminalFailure = frameworkError(code, "receive");
|
|
708
898
|
invalidate(reason, terminalFailure);
|
|
899
|
+
session.beginClose(reason);
|
|
709
900
|
try {
|
|
710
901
|
options.transport.close?.();
|
|
711
902
|
} catch {
|
|
712
903
|
}
|
|
904
|
+
session.close();
|
|
905
|
+
};
|
|
906
|
+
const removeSessionBeginClose = session.onBeginClose((reason) => {
|
|
907
|
+
if (!disposed) invalidate(reason);
|
|
908
|
+
});
|
|
909
|
+
const bounded = async (promise, fallback, deadline) => {
|
|
910
|
+
const remaining = deadline - Date.now();
|
|
911
|
+
if (remaining <= 0) return fallback;
|
|
912
|
+
return Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(fallback), remaining))]);
|
|
913
|
+
};
|
|
914
|
+
const beginClose = (reason = "Runtime bridge closing") => {
|
|
915
|
+
session.beginClose(reason);
|
|
916
|
+
};
|
|
917
|
+
const drain = (requestedTimeoutMs = DEFAULT_RUNTIME_DRAIN_TIMEOUT_MS) => {
|
|
918
|
+
if (drainPromise) return drainPromise;
|
|
919
|
+
if (!Number.isFinite(requestedTimeoutMs) || requestedTimeoutMs < 1 || requestedTimeoutMs > 3e5) {
|
|
920
|
+
return Promise.reject(new TypeError("drain timeoutMs must be a finite number from 1 to 300000"));
|
|
921
|
+
}
|
|
922
|
+
const alreadyClosed = session.state === "closed";
|
|
923
|
+
beginClose("Runtime bridge drain requested");
|
|
924
|
+
const effectiveTimeout = Math.min(requestedTimeoutMs, session.maxDrainTimeoutMs);
|
|
925
|
+
const deadline = Date.now() + effectiveTimeout;
|
|
926
|
+
const localDrain = session.drain(effectiveTimeout);
|
|
927
|
+
if (alreadyClosed) {
|
|
928
|
+
drainPromise = (async () => {
|
|
929
|
+
const localFallback = {
|
|
930
|
+
state: session.state,
|
|
931
|
+
drained: false,
|
|
932
|
+
timedOut: true,
|
|
933
|
+
pendingExecutions: callbackExecutions.size
|
|
934
|
+
};
|
|
935
|
+
return bounded(localDrain, localFallback, deadline);
|
|
936
|
+
})();
|
|
937
|
+
return drainPromise;
|
|
938
|
+
}
|
|
939
|
+
const ackPromise = receivedCloseAck ? Promise.resolve(receivedCloseAck) : new Promise((resolve) => {
|
|
940
|
+
closeAckWaiter = { resolve };
|
|
941
|
+
});
|
|
942
|
+
if (!closeMessageSent) {
|
|
943
|
+
try {
|
|
944
|
+
sendWire({
|
|
945
|
+
type: RUNTIME_CLOSE_TYPE,
|
|
946
|
+
protocolVersion: RUNTIME_PROTOCOL_VERSION,
|
|
947
|
+
binding: localBinding,
|
|
948
|
+
timeoutMs: effectiveTimeout
|
|
949
|
+
});
|
|
950
|
+
closeMessageSent = true;
|
|
951
|
+
} catch {
|
|
952
|
+
closeAckWaiter = void 0;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
drainPromise = (async () => {
|
|
956
|
+
const localFallback = {
|
|
957
|
+
state: session.state,
|
|
958
|
+
drained: false,
|
|
959
|
+
timedOut: true,
|
|
960
|
+
pendingExecutions: callbackExecutions.size
|
|
961
|
+
};
|
|
962
|
+
const local = await bounded(localDrain, localFallback, deadline);
|
|
963
|
+
const ack = await bounded(ackPromise, void 0, deadline);
|
|
964
|
+
if (ack === void 0) closeAckWaiter = void 0;
|
|
965
|
+
const timedOut = local.timedOut || !ack || ack.timedOut;
|
|
966
|
+
return {
|
|
967
|
+
state: session.state,
|
|
968
|
+
drained: local.drained && !!ack && ack.drained,
|
|
969
|
+
timedOut,
|
|
970
|
+
pendingExecutions: local.pendingExecutions + (ack?.pendingExecutions ?? 0)
|
|
971
|
+
};
|
|
972
|
+
})();
|
|
973
|
+
return drainPromise;
|
|
713
974
|
};
|
|
714
975
|
const onMessage = (message, metadata) => {
|
|
715
976
|
const ledger = metadata?.ledger ?? createReceivePortLedger(metadata?.ports, { limits, phase: "receive" });
|
|
@@ -718,9 +979,7 @@ function createCapabilityBridge(options) {
|
|
|
718
979
|
return;
|
|
719
980
|
}
|
|
720
981
|
try {
|
|
721
|
-
if (!metadata?.decoded)
|
|
722
|
-
message = codec.decode(message);
|
|
723
|
-
}
|
|
982
|
+
if (!metadata?.decoded) message = codec.decode(message);
|
|
724
983
|
} catch (error) {
|
|
725
984
|
ledger.closeUndelivered();
|
|
726
985
|
if (error instanceof WebLoomError && error.code === "protocol_mismatch") {
|
|
@@ -731,6 +990,61 @@ function createCapabilityBridge(options) {
|
|
|
731
990
|
return;
|
|
732
991
|
}
|
|
733
992
|
try {
|
|
993
|
+
const incomingBinding = message.binding ?? session.remoteBinding;
|
|
994
|
+
const bindingAccepted = message.binding === void 0 ? session.remoteBinding === void 0 : session.acceptRemoteBinding(message.binding);
|
|
995
|
+
const closedKnownCloseAck = !bindingAccepted && session.state === "closed" && message.type === RUNTIME_CLOSE_ACK_TYPE && incomingBinding !== void 0 && sameRuntimeEndpointBinding(session.remoteBinding, incomingBinding);
|
|
996
|
+
if (!bindingAccepted && !closedKnownCloseAck) {
|
|
997
|
+
ledger.closeUndelivered();
|
|
998
|
+
failClose("Runtime endpoint binding mismatch", "invalid_message");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (message.type === RUNTIME_CLOSE_TYPE) {
|
|
1002
|
+
ledger.closeUndelivered();
|
|
1003
|
+
session.beginClose("Remote Runtime endpoint closing");
|
|
1004
|
+
const timeout = Math.min(message.timeoutMs ?? session.maxDrainTimeoutMs, session.maxDrainTimeoutMs);
|
|
1005
|
+
void session.drain(timeout).then((result) => {
|
|
1006
|
+
if (closeAckSent) return;
|
|
1007
|
+
closeAckSent = true;
|
|
1008
|
+
try {
|
|
1009
|
+
sendWire({
|
|
1010
|
+
type: RUNTIME_CLOSE_ACK_TYPE,
|
|
1011
|
+
protocolVersion: RUNTIME_PROTOCOL_VERSION,
|
|
1012
|
+
binding: localBinding,
|
|
1013
|
+
acknowledgedBinding: incomingBinding ?? compatibilityControlBinding,
|
|
1014
|
+
drained: result.drained,
|
|
1015
|
+
timedOut: result.timedOut,
|
|
1016
|
+
pendingExecutions: result.pendingExecutions
|
|
1017
|
+
});
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
session.close();
|
|
1021
|
+
setTimeout(() => {
|
|
1022
|
+
try {
|
|
1023
|
+
options.transport.close?.();
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
}, 10);
|
|
1027
|
+
});
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (message.type === RUNTIME_CLOSE_ACK_TYPE) {
|
|
1031
|
+
ledger.closeUndelivered();
|
|
1032
|
+
if (!sameRuntimeEndpointBinding(message.acknowledgedBinding, localBinding)) {
|
|
1033
|
+
failClose("Runtime close acknowledgement binding mismatch", "invalid_message");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const result = {
|
|
1037
|
+
state: session.state,
|
|
1038
|
+
drained: message.drained,
|
|
1039
|
+
timedOut: message.timedOut,
|
|
1040
|
+
pendingExecutions: message.pendingExecutions
|
|
1041
|
+
};
|
|
1042
|
+
receivedCloseAck = result;
|
|
1043
|
+
const waiter = closeAckWaiter;
|
|
1044
|
+
closeAckWaiter = void 0;
|
|
1045
|
+
waiter?.resolve(result);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
734
1048
|
if (message.type === RUNTIME_CALL_TYPE || message.type === RUNTIME_CANCEL_TYPE || message.type === RUNTIME_CREDIT_TYPE) return;
|
|
735
1049
|
if (message.type === RUNTIME_SNAPSHOT_TYPE) {
|
|
736
1050
|
if (ledger.ports.length > 0) throw frameworkError("transfer_invalid", "receive");
|
|
@@ -886,10 +1200,13 @@ function createCapabilityBridge(options) {
|
|
|
886
1200
|
if (disposed) return { accepted: false, reason: "disposed" };
|
|
887
1201
|
if (!snapshot || typeof snapshot !== "object") return { accepted: false, reason: "invalid-snapshot" };
|
|
888
1202
|
if (snapshot.protocolVersion !== RUNTIME_PROTOCOL_VERSION) return { accepted: false, reason: "protocol-mismatch" };
|
|
1203
|
+
const binding = snapshot.binding ?? session.remoteBinding;
|
|
1204
|
+
if (snapshot.binding !== void 0 && !session.acceptRemoteBinding(snapshot.binding)) return { accepted: false, reason: "invalid-snapshot" };
|
|
889
1205
|
try {
|
|
890
1206
|
if (snapshot.units.length > limits.maxSnapshotUnits || snapshot.services.length > limits.maxSnapshotServices) return { accepted: false, reason: "invalid-snapshot" };
|
|
891
|
-
|
|
892
|
-
|
|
1207
|
+
const wireSnapshot = { ...snapshot, type: RUNTIME_SNAPSHOT_TYPE, ...binding !== void 0 ? { binding } : {} };
|
|
1208
|
+
codec.encode(wireSnapshot);
|
|
1209
|
+
validateDto(wireSnapshot, { limits: { maxDepth: limits.maxDtoDepth, maxNodes: limits.maxDtoNodes, maxEdges: limits.maxDtoEdges, maxBudgetBytes: limits.maxMessageBudgetBytes }, phase: "receive" });
|
|
893
1210
|
} catch {
|
|
894
1211
|
return { accepted: false, reason: "invalid-snapshot" };
|
|
895
1212
|
}
|
|
@@ -953,6 +1270,12 @@ function createCapabilityBridge(options) {
|
|
|
953
1270
|
get runtimeKind() {
|
|
954
1271
|
return remoteRuntimeKind;
|
|
955
1272
|
},
|
|
1273
|
+
get binding() {
|
|
1274
|
+
return localBinding;
|
|
1275
|
+
},
|
|
1276
|
+
get endpointState() {
|
|
1277
|
+
return session.state;
|
|
1278
|
+
},
|
|
956
1279
|
getClient(capability, scope) {
|
|
957
1280
|
const key = `${capability.kind}\0${capability.id}\0${capability.version}\0${scope?.identity.scopeId ?? "root"}`;
|
|
958
1281
|
let proxy = proxies.get(key);
|
|
@@ -973,13 +1296,18 @@ function createCapabilityBridge(options) {
|
|
|
973
1296
|
invalidate(reason);
|
|
974
1297
|
currentState = "stale";
|
|
975
1298
|
},
|
|
1299
|
+
beginClose,
|
|
1300
|
+
drain,
|
|
976
1301
|
dispose(reason = "Runtime bridge disposed") {
|
|
977
1302
|
if (disposed) return;
|
|
1303
|
+
session.beginClose(reason);
|
|
978
1304
|
disposed = true;
|
|
979
1305
|
invalidate(reason);
|
|
980
1306
|
removeTransport();
|
|
981
1307
|
removeTransportError?.();
|
|
982
1308
|
options.transport.close?.();
|
|
1309
|
+
session.close();
|
|
1310
|
+
removeSessionBeginClose();
|
|
983
1311
|
currentState = "disposed";
|
|
984
1312
|
emit();
|
|
985
1313
|
},
|
|
@@ -1116,9 +1444,15 @@ function createMessagePortServiceProvider(options) {
|
|
|
1116
1444
|
const configuredTransport = options.transport ?? (options.port ? createMessagePortRuntimeTransport(options.port, { limits }) : void 0);
|
|
1117
1445
|
if (!configuredTransport) throw new TypeError("MessagePort service provider requires transport or port");
|
|
1118
1446
|
const transport = configuredTransport;
|
|
1447
|
+
const ownsSession = options.session === void 0;
|
|
1448
|
+
const session = options.session ?? createRuntimeEndpointSession(options.binding ?? createRuntimeEndpointBinding(`endpoint:${Date.now().toString(36)}`), { defaultDrainTimeoutMs: options.drainTimeoutMs });
|
|
1449
|
+
if (options.binding && !sameRuntimeEndpointBinding(options.binding, session.binding)) throw new TypeError("MessagePort service provider binding disagrees with endpoint session");
|
|
1450
|
+
const localBinding = session.binding;
|
|
1451
|
+
const compatibilityRemoteBinding = createRuntimeEndpointBinding("legacy-remote");
|
|
1119
1452
|
const activeCalls = /* @__PURE__ */ new Map();
|
|
1120
1453
|
const streams = /* @__PURE__ */ new Map();
|
|
1121
1454
|
const executionSlots = /* @__PURE__ */ new Map();
|
|
1455
|
+
const executionDrainWaiters = /* @__PURE__ */ new Set();
|
|
1122
1456
|
let peerRetainedPayloadBytes = 0;
|
|
1123
1457
|
let disposed = false;
|
|
1124
1458
|
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);
|
|
@@ -1130,6 +1464,7 @@ function createMessagePortServiceProvider(options) {
|
|
|
1130
1464
|
post(transport, codec, {
|
|
1131
1465
|
type: RUNTIME_ERROR_MESSAGE_TYPE,
|
|
1132
1466
|
protocolVersion: RUNTIME_PROTOCOL_VERSION,
|
|
1467
|
+
binding: localBinding,
|
|
1133
1468
|
callId: call.callId,
|
|
1134
1469
|
serviceInstanceId: call.serviceInstanceId,
|
|
1135
1470
|
error: { code, message: safeErrorMessage(code), phase }
|
|
@@ -1144,6 +1479,10 @@ function createMessagePortServiceProvider(options) {
|
|
|
1144
1479
|
budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - active.requestBytes);
|
|
1145
1480
|
active.removePeerRevoke?.();
|
|
1146
1481
|
active.removePeerRevoke = void 0;
|
|
1482
|
+
if (executionSlots.size === 0) {
|
|
1483
|
+
for (const resolve of [...executionDrainWaiters]) resolve();
|
|
1484
|
+
executionDrainWaiters.clear();
|
|
1485
|
+
}
|
|
1147
1486
|
};
|
|
1148
1487
|
const releaseStreamReservation = (active) => {
|
|
1149
1488
|
if (!active.streamReserved) return;
|
|
@@ -1297,7 +1636,7 @@ function createMessagePortServiceProvider(options) {
|
|
|
1297
1636
|
if (entry.closed || entry.active.cancelled) break;
|
|
1298
1637
|
if (next.done) {
|
|
1299
1638
|
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);
|
|
1639
|
+
if (post(transport, codec, { type: RUNTIME_RESULT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: entry.call.callId, serviceInstanceId: entry.reference.serviceInstanceId, done: true })) closeStream(entry, true);
|
|
1301
1640
|
else {
|
|
1302
1641
|
sendError(entry.call, "transport_unavailable", "receive");
|
|
1303
1642
|
closeStream(entry, true);
|
|
@@ -1307,7 +1646,7 @@ function createMessagePortServiceProvider(options) {
|
|
|
1307
1646
|
try {
|
|
1308
1647
|
const prepared = options.prepareItem?.(next.value, entry.call) ?? { value: next.value };
|
|
1309
1648
|
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)) {
|
|
1649
|
+
if (!post(transport, codec, { type: RUNTIME_NEXT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: entry.call.callId, serviceInstanceId: entry.reference.serviceInstanceId, sequence: entry.sequence, item: output.value }, output.transfer)) {
|
|
1311
1650
|
sendError(entry.call, "response_clone_failed", "receive");
|
|
1312
1651
|
closeStream(entry);
|
|
1313
1652
|
break;
|
|
@@ -1332,6 +1671,21 @@ function createMessagePortServiceProvider(options) {
|
|
|
1332
1671
|
failClose();
|
|
1333
1672
|
return;
|
|
1334
1673
|
}
|
|
1674
|
+
try {
|
|
1675
|
+
if (!metadata?.decoded) message = codec.decode(message);
|
|
1676
|
+
} catch {
|
|
1677
|
+
ledger.closeUndelivered();
|
|
1678
|
+
failClose();
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const incomingBinding = message.binding ?? session.remoteBinding;
|
|
1682
|
+
const bindingAccepted = message.binding === void 0 ? session.remoteBinding === void 0 : session.acceptRemoteBinding(message.binding);
|
|
1683
|
+
const closedKnownControl = !bindingAccepted && session.state === "closed" && incomingBinding !== void 0 && sameRuntimeEndpointBinding(session.remoteBinding, incomingBinding);
|
|
1684
|
+
if (!bindingAccepted && !closedKnownControl) {
|
|
1685
|
+
ledger.closeUndelivered();
|
|
1686
|
+
failClose();
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1335
1689
|
if (message.type === RUNTIME_CANCEL_TYPE) {
|
|
1336
1690
|
ledger.closeUndelivered();
|
|
1337
1691
|
const active2 = activeCalls.get(message.callId) ?? streams.get(message.callId)?.active;
|
|
@@ -1353,6 +1707,11 @@ function createMessagePortServiceProvider(options) {
|
|
|
1353
1707
|
return;
|
|
1354
1708
|
}
|
|
1355
1709
|
if (message.type !== RUNTIME_CALL_TYPE) return;
|
|
1710
|
+
if (session.state !== "active" || disposed) {
|
|
1711
|
+
ledger.closeUndelivered();
|
|
1712
|
+
sendError(message, "service_revoked", "dispose");
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1356
1715
|
const reference = serviceFor(message);
|
|
1357
1716
|
if (!reference) {
|
|
1358
1717
|
ledger.closeUndelivered();
|
|
@@ -1364,7 +1723,7 @@ function createMessagePortServiceProvider(options) {
|
|
|
1364
1723
|
sendError(message, "invalid_message", "dispatch");
|
|
1365
1724
|
return;
|
|
1366
1725
|
}
|
|
1367
|
-
if (message.grantId !==
|
|
1726
|
+
if (message.grantId !== reference.grantId) {
|
|
1368
1727
|
ledger.closeUndelivered();
|
|
1369
1728
|
sendError(message, "permission_denied", "dispatch");
|
|
1370
1729
|
return;
|
|
@@ -1403,7 +1762,7 @@ function createMessagePortServiceProvider(options) {
|
|
|
1403
1762
|
releaseExecution(active);
|
|
1404
1763
|
return;
|
|
1405
1764
|
}
|
|
1406
|
-
const result = await options.handleCall({ message: active.call, request: request.value, reference, signal: controller.signal, deadlineAt: active.deadlineAt, peer: callPeer });
|
|
1765
|
+
const result = await options.handleCall({ message: active.call, request: request.value, reference, signal: controller.signal, deadlineAt: active.deadlineAt, binding: active.call.binding ?? compatibilityRemoteBinding, peer: callPeer });
|
|
1407
1766
|
if (active.cancelled || disposed || executionSlots.get(message.callId) !== active) {
|
|
1408
1767
|
if (message.mode === "stream") await closeLateIterable(result);
|
|
1409
1768
|
releaseExecution(active);
|
|
@@ -1412,7 +1771,7 @@ function createMessagePortServiceProvider(options) {
|
|
|
1412
1771
|
if (message.mode === "unary") {
|
|
1413
1772
|
try {
|
|
1414
1773
|
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");
|
|
1774
|
+
if (!active.cancelled && !post(transport, codec, { type: RUNTIME_RESULT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: message.callId, serviceInstanceId: reference.serviceInstanceId, result: output.value }, output.transfer)) sendError(message, "response_clone_failed", "receive");
|
|
1416
1775
|
} catch (error) {
|
|
1417
1776
|
if (!active.cancelled) sendError(message, safeErrorCode(error, "response_validation_failed"), "receive");
|
|
1418
1777
|
}
|
|
@@ -1424,7 +1783,7 @@ function createMessagePortServiceProvider(options) {
|
|
|
1424
1783
|
const iterator = result[Symbol.asyncIterator]();
|
|
1425
1784
|
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
1785
|
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 })) {
|
|
1786
|
+
if (!post(transport, codec, { type: RUNTIME_RESULT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: message.callId, serviceInstanceId: reference.serviceInstanceId, streamReady: true })) {
|
|
1428
1787
|
sendError(message, "transport_unavailable", "dispatch");
|
|
1429
1788
|
closeStream(stream);
|
|
1430
1789
|
return;
|
|
@@ -1448,12 +1807,22 @@ function createMessagePortServiceProvider(options) {
|
|
|
1448
1807
|
}
|
|
1449
1808
|
});
|
|
1450
1809
|
};
|
|
1810
|
+
const drainExecutions = () => {
|
|
1811
|
+
if (executionSlots.size === 0) return Promise.resolve();
|
|
1812
|
+
return new Promise((resolve) => executionDrainWaiters.add(resolve));
|
|
1813
|
+
};
|
|
1814
|
+
session.registerDrainParticipant({
|
|
1815
|
+
drain: drainExecutions,
|
|
1816
|
+
pending: () => executionSlots.size
|
|
1817
|
+
});
|
|
1818
|
+
let removeTransport = () => void 0;
|
|
1451
1819
|
function dispose() {
|
|
1452
1820
|
if (disposed) return;
|
|
1453
1821
|
disposed = true;
|
|
1454
1822
|
for (const active of [...activeCalls.values()]) cancelActive(active, "service_revoked");
|
|
1455
1823
|
for (const stream of [...streams.values()]) closeStream(stream);
|
|
1456
1824
|
removeTransport();
|
|
1825
|
+
if (ownsSession) session.close();
|
|
1457
1826
|
}
|
|
1458
1827
|
function failClose() {
|
|
1459
1828
|
dispose();
|
|
@@ -1461,12 +1830,30 @@ function createMessagePortServiceProvider(options) {
|
|
|
1461
1830
|
transport.close?.();
|
|
1462
1831
|
} catch {
|
|
1463
1832
|
}
|
|
1833
|
+
session.close();
|
|
1464
1834
|
}
|
|
1465
|
-
|
|
1835
|
+
removeTransport = transport.subscribe(onMessage);
|
|
1836
|
+
let removeSessionBeginClose = () => void 0;
|
|
1837
|
+
removeSessionBeginClose = session.onBeginClose(() => dispose());
|
|
1838
|
+
session.onClosed(() => {
|
|
1839
|
+
removeSessionBeginClose();
|
|
1840
|
+
});
|
|
1466
1841
|
return {
|
|
1467
1842
|
setServices() {
|
|
1468
1843
|
},
|
|
1469
1844
|
dispose,
|
|
1845
|
+
get binding() {
|
|
1846
|
+
return localBinding;
|
|
1847
|
+
},
|
|
1848
|
+
get endpointState() {
|
|
1849
|
+
return session.state;
|
|
1850
|
+
},
|
|
1851
|
+
beginClose(reason = "Runtime endpoint closing") {
|
|
1852
|
+
session.beginClose(reason);
|
|
1853
|
+
},
|
|
1854
|
+
drain(timeoutMs2) {
|
|
1855
|
+
return session.drain(timeoutMs2);
|
|
1856
|
+
},
|
|
1470
1857
|
pendingCount() {
|
|
1471
1858
|
return activeCalls.size;
|
|
1472
1859
|
},
|
|
@@ -1498,6 +1885,7 @@ function createCapabilityPeerView(options) {
|
|
|
1498
1885
|
const allowed = options.allowed === void 0 ? void 0 : new Set(options.allowed.map((descriptor) => capabilityKey(descriptor)));
|
|
1499
1886
|
return Object.freeze({
|
|
1500
1887
|
peerId: options.peerId,
|
|
1888
|
+
binding: Object.freeze({ ...options.binding ?? options.bridge.binding }),
|
|
1501
1889
|
get runtime() {
|
|
1502
1890
|
return options.bridge.runtimeKind;
|
|
1503
1891
|
},
|
|
@@ -1523,7 +1911,7 @@ function makeWorker(options) {
|
|
|
1523
1911
|
if (!WorkerConstructor) throw new RuntimeUnavailableError("SharedWorker is not supported by this browser");
|
|
1524
1912
|
return new WorkerConstructor(options.url, workerOptions);
|
|
1525
1913
|
}
|
|
1526
|
-
function snapshotForClient(app, runtimeId, runtimeInstanceId, exposed, revision) {
|
|
1914
|
+
function snapshotForClient(app, runtimeId, runtimeInstanceId, binding, exposed, revision) {
|
|
1527
1915
|
const allowed = new Set(exposed.map((capability) => capabilityKey(capability)));
|
|
1528
1916
|
const state = app.state();
|
|
1529
1917
|
const runtimeState = state.state === "disconnected" ? "failed" : state.state;
|
|
@@ -1536,7 +1924,8 @@ function snapshotForClient(app, runtimeId, runtimeInstanceId, exposed, revision)
|
|
|
1536
1924
|
revision,
|
|
1537
1925
|
state: runtimeState,
|
|
1538
1926
|
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 } : {} })) : []
|
|
1927
|
+
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 } : {} })) : [],
|
|
1928
|
+
binding
|
|
1540
1929
|
};
|
|
1541
1930
|
}
|
|
1542
1931
|
function connectInternal(options) {
|
|
@@ -1573,18 +1962,22 @@ function connectInternal(options) {
|
|
|
1573
1962
|
}, { limits });
|
|
1574
1963
|
const outboundBudget = createRuntimeBudget(limits);
|
|
1575
1964
|
const inboundBudget = createRuntimeBudget(limits);
|
|
1576
|
-
const
|
|
1965
|
+
const localRuntimeInstanceId = options.client?.app.runtimeInstanceId ?? `window:${Date.now().toString(36)}`;
|
|
1966
|
+
const binding = createRuntimeEndpointBinding(localRuntimeInstanceId);
|
|
1967
|
+
const session = createRuntimeEndpointSession(binding);
|
|
1968
|
+
const bridge = createCapabilityBridge({ transport, remoteRuntimeKind: "shared-worker", remoteRuntimeId: options.id, defaultCallTimeoutMs: options.defaultCallTimeoutMs, limits, budget: outboundBudget, binding, session });
|
|
1577
1969
|
const listeners = /* @__PURE__ */ new Set();
|
|
1578
1970
|
const workerInstanceId = { value: "" };
|
|
1579
|
-
const localRuntimeInstanceId = options.client?.app.runtimeInstanceId ?? `window:${Date.now().toString(36)}`;
|
|
1580
1971
|
let revision = 0;
|
|
1581
1972
|
let disposed = false;
|
|
1973
|
+
let disposePromise;
|
|
1582
1974
|
let current = Object.freeze({ protocolVersion: RUNTIME_PROTOCOL_VERSION, runtimeId: options.id, runtimeKind: "shared-worker", runtimeInstanceId: "", state: "starting", revision: 0, units: [], services: [] });
|
|
1583
1975
|
const clientHost = requestedClientHost;
|
|
1584
1976
|
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
1977
|
const clientPeerScopeView = clientPeerScope ? createPeerScopeView(clientPeerScope) : void 0;
|
|
1586
1978
|
const clientPeer = clientPeerScope && clientPeerScopeView ? createCapabilityPeerView({
|
|
1587
1979
|
peerId: clientPeerScope.identity.attributes.peerId,
|
|
1980
|
+
binding,
|
|
1588
1981
|
scope: clientPeerScopeView,
|
|
1589
1982
|
bridge,
|
|
1590
1983
|
capabilityScope: clientPeerScope
|
|
@@ -1593,6 +1986,8 @@ function connectInternal(options) {
|
|
|
1593
1986
|
transport,
|
|
1594
1987
|
peerScope: clientPeerScope,
|
|
1595
1988
|
peer: clientPeer,
|
|
1989
|
+
binding,
|
|
1990
|
+
session,
|
|
1596
1991
|
budget: inboundBudget,
|
|
1597
1992
|
limits,
|
|
1598
1993
|
services: () => clientHost.serviceReferences().filter((service) => exposed.some((capability) => capabilityKey(capability) === capabilityKey({ kind: service.kind, id: service.capabilityId, version: service.contractVersion }))),
|
|
@@ -1600,6 +1995,7 @@ function connectInternal(options) {
|
|
|
1600
1995
|
const registration = clientHost.capabilities.registration({ kind: reference.kind, id: reference.capabilityId, version: reference.contractVersion });
|
|
1601
1996
|
return clientPeerScope && clientPeerScopeView ? createCapabilityPeerView({
|
|
1602
1997
|
peerId: clientPeerScope.identity.attributes.peerId,
|
|
1998
|
+
binding,
|
|
1603
1999
|
scope: clientPeerScopeView,
|
|
1604
2000
|
bridge,
|
|
1605
2001
|
allowed: registration?.peerDependencies ?? [],
|
|
@@ -1618,12 +2014,12 @@ function connectInternal(options) {
|
|
|
1618
2014
|
}
|
|
1619
2015
|
return { value, transfer: capability.transfer?.request?.(value) };
|
|
1620
2016
|
},
|
|
1621
|
-
handleCall: async ({ request, reference, signal, deadlineAt, peer }) => {
|
|
2017
|
+
handleCall: async ({ request, reference, signal, deadlineAt, binding: callBinding, peer }) => {
|
|
1622
2018
|
const registration = clientHost.capabilities.registration({ kind: reference.kind, id: reference.capabilityId, version: reference.contractVersion });
|
|
1623
2019
|
if (!registration) throw new WebLoomError("service_stale", "Window exposure is no longer available", "dispatch");
|
|
1624
2020
|
const handler = registration.handler;
|
|
1625
2021
|
if (!handler) throw new WebLoomError("service_stale", "Window service handler is unavailable", "dispatch");
|
|
1626
|
-
return handler(request, { signal, deadlineAt, reference, origin: "remote", peer });
|
|
2022
|
+
return handler(request, { signal, deadlineAt, reference, binding: callBinding, origin: "remote", peer });
|
|
1627
2023
|
},
|
|
1628
2024
|
prepareResult: (value, call) => {
|
|
1629
2025
|
const registration = clientHost.capabilities.registration({ kind: "rpc", id: call.capabilityId, version: call.contractVersion });
|
|
@@ -1653,7 +2049,7 @@ function connectInternal(options) {
|
|
|
1653
2049
|
if (!options.client || disposed) return;
|
|
1654
2050
|
revision += 1;
|
|
1655
2051
|
try {
|
|
1656
|
-
transport.send(snapshotForClient(options.client.app, `${options.client.app.runtimeId}`, localRuntimeInstanceId, exposed, revision));
|
|
2052
|
+
transport.send(snapshotForClient(options.client.app, `${options.client.app.runtimeId}`, localRuntimeInstanceId, binding, exposed, revision));
|
|
1657
2053
|
} catch {
|
|
1658
2054
|
}
|
|
1659
2055
|
};
|
|
@@ -1662,20 +2058,21 @@ function connectInternal(options) {
|
|
|
1662
2058
|
const applied = bridge.applySnapshot(messageValue);
|
|
1663
2059
|
if (applied.accepted) {
|
|
1664
2060
|
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 });
|
|
2061
|
+
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, binding: messageValue.binding });
|
|
1666
2062
|
}
|
|
1667
2063
|
} else if (messageValue.type === RUNTIME_ERROR_TYPE) {
|
|
1668
2064
|
bridge.invalidate(messageValue.message);
|
|
1669
|
-
emit({ ...current, state: "failed", error: messageValue.message, units: [], services: [] });
|
|
2065
|
+
emit({ ...current, state: "failed", error: messageValue.message, errorCode: messageValue.code, units: [], services: [] });
|
|
1670
2066
|
}
|
|
1671
2067
|
});
|
|
1672
2068
|
const onError = () => {
|
|
1673
|
-
if (
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2069
|
+
if (disposed) return;
|
|
2070
|
+
session.beginClose("SharedWorker disconnected");
|
|
2071
|
+
clientPeerScope?.revoke("SharedWorker disconnected");
|
|
2072
|
+
provider?.dispose();
|
|
2073
|
+
bridge.disconnect("SharedWorker disconnected");
|
|
2074
|
+
session.close();
|
|
2075
|
+
emit({ ...current, state: "disconnected", runtimeInstanceId: "", revision: 0, units: [], services: [], binding: void 0, error: "SharedWorker disconnected" });
|
|
1679
2076
|
};
|
|
1680
2077
|
port.addEventListener("messageerror", onError);
|
|
1681
2078
|
if (worker.addEventListener) worker.addEventListener("error", onError);
|
|
@@ -1685,6 +2082,10 @@ function connectInternal(options) {
|
|
|
1685
2082
|
const handle = {
|
|
1686
2083
|
runtimeKind: "shared-worker",
|
|
1687
2084
|
runtimeId: options.id,
|
|
2085
|
+
binding,
|
|
2086
|
+
get endpointState() {
|
|
2087
|
+
return session.state;
|
|
2088
|
+
},
|
|
1688
2089
|
get runtimeInstanceId() {
|
|
1689
2090
|
return workerInstanceId.value;
|
|
1690
2091
|
},
|
|
@@ -1701,20 +2102,38 @@ function connectInternal(options) {
|
|
|
1701
2102
|
listener(current);
|
|
1702
2103
|
return () => listeners.delete(listener);
|
|
1703
2104
|
},
|
|
2105
|
+
beginClose(reason = "SharedWorker connection closing") {
|
|
2106
|
+
if (disposed) return;
|
|
2107
|
+
session.beginClose(reason);
|
|
2108
|
+
clientPeerScope?.revoke(reason);
|
|
2109
|
+
},
|
|
2110
|
+
drain(timeoutMs2) {
|
|
2111
|
+
session.beginClose("SharedWorker connection drain requested");
|
|
2112
|
+
clientPeerScope?.revoke("SharedWorker connection drain requested");
|
|
2113
|
+
return bridge.drain(timeoutMs2);
|
|
2114
|
+
},
|
|
1704
2115
|
dispose(reason = "SharedWorker connection disposed") {
|
|
1705
2116
|
if (disposed) return Promise.resolve();
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2117
|
+
if (disposePromise) return disposePromise;
|
|
2118
|
+
disposePromise = (async () => {
|
|
2119
|
+
emit({ ...current, state: "stopping" });
|
|
2120
|
+
removeClient?.();
|
|
2121
|
+
removeRaw();
|
|
2122
|
+
clientPeerScope?.revoke(reason);
|
|
2123
|
+
session.beginClose(reason);
|
|
2124
|
+
const drainResult = bridge.drain();
|
|
2125
|
+
await drainResult.catch(() => void 0);
|
|
2126
|
+
disposed = true;
|
|
2127
|
+
provider?.dispose();
|
|
2128
|
+
bridge.dispose(reason);
|
|
2129
|
+
port.removeEventListener("messageerror", onError);
|
|
2130
|
+
worker.removeEventListener?.("error", onError);
|
|
2131
|
+
transport.close?.();
|
|
2132
|
+
session.close();
|
|
2133
|
+
emit({ ...current, state: "disposed", runtimeInstanceId: "", revision: 0, units: [], services: [], binding: void 0 });
|
|
2134
|
+
if (clientPeerScope) await clientPeerScope.dispose({ reason });
|
|
2135
|
+
})();
|
|
2136
|
+
return disposePromise;
|
|
1718
2137
|
}
|
|
1719
2138
|
};
|
|
1720
2139
|
handleBridges.set(handle, bridge);
|
|
@@ -1732,6 +2151,93 @@ function bridgeForRuntimeHandle(handle) {
|
|
|
1732
2151
|
return bridge;
|
|
1733
2152
|
}
|
|
1734
2153
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
2154
|
+
// src/runtime/runtimeLock.ts
|
|
2155
|
+
var WEBLOOM_RUNTIME_LOCK_PREFIX = "webloom.runtime";
|
|
2156
|
+
var RuntimeLockError = class extends Error {
|
|
2157
|
+
code;
|
|
2158
|
+
lockName;
|
|
2159
|
+
constructor(code, lockName, messages = {}) {
|
|
2160
|
+
const message = code === "runtime_lock_conflict" ? messages.conflict ?? "\u68C0\u6D4B\u5230\u53E6\u4E00\u4E2A WebLoom Runtime \u6B63\u5728\u8FD0\u884C\u3002\u8BF7\u5237\u65B0\u6216\u5173\u95ED\u6240\u6709\u76F8\u5173\u9875\u9762\u540E\u91CD\u65B0\u6253\u5F00\u3002" : messages.unavailable ?? "\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301 WebLoom \u8FD0\u884C\u9501\uFF0C\u65E0\u6CD5\u5B89\u5168\u542F\u52A8 Runtime\u3002\u8BF7\u4F7F\u7528\u652F\u6301 Web Locks \u7684\u6D4F\u89C8\u5668\u3002";
|
|
2161
|
+
super(message);
|
|
2162
|
+
this.name = "RuntimeLockError";
|
|
2163
|
+
this.code = code;
|
|
2164
|
+
this.lockName = lockName;
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
function defaultLockName(runtimeId) {
|
|
2168
|
+
const normalized = runtimeId.trim();
|
|
2169
|
+
return `${WEBLOOM_RUNTIME_LOCK_PREFIX}:${normalized}`;
|
|
2170
|
+
}
|
|
2171
|
+
function browserLockManager() {
|
|
2172
|
+
const navigatorValue = globalThis.navigator;
|
|
2173
|
+
return navigatorValue?.locks;
|
|
2174
|
+
}
|
|
2175
|
+
function createRuntimeLock(runtimeId, options = {}) {
|
|
2176
|
+
const name = defaultLockName(runtimeId);
|
|
2177
|
+
let finish;
|
|
2178
|
+
let released = false;
|
|
2179
|
+
let requestResult = Promise.resolve();
|
|
2180
|
+
let acquiredResolve;
|
|
2181
|
+
let acquiredReject;
|
|
2182
|
+
const acquired = new Promise((resolve, reject) => {
|
|
2183
|
+
acquiredResolve = resolve;
|
|
2184
|
+
acquiredReject = reject;
|
|
2185
|
+
});
|
|
2186
|
+
const hold = new Promise((resolve) => {
|
|
2187
|
+
finish = resolve;
|
|
2188
|
+
});
|
|
2189
|
+
const manager = options.manager === void 0 ? browserLockManager() : options.manager;
|
|
2190
|
+
const messages = options.messages ?? {};
|
|
2191
|
+
if (!manager) {
|
|
2192
|
+
acquiredReject(new RuntimeLockError("runtime_lock_unavailable", name, messages));
|
|
2193
|
+
} else {
|
|
2194
|
+
try {
|
|
2195
|
+
requestResult = manager.request(name, { mode: "exclusive", ifAvailable: true }, async (lock) => {
|
|
2196
|
+
if (!lock) {
|
|
2197
|
+
acquiredReject(new RuntimeLockError("runtime_lock_conflict", name, messages));
|
|
2198
|
+
return void 0;
|
|
2199
|
+
}
|
|
2200
|
+
if (released) {
|
|
2201
|
+
acquiredReject(new RuntimeLockError("runtime_lock_conflict", name, messages));
|
|
2202
|
+
return void 0;
|
|
2203
|
+
}
|
|
2204
|
+
acquiredResolve();
|
|
2205
|
+
await hold;
|
|
2206
|
+
return void 0;
|
|
2207
|
+
});
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
acquiredReject(error instanceof RuntimeLockError ? error : new RuntimeLockError("runtime_lock_unavailable", name, messages));
|
|
2210
|
+
requestResult = Promise.resolve();
|
|
2211
|
+
}
|
|
2212
|
+
void requestResult.catch((error) => acquiredReject(error instanceof RuntimeLockError ? error : new RuntimeLockError("runtime_lock_unavailable", name, messages)));
|
|
2213
|
+
}
|
|
2214
|
+
return {
|
|
2215
|
+
name,
|
|
2216
|
+
acquired,
|
|
2217
|
+
release() {
|
|
2218
|
+
if (released) return requestResult.then(() => void 0, () => void 0);
|
|
2219
|
+
released = true;
|
|
2220
|
+
finish?.();
|
|
2221
|
+
return requestResult.then(() => void 0, () => void 0);
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
function createInMemoryRuntimeLockManager() {
|
|
2226
|
+
const held = /* @__PURE__ */ new Set();
|
|
2227
|
+
return {
|
|
2228
|
+
async request(name, _options, callback) {
|
|
2229
|
+
const lock = held.has(name) ? null : { name, mode: "exclusive" };
|
|
2230
|
+
if (!lock) return callback(null);
|
|
2231
|
+
held.add(name);
|
|
2232
|
+
try {
|
|
2233
|
+
return await callback(lock);
|
|
2234
|
+
} finally {
|
|
2235
|
+
held.delete(name);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
export { RuntimeLockError, WEBLOOM_RUNTIME_LOCK_PREFIX, bridgeForRuntimeHandle, connectSharedWorker, connectSharedWorkerForTesting, createCapabilityBridge, createCapabilityPeerView, createInMemoryRuntimeLockManager, createMessagePortRuntimeTransport, createMessagePortServiceProvider, createPeerScopeView, createRuntimeEndpointBinding, createRuntimeEndpointSession, createRuntimeLock, validateTransferables };
|
|
2242
|
+
//# sourceMappingURL=chunk-YIIDRFHK.js.map
|
|
2243
|
+
//# sourceMappingURL=chunk-YIIDRFHK.js.map
|