webloom-framework 0.4.1 → 0.4.2

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.
@@ -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, RUNTIME_PROTOCOL_VERSION, validateDto, cloneFrozenAttributes, hostForWindowApp, RuntimeUnavailableError, validateRawDto, validateTransferListWithStats, assertReceivedPortSet } from './chunk-HJKPKWI7.js';
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-CKML74MG.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,15 @@ 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;
76
246
  const listeners = /* @__PURE__ */ new Set();
77
247
  const pending = /* @__PURE__ */ new Map();
78
248
  const streams = /* @__PURE__ */ new Map();
79
249
  const waitingCalls = /* @__PURE__ */ new Set();
80
250
  const callbackExecutions = /* @__PURE__ */ new Set();
251
+ const executionDrainWaiters = /* @__PURE__ */ new Set();
81
252
  const proxies = /* @__PURE__ */ new Map();
82
253
  const clients = /* @__PURE__ */ new Map();
83
254
  let pendingCount = 0;
@@ -91,6 +262,11 @@ function createCapabilityBridge(options) {
91
262
  let appliedSnapshotFingerprint;
92
263
  let currentServices = [];
93
264
  let disposed = false;
265
+ let closeMessageSent = false;
266
+ let closeAckWaiter;
267
+ let receivedCloseAck;
268
+ let closeAckSent = false;
269
+ let drainPromise;
94
270
  let reservedStreamCount = 0;
95
271
  const defaultTimeout = timeoutMs(options.defaultCallTimeoutMs, 3e4);
96
272
  const emit = () => {
@@ -143,6 +319,10 @@ function createCapabilityBridge(options) {
143
319
  peerRetainedPayloadBytes = Math.max(0, peerRetainedPayloadBytes - record.item.budgetBytes);
144
320
  budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - record.item.budgetBytes);
145
321
  budget.releaseExecutionSlot();
322
+ if (callbackExecutions.size === 0) {
323
+ for (const resolve of [...executionDrainWaiters]) resolve();
324
+ executionDrainWaiters.clear();
325
+ }
146
326
  emit();
147
327
  };
148
328
  const releaseQueueItem = (item) => {
@@ -165,7 +345,7 @@ function createCapabilityBridge(options) {
165
345
  if (entry.cancelSent) return;
166
346
  entry.cancelSent = true;
167
347
  try {
168
- options.transport.send({ type: RUNTIME_CANCEL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: entry.callId, serviceInstanceId: entry.reference.serviceInstanceId });
348
+ options.transport.send({ type: RUNTIME_CANCEL_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: entry.callId, serviceInstanceId: entry.reference.serviceInstanceId });
169
349
  } catch {
170
350
  }
171
351
  try {
@@ -244,6 +424,7 @@ function createCapabilityBridge(options) {
244
424
  const findService = (capability) => currentServices.find((service) => serviceMatches(service, capability));
245
425
  const terminalError = (proxy, capability) => {
246
426
  if (terminalFailure && !disposed) return frameworkError(terminalFailure.code, terminalFailure.phase, contextFor(capability, proxy.bound));
427
+ if (session.state !== "active") return frameworkError("service_revoked", "dispose", contextFor(capability, proxy.bound));
247
428
  if (disposed || proxy.revoked) return frameworkError("service_revoked", "dispatch", contextFor(capability, proxy.bound));
248
429
  if (currentState === "stale" || currentState === "disposed") return frameworkError("service_revoked", "dispatch", contextFor(capability, proxy.bound));
249
430
  if (proxy.bound && !currentServices.some((service) => serviceKey(service) === serviceKey(proxy.bound) && service.serviceInstanceId === proxy.bound?.serviceInstanceId)) {
@@ -334,7 +515,7 @@ function createCapabilityBridge(options) {
334
515
  return;
335
516
  }
336
517
  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);
518
+ 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
519
  } catch {
339
520
  settleUnary(entry, frameworkError("request_clone_failed", "dispatch", contextFor(entry.capability, reference)));
340
521
  }
@@ -509,7 +690,7 @@ function createCapabilityBridge(options) {
509
690
  return { ready, closed, cancel };
510
691
  }
511
692
  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);
693
+ 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
694
  } catch {
514
695
  terminateStream(entry, frameworkError("request_clone_failed", "dispatch", contextFor(entry.capability, reference)), false);
515
696
  }
@@ -652,7 +833,7 @@ function createCapabilityBridge(options) {
652
833
  if (stream.state === "active" && !stream.doneReceived) {
653
834
  stream.credit += 1;
654
835
  try {
655
- sendWire({ type: RUNTIME_CREDIT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, callId: stream.callId, serviceInstanceId: stream.reference.serviceInstanceId, count: 1 });
836
+ sendWire({ type: RUNTIME_CREDIT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: stream.callId, serviceInstanceId: stream.reference.serviceInstanceId, count: 1 });
656
837
  } catch {
657
838
  terminateStream(stream, frameworkError("transport_unavailable", "dispatch", contextFor(stream.capability, stream.reference)), true);
658
839
  break;
@@ -682,6 +863,14 @@ function createCapabilityBridge(options) {
682
863
  terminateStream(stream, frameworkError("handler_failed", "execute", contextFor(stream.capability, stream.reference)), true);
683
864
  });
684
865
  };
866
+ const drainCallbackExecutions = () => {
867
+ if (callbackExecutions.size === 0) return Promise.resolve();
868
+ return new Promise((resolve) => executionDrainWaiters.add(resolve));
869
+ };
870
+ session.registerDrainParticipant({
871
+ drain: drainCallbackExecutions,
872
+ pending: () => callbackExecutions.size
873
+ });
685
874
  const invalidate = (reason = "remote service directory replaced", terminalCause) => {
686
875
  if (terminalCause) {
687
876
  for (const record of [...waitingCalls]) record.fail(errorWithTerminalCause(terminalCause, record.proxy.capability, record.proxy.bound));
@@ -706,10 +895,81 @@ function createCapabilityBridge(options) {
706
895
  if (disposed) return;
707
896
  terminalFailure = frameworkError(code, "receive");
708
897
  invalidate(reason, terminalFailure);
898
+ session.beginClose(reason);
709
899
  try {
710
900
  options.transport.close?.();
711
901
  } catch {
712
902
  }
903
+ session.close();
904
+ };
905
+ const removeSessionBeginClose = session.onBeginClose((reason) => {
906
+ if (!disposed) invalidate(reason);
907
+ });
908
+ const bounded = async (promise, fallback, deadline) => {
909
+ const remaining = deadline - Date.now();
910
+ if (remaining <= 0) return fallback;
911
+ return Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(fallback), remaining))]);
912
+ };
913
+ const beginClose = (reason = "Runtime bridge closing") => {
914
+ session.beginClose(reason);
915
+ };
916
+ const drain = (requestedTimeoutMs = DEFAULT_RUNTIME_DRAIN_TIMEOUT_MS) => {
917
+ if (drainPromise) return drainPromise;
918
+ if (!Number.isFinite(requestedTimeoutMs) || requestedTimeoutMs < 1 || requestedTimeoutMs > 3e5) {
919
+ return Promise.reject(new TypeError("drain timeoutMs must be a finite number from 1 to 300000"));
920
+ }
921
+ const alreadyClosed = session.state === "closed";
922
+ beginClose("Runtime bridge drain requested");
923
+ const effectiveTimeout = Math.min(requestedTimeoutMs, session.maxDrainTimeoutMs);
924
+ const deadline = Date.now() + effectiveTimeout;
925
+ const localDrain = session.drain(effectiveTimeout);
926
+ if (alreadyClosed) {
927
+ drainPromise = (async () => {
928
+ const localFallback = {
929
+ state: session.state,
930
+ drained: false,
931
+ timedOut: true,
932
+ pendingExecutions: callbackExecutions.size
933
+ };
934
+ return bounded(localDrain, localFallback, deadline);
935
+ })();
936
+ return drainPromise;
937
+ }
938
+ const ackPromise = receivedCloseAck ? Promise.resolve(receivedCloseAck) : new Promise((resolve) => {
939
+ closeAckWaiter = { resolve };
940
+ });
941
+ if (!closeMessageSent) {
942
+ try {
943
+ sendWire({
944
+ type: RUNTIME_CLOSE_TYPE,
945
+ protocolVersion: RUNTIME_PROTOCOL_VERSION,
946
+ binding: localBinding,
947
+ timeoutMs: effectiveTimeout
948
+ });
949
+ closeMessageSent = true;
950
+ } catch {
951
+ closeAckWaiter = void 0;
952
+ }
953
+ }
954
+ drainPromise = (async () => {
955
+ const localFallback = {
956
+ state: session.state,
957
+ drained: false,
958
+ timedOut: true,
959
+ pendingExecutions: callbackExecutions.size
960
+ };
961
+ const local = await bounded(localDrain, localFallback, deadline);
962
+ const ack = await bounded(ackPromise, void 0, deadline);
963
+ if (ack === void 0) closeAckWaiter = void 0;
964
+ const timedOut = local.timedOut || !ack || ack.timedOut;
965
+ return {
966
+ state: session.state,
967
+ drained: local.drained && !!ack && ack.drained,
968
+ timedOut,
969
+ pendingExecutions: local.pendingExecutions + (ack?.pendingExecutions ?? 0)
970
+ };
971
+ })();
972
+ return drainPromise;
713
973
  };
714
974
  const onMessage = (message, metadata) => {
715
975
  const ledger = metadata?.ledger ?? createReceivePortLedger(metadata?.ports, { limits, phase: "receive" });
@@ -718,9 +978,7 @@ function createCapabilityBridge(options) {
718
978
  return;
719
979
  }
720
980
  try {
721
- if (!metadata?.decoded) {
722
- message = codec.decode(message);
723
- }
981
+ if (!metadata?.decoded) message = codec.decode(message);
724
982
  } catch (error) {
725
983
  ledger.closeUndelivered();
726
984
  if (error instanceof WebLoomError && error.code === "protocol_mismatch") {
@@ -731,6 +989,60 @@ function createCapabilityBridge(options) {
731
989
  return;
732
990
  }
733
991
  try {
992
+ const bindingAccepted = session.acceptRemoteBinding(message.binding);
993
+ const closedKnownCloseAck = !bindingAccepted && session.state === "closed" && message.type === RUNTIME_CLOSE_ACK_TYPE && sameRuntimeEndpointBinding(session.remoteBinding, message.binding);
994
+ if (!bindingAccepted && !closedKnownCloseAck) {
995
+ ledger.closeUndelivered();
996
+ failClose("Runtime endpoint binding mismatch", "invalid_message");
997
+ return;
998
+ }
999
+ if (message.type === RUNTIME_CLOSE_TYPE) {
1000
+ ledger.closeUndelivered();
1001
+ session.beginClose("Remote Runtime endpoint closing");
1002
+ const timeout = Math.min(message.timeoutMs ?? session.maxDrainTimeoutMs, session.maxDrainTimeoutMs);
1003
+ void session.drain(timeout).then((result) => {
1004
+ if (closeAckSent) return;
1005
+ closeAckSent = true;
1006
+ try {
1007
+ sendWire({
1008
+ type: RUNTIME_CLOSE_ACK_TYPE,
1009
+ protocolVersion: RUNTIME_PROTOCOL_VERSION,
1010
+ binding: localBinding,
1011
+ acknowledgedBinding: message.binding,
1012
+ drained: result.drained,
1013
+ timedOut: result.timedOut,
1014
+ pendingExecutions: result.pendingExecutions
1015
+ });
1016
+ } catch {
1017
+ }
1018
+ session.close();
1019
+ setTimeout(() => {
1020
+ try {
1021
+ options.transport.close?.();
1022
+ } catch {
1023
+ }
1024
+ }, 10);
1025
+ });
1026
+ return;
1027
+ }
1028
+ if (message.type === RUNTIME_CLOSE_ACK_TYPE) {
1029
+ ledger.closeUndelivered();
1030
+ if (!sameRuntimeEndpointBinding(message.acknowledgedBinding, localBinding)) {
1031
+ failClose("Runtime close acknowledgement binding mismatch", "invalid_message");
1032
+ return;
1033
+ }
1034
+ const result = {
1035
+ state: session.state,
1036
+ drained: message.drained,
1037
+ timedOut: message.timedOut,
1038
+ pendingExecutions: message.pendingExecutions
1039
+ };
1040
+ receivedCloseAck = result;
1041
+ const waiter = closeAckWaiter;
1042
+ closeAckWaiter = void 0;
1043
+ waiter?.resolve(result);
1044
+ return;
1045
+ }
734
1046
  if (message.type === RUNTIME_CALL_TYPE || message.type === RUNTIME_CANCEL_TYPE || message.type === RUNTIME_CREDIT_TYPE) return;
735
1047
  if (message.type === RUNTIME_SNAPSHOT_TYPE) {
736
1048
  if (ledger.ports.length > 0) throw frameworkError("transfer_invalid", "receive");
@@ -886,10 +1198,13 @@ function createCapabilityBridge(options) {
886
1198
  if (disposed) return { accepted: false, reason: "disposed" };
887
1199
  if (!snapshot || typeof snapshot !== "object") return { accepted: false, reason: "invalid-snapshot" };
888
1200
  if (snapshot.protocolVersion !== RUNTIME_PROTOCOL_VERSION) return { accepted: false, reason: "protocol-mismatch" };
1201
+ const binding = snapshot.binding;
1202
+ if (!session.acceptRemoteBinding(binding)) return { accepted: false, reason: "invalid-snapshot" };
889
1203
  try {
890
1204
  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" });
1205
+ const wireSnapshot = { ...snapshot, type: RUNTIME_SNAPSHOT_TYPE };
1206
+ codec.encode(wireSnapshot);
1207
+ validateDto(wireSnapshot, { limits: { maxDepth: limits.maxDtoDepth, maxNodes: limits.maxDtoNodes, maxEdges: limits.maxDtoEdges, maxBudgetBytes: limits.maxMessageBudgetBytes }, phase: "receive" });
893
1208
  } catch {
894
1209
  return { accepted: false, reason: "invalid-snapshot" };
895
1210
  }
@@ -953,6 +1268,12 @@ function createCapabilityBridge(options) {
953
1268
  get runtimeKind() {
954
1269
  return remoteRuntimeKind;
955
1270
  },
1271
+ get binding() {
1272
+ return localBinding;
1273
+ },
1274
+ get endpointState() {
1275
+ return session.state;
1276
+ },
956
1277
  getClient(capability, scope) {
957
1278
  const key = `${capability.kind}\0${capability.id}\0${capability.version}\0${scope?.identity.scopeId ?? "root"}`;
958
1279
  let proxy = proxies.get(key);
@@ -973,13 +1294,18 @@ function createCapabilityBridge(options) {
973
1294
  invalidate(reason);
974
1295
  currentState = "stale";
975
1296
  },
1297
+ beginClose,
1298
+ drain,
976
1299
  dispose(reason = "Runtime bridge disposed") {
977
1300
  if (disposed) return;
1301
+ session.beginClose(reason);
978
1302
  disposed = true;
979
1303
  invalidate(reason);
980
1304
  removeTransport();
981
1305
  removeTransportError?.();
982
1306
  options.transport.close?.();
1307
+ session.close();
1308
+ removeSessionBeginClose();
983
1309
  currentState = "disposed";
984
1310
  emit();
985
1311
  },
@@ -1116,9 +1442,14 @@ function createMessagePortServiceProvider(options) {
1116
1442
  const configuredTransport = options.transport ?? (options.port ? createMessagePortRuntimeTransport(options.port, { limits }) : void 0);
1117
1443
  if (!configuredTransport) throw new TypeError("MessagePort service provider requires transport or port");
1118
1444
  const transport = configuredTransport;
1445
+ const ownsSession = options.session === void 0;
1446
+ const session = options.session ?? createRuntimeEndpointSession(options.binding ?? createRuntimeEndpointBinding(`provider:${Date.now().toString(36)}`), { defaultDrainTimeoutMs: options.drainTimeoutMs });
1447
+ if (options.binding && !sameRuntimeEndpointBinding(options.binding, session.binding)) throw new TypeError("MessagePort service provider binding disagrees with endpoint session");
1448
+ const localBinding = session.binding;
1119
1449
  const activeCalls = /* @__PURE__ */ new Map();
1120
1450
  const streams = /* @__PURE__ */ new Map();
1121
1451
  const executionSlots = /* @__PURE__ */ new Map();
1452
+ const executionDrainWaiters = /* @__PURE__ */ new Set();
1122
1453
  let peerRetainedPayloadBytes = 0;
1123
1454
  let disposed = false;
1124
1455
  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 +1461,7 @@ function createMessagePortServiceProvider(options) {
1130
1461
  post(transport, codec, {
1131
1462
  type: RUNTIME_ERROR_MESSAGE_TYPE,
1132
1463
  protocolVersion: RUNTIME_PROTOCOL_VERSION,
1464
+ binding: localBinding,
1133
1465
  callId: call.callId,
1134
1466
  serviceInstanceId: call.serviceInstanceId,
1135
1467
  error: { code, message: safeErrorMessage(code), phase }
@@ -1144,6 +1476,10 @@ function createMessagePortServiceProvider(options) {
1144
1476
  budget.retainedPayloadBytes = Math.max(0, budget.retainedPayloadBytes - active.requestBytes);
1145
1477
  active.removePeerRevoke?.();
1146
1478
  active.removePeerRevoke = void 0;
1479
+ if (executionSlots.size === 0) {
1480
+ for (const resolve of [...executionDrainWaiters]) resolve();
1481
+ executionDrainWaiters.clear();
1482
+ }
1147
1483
  };
1148
1484
  const releaseStreamReservation = (active) => {
1149
1485
  if (!active.streamReserved) return;
@@ -1297,7 +1633,7 @@ function createMessagePortServiceProvider(options) {
1297
1633
  if (entry.closed || entry.active.cancelled) break;
1298
1634
  if (next.done) {
1299
1635
  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);
1636
+ 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
1637
  else {
1302
1638
  sendError(entry.call, "transport_unavailable", "receive");
1303
1639
  closeStream(entry, true);
@@ -1307,7 +1643,7 @@ function createMessagePortServiceProvider(options) {
1307
1643
  try {
1308
1644
  const prepared = options.prepareItem?.(next.value, entry.call) ?? { value: next.value };
1309
1645
  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)) {
1646
+ 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
1647
  sendError(entry.call, "response_clone_failed", "receive");
1312
1648
  closeStream(entry);
1313
1649
  break;
@@ -1332,6 +1668,20 @@ function createMessagePortServiceProvider(options) {
1332
1668
  failClose();
1333
1669
  return;
1334
1670
  }
1671
+ try {
1672
+ if (!metadata?.decoded) message = codec.decode(message);
1673
+ } catch {
1674
+ ledger.closeUndelivered();
1675
+ failClose();
1676
+ return;
1677
+ }
1678
+ const bindingAccepted = session.acceptRemoteBinding(message.binding);
1679
+ const closedKnownControl = !bindingAccepted && session.state === "closed" && sameRuntimeEndpointBinding(session.remoteBinding, message.binding);
1680
+ if (!bindingAccepted && !closedKnownControl) {
1681
+ ledger.closeUndelivered();
1682
+ failClose();
1683
+ return;
1684
+ }
1335
1685
  if (message.type === RUNTIME_CANCEL_TYPE) {
1336
1686
  ledger.closeUndelivered();
1337
1687
  const active2 = activeCalls.get(message.callId) ?? streams.get(message.callId)?.active;
@@ -1353,6 +1703,11 @@ function createMessagePortServiceProvider(options) {
1353
1703
  return;
1354
1704
  }
1355
1705
  if (message.type !== RUNTIME_CALL_TYPE) return;
1706
+ if (session.state !== "active" || disposed) {
1707
+ ledger.closeUndelivered();
1708
+ sendError(message, "service_revoked", "dispose");
1709
+ return;
1710
+ }
1356
1711
  const reference = serviceFor(message);
1357
1712
  if (!reference) {
1358
1713
  ledger.closeUndelivered();
@@ -1364,7 +1719,7 @@ function createMessagePortServiceProvider(options) {
1364
1719
  sendError(message, "invalid_message", "dispatch");
1365
1720
  return;
1366
1721
  }
1367
- if (message.grantId !== void 0 && message.grantId !== reference.grantId) {
1722
+ if (message.grantId !== reference.grantId) {
1368
1723
  ledger.closeUndelivered();
1369
1724
  sendError(message, "permission_denied", "dispatch");
1370
1725
  return;
@@ -1403,7 +1758,7 @@ function createMessagePortServiceProvider(options) {
1403
1758
  releaseExecution(active);
1404
1759
  return;
1405
1760
  }
1406
- const result = await options.handleCall({ message: active.call, request: request.value, reference, signal: controller.signal, deadlineAt: active.deadlineAt, peer: callPeer });
1761
+ const result = await options.handleCall({ message: active.call, request: request.value, reference, signal: controller.signal, deadlineAt: active.deadlineAt, binding: active.call.binding, peer: callPeer });
1407
1762
  if (active.cancelled || disposed || executionSlots.get(message.callId) !== active) {
1408
1763
  if (message.mode === "stream") await closeLateIterable(result);
1409
1764
  releaseExecution(active);
@@ -1412,7 +1767,7 @@ function createMessagePortServiceProvider(options) {
1412
1767
  if (message.mode === "unary") {
1413
1768
  try {
1414
1769
  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");
1770
+ 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
1771
  } catch (error) {
1417
1772
  if (!active.cancelled) sendError(message, safeErrorCode(error, "response_validation_failed"), "receive");
1418
1773
  }
@@ -1424,7 +1779,7 @@ function createMessagePortServiceProvider(options) {
1424
1779
  const iterator = result[Symbol.asyncIterator]();
1425
1780
  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
1781
  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 })) {
1782
+ if (!post(transport, codec, { type: RUNTIME_RESULT_TYPE, protocolVersion: RUNTIME_PROTOCOL_VERSION, binding: localBinding, callId: message.callId, serviceInstanceId: reference.serviceInstanceId, streamReady: true })) {
1428
1783
  sendError(message, "transport_unavailable", "dispatch");
1429
1784
  closeStream(stream);
1430
1785
  return;
@@ -1448,12 +1803,22 @@ function createMessagePortServiceProvider(options) {
1448
1803
  }
1449
1804
  });
1450
1805
  };
1806
+ const drainExecutions = () => {
1807
+ if (executionSlots.size === 0) return Promise.resolve();
1808
+ return new Promise((resolve) => executionDrainWaiters.add(resolve));
1809
+ };
1810
+ session.registerDrainParticipant({
1811
+ drain: drainExecutions,
1812
+ pending: () => executionSlots.size
1813
+ });
1814
+ let removeTransport = () => void 0;
1451
1815
  function dispose() {
1452
1816
  if (disposed) return;
1453
1817
  disposed = true;
1454
1818
  for (const active of [...activeCalls.values()]) cancelActive(active, "service_revoked");
1455
1819
  for (const stream of [...streams.values()]) closeStream(stream);
1456
1820
  removeTransport();
1821
+ if (ownsSession) session.close();
1457
1822
  }
1458
1823
  function failClose() {
1459
1824
  dispose();
@@ -1461,12 +1826,30 @@ function createMessagePortServiceProvider(options) {
1461
1826
  transport.close?.();
1462
1827
  } catch {
1463
1828
  }
1829
+ session.close();
1464
1830
  }
1465
- const removeTransport = transport.subscribe(onMessage);
1831
+ removeTransport = transport.subscribe(onMessage);
1832
+ let removeSessionBeginClose = () => void 0;
1833
+ removeSessionBeginClose = session.onBeginClose(() => dispose());
1834
+ session.onClosed(() => {
1835
+ removeSessionBeginClose();
1836
+ });
1466
1837
  return {
1467
1838
  setServices() {
1468
1839
  },
1469
1840
  dispose,
1841
+ get binding() {
1842
+ return localBinding;
1843
+ },
1844
+ get endpointState() {
1845
+ return session.state;
1846
+ },
1847
+ beginClose(reason = "Runtime endpoint closing") {
1848
+ session.beginClose(reason);
1849
+ },
1850
+ drain(timeoutMs2) {
1851
+ return session.drain(timeoutMs2);
1852
+ },
1470
1853
  pendingCount() {
1471
1854
  return activeCalls.size;
1472
1855
  },
@@ -1498,6 +1881,7 @@ function createCapabilityPeerView(options) {
1498
1881
  const allowed = options.allowed === void 0 ? void 0 : new Set(options.allowed.map((descriptor) => capabilityKey(descriptor)));
1499
1882
  return Object.freeze({
1500
1883
  peerId: options.peerId,
1884
+ binding: Object.freeze({ ...options.binding ?? options.bridge.binding }),
1501
1885
  get runtime() {
1502
1886
  return options.bridge.runtimeKind;
1503
1887
  },
@@ -1523,7 +1907,7 @@ function makeWorker(options) {
1523
1907
  if (!WorkerConstructor) throw new RuntimeUnavailableError("SharedWorker is not supported by this browser");
1524
1908
  return new WorkerConstructor(options.url, workerOptions);
1525
1909
  }
1526
- function snapshotForClient(app, runtimeId, runtimeInstanceId, exposed, revision) {
1910
+ function snapshotForClient(app, runtimeId, runtimeInstanceId, binding, exposed, revision) {
1527
1911
  const allowed = new Set(exposed.map((capability) => capabilityKey(capability)));
1528
1912
  const state = app.state();
1529
1913
  const runtimeState = state.state === "disconnected" ? "failed" : state.state;
@@ -1536,7 +1920,8 @@ function snapshotForClient(app, runtimeId, runtimeInstanceId, exposed, revision)
1536
1920
  revision,
1537
1921
  state: runtimeState,
1538
1922
  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 } : {} })) : []
1923
+ 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 } : {} })) : [],
1924
+ binding
1540
1925
  };
1541
1926
  }
1542
1927
  function connectInternal(options) {
@@ -1573,18 +1958,22 @@ function connectInternal(options) {
1573
1958
  }, { limits });
1574
1959
  const outboundBudget = createRuntimeBudget(limits);
1575
1960
  const inboundBudget = createRuntimeBudget(limits);
1576
- const bridge = createCapabilityBridge({ transport, remoteRuntimeKind: "shared-worker", remoteRuntimeId: options.id, defaultCallTimeoutMs: options.defaultCallTimeoutMs, limits, budget: outboundBudget });
1961
+ const localRuntimeInstanceId = options.client?.app.runtimeInstanceId ?? `window:${Date.now().toString(36)}`;
1962
+ const binding = createRuntimeEndpointBinding(localRuntimeInstanceId);
1963
+ const session = createRuntimeEndpointSession(binding);
1964
+ const bridge = createCapabilityBridge({ transport, remoteRuntimeKind: "shared-worker", remoteRuntimeId: options.id, defaultCallTimeoutMs: options.defaultCallTimeoutMs, limits, budget: outboundBudget, binding, session });
1577
1965
  const listeners = /* @__PURE__ */ new Set();
1578
1966
  const workerInstanceId = { value: "" };
1579
- const localRuntimeInstanceId = options.client?.app.runtimeInstanceId ?? `window:${Date.now().toString(36)}`;
1580
1967
  let revision = 0;
1581
1968
  let disposed = false;
1969
+ let disposePromise;
1582
1970
  let current = Object.freeze({ protocolVersion: RUNTIME_PROTOCOL_VERSION, runtimeId: options.id, runtimeKind: "shared-worker", runtimeInstanceId: "", state: "starting", revision: 0, units: [], services: [] });
1583
1971
  const clientHost = requestedClientHost;
1584
1972
  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
1973
  const clientPeerScopeView = clientPeerScope ? createPeerScopeView(clientPeerScope) : void 0;
1586
1974
  const clientPeer = clientPeerScope && clientPeerScopeView ? createCapabilityPeerView({
1587
1975
  peerId: clientPeerScope.identity.attributes.peerId,
1976
+ binding,
1588
1977
  scope: clientPeerScopeView,
1589
1978
  bridge,
1590
1979
  capabilityScope: clientPeerScope
@@ -1593,6 +1982,8 @@ function connectInternal(options) {
1593
1982
  transport,
1594
1983
  peerScope: clientPeerScope,
1595
1984
  peer: clientPeer,
1985
+ binding,
1986
+ session,
1596
1987
  budget: inboundBudget,
1597
1988
  limits,
1598
1989
  services: () => clientHost.serviceReferences().filter((service) => exposed.some((capability) => capabilityKey(capability) === capabilityKey({ kind: service.kind, id: service.capabilityId, version: service.contractVersion }))),
@@ -1600,6 +1991,7 @@ function connectInternal(options) {
1600
1991
  const registration = clientHost.capabilities.registration({ kind: reference.kind, id: reference.capabilityId, version: reference.contractVersion });
1601
1992
  return clientPeerScope && clientPeerScopeView ? createCapabilityPeerView({
1602
1993
  peerId: clientPeerScope.identity.attributes.peerId,
1994
+ binding,
1603
1995
  scope: clientPeerScopeView,
1604
1996
  bridge,
1605
1997
  allowed: registration?.peerDependencies ?? [],
@@ -1618,12 +2010,12 @@ function connectInternal(options) {
1618
2010
  }
1619
2011
  return { value, transfer: capability.transfer?.request?.(value) };
1620
2012
  },
1621
- handleCall: async ({ request, reference, signal, deadlineAt, peer }) => {
2013
+ handleCall: async ({ request, reference, signal, deadlineAt, binding: callBinding, peer }) => {
1622
2014
  const registration = clientHost.capabilities.registration({ kind: reference.kind, id: reference.capabilityId, version: reference.contractVersion });
1623
2015
  if (!registration) throw new WebLoomError("service_stale", "Window exposure is no longer available", "dispatch");
1624
2016
  const handler = registration.handler;
1625
2017
  if (!handler) throw new WebLoomError("service_stale", "Window service handler is unavailable", "dispatch");
1626
- return handler(request, { signal, deadlineAt, reference, origin: "remote", peer });
2018
+ return handler(request, { signal, deadlineAt, reference, binding: callBinding, origin: "remote", peer });
1627
2019
  },
1628
2020
  prepareResult: (value, call) => {
1629
2021
  const registration = clientHost.capabilities.registration({ kind: "rpc", id: call.capabilityId, version: call.contractVersion });
@@ -1653,7 +2045,7 @@ function connectInternal(options) {
1653
2045
  if (!options.client || disposed) return;
1654
2046
  revision += 1;
1655
2047
  try {
1656
- transport.send(snapshotForClient(options.client.app, `${options.client.app.runtimeId}`, localRuntimeInstanceId, exposed, revision));
2048
+ transport.send(snapshotForClient(options.client.app, `${options.client.app.runtimeId}`, localRuntimeInstanceId, binding, exposed, revision));
1657
2049
  } catch {
1658
2050
  }
1659
2051
  };
@@ -1662,7 +2054,7 @@ function connectInternal(options) {
1662
2054
  const applied = bridge.applySnapshot(messageValue);
1663
2055
  if (applied.accepted) {
1664
2056
  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 });
2057
+ 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
2058
  }
1667
2059
  } else if (messageValue.type === RUNTIME_ERROR_TYPE) {
1668
2060
  bridge.invalidate(messageValue.message);
@@ -1670,12 +2062,13 @@ function connectInternal(options) {
1670
2062
  }
1671
2063
  });
1672
2064
  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
- }
2065
+ if (disposed) return;
2066
+ session.beginClose("SharedWorker disconnected");
2067
+ clientPeerScope?.revoke("SharedWorker disconnected");
2068
+ provider?.dispose();
2069
+ bridge.disconnect("SharedWorker disconnected");
2070
+ session.close();
2071
+ emit({ ...current, state: "disconnected", runtimeInstanceId: "", revision: 0, units: [], services: [], binding: void 0, error: "SharedWorker disconnected" });
1679
2072
  };
1680
2073
  port.addEventListener("messageerror", onError);
1681
2074
  if (worker.addEventListener) worker.addEventListener("error", onError);
@@ -1685,6 +2078,10 @@ function connectInternal(options) {
1685
2078
  const handle = {
1686
2079
  runtimeKind: "shared-worker",
1687
2080
  runtimeId: options.id,
2081
+ binding,
2082
+ get endpointState() {
2083
+ return session.state;
2084
+ },
1688
2085
  get runtimeInstanceId() {
1689
2086
  return workerInstanceId.value;
1690
2087
  },
@@ -1701,20 +2098,38 @@ function connectInternal(options) {
1701
2098
  listener(current);
1702
2099
  return () => listeners.delete(listener);
1703
2100
  },
2101
+ beginClose(reason = "SharedWorker connection closing") {
2102
+ if (disposed) return;
2103
+ session.beginClose(reason);
2104
+ clientPeerScope?.revoke(reason);
2105
+ },
2106
+ drain(timeoutMs2) {
2107
+ session.beginClose("SharedWorker connection drain requested");
2108
+ clientPeerScope?.revoke("SharedWorker connection drain requested");
2109
+ return bridge.drain(timeoutMs2);
2110
+ },
1704
2111
  dispose(reason = "SharedWorker connection disposed") {
1705
2112
  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();
2113
+ if (disposePromise) return disposePromise;
2114
+ disposePromise = (async () => {
2115
+ emit({ ...current, state: "stopping" });
2116
+ removeClient?.();
2117
+ removeRaw();
2118
+ clientPeerScope?.revoke(reason);
2119
+ session.beginClose(reason);
2120
+ const drainResult = bridge.drain();
2121
+ await drainResult.catch(() => void 0);
2122
+ disposed = true;
2123
+ provider?.dispose();
2124
+ bridge.dispose(reason);
2125
+ port.removeEventListener("messageerror", onError);
2126
+ worker.removeEventListener?.("error", onError);
2127
+ transport.close?.();
2128
+ session.close();
2129
+ emit({ ...current, state: "disposed", runtimeInstanceId: "", revision: 0, units: [], services: [], binding: void 0 });
2130
+ if (clientPeerScope) await clientPeerScope.dispose({ reason });
2131
+ })();
2132
+ return disposePromise;
1718
2133
  }
1719
2134
  };
1720
2135
  handleBridges.set(handle, bridge);
@@ -1732,6 +2147,6 @@ function bridgeForRuntimeHandle(handle) {
1732
2147
  return bridge;
1733
2148
  }
1734
2149
 
1735
- export { bridgeForRuntimeHandle, connectSharedWorker, connectSharedWorkerForTesting, createCapabilityBridge, createCapabilityPeerView, createMessagePortRuntimeTransport, createMessagePortServiceProvider, createPeerScopeView, validateTransferables };
1736
- //# sourceMappingURL=chunk-ANA6GBEI.js.map
1737
- //# sourceMappingURL=chunk-ANA6GBEI.js.map
2150
+ export { DEFAULT_RUNTIME_DRAIN_TIMEOUT_MS, bridgeForRuntimeHandle, connectSharedWorker, connectSharedWorkerForTesting, createCapabilityBridge, createCapabilityPeerView, createMessagePortRuntimeTransport, createMessagePortServiceProvider, createPeerScopeView, createRuntimeEndpointBinding, createRuntimeEndpointSession, isRuntimeEndpointBinding, sameRuntimeEndpointBinding, validateTransferables };
2151
+ //# sourceMappingURL=chunk-XVD7ALPV.js.map
2152
+ //# sourceMappingURL=chunk-XVD7ALPV.js.map