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.
@@ -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-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
- 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" });
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 !== void 0 && message.grantId !== reference.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
- const removeTransport = transport.subscribe(onMessage);
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 bridge = createCapabilityBridge({ transport, remoteRuntimeKind: "shared-worker", remoteRuntimeId: options.id, defaultCallTimeoutMs: options.defaultCallTimeoutMs, limits, budget: outboundBudget });
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 (!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
- }
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
- 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();
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
- export { bridgeForRuntimeHandle, connectSharedWorker, connectSharedWorkerForTesting, createCapabilityBridge, createCapabilityPeerView, createMessagePortRuntimeTransport, createMessagePortServiceProvider, createPeerScopeView, validateTransferables };
1736
- //# sourceMappingURL=chunk-ANA6GBEI.js.map
1737
- //# sourceMappingURL=chunk-ANA6GBEI.js.map
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