webloom-framework 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,16 +24,12 @@ var PermissionDeniedError = class extends Error {
24
24
  };
25
25
  function createRemoteServiceMessageCodec(options = {}) {
26
26
  const prefix = options.prefix ?? "webloom.remote-service";
27
- const protocolVersion = options.protocolVersion ?? "1";
27
+ const protocolVersion = options.protocolVersion ?? "webloom.remote-service.v2";
28
28
  const types = /* @__PURE__ */ new Map([
29
29
  ["call", `${prefix}.call`],
30
30
  ["result", `${prefix}.result`],
31
31
  ["error", `${prefix}.error`],
32
- ["cancel", `${prefix}.cancel`],
33
- ["handshake", `${prefix}.handshake`],
34
- ["snapshot", `${prefix}.snapshot`],
35
- ["invalidate", `${prefix}.invalidate`],
36
- ["disconnect", `${prefix}.disconnect`]
32
+ ["cancel", `${prefix}.cancel`]
37
33
  ]);
38
34
  return Object.freeze({
39
35
  protocolVersion,
@@ -53,11 +49,14 @@ function createRemoteServiceMessageCodec(options = {}) {
53
49
  }
54
50
  });
55
51
  }
56
- var RemoteServiceUnavailableError = class extends Error {
57
- code = "service.unavailable";
58
- constructor(message = "Remote service is unavailable") {
52
+ var RemoteServiceError = class extends Error {
53
+ code;
54
+ details;
55
+ constructor(code, message, details) {
59
56
  super(message);
60
- this.name = "RemoteServiceUnavailableError";
57
+ this.name = "RemoteServiceError";
58
+ this.code = code;
59
+ this.details = details;
61
60
  }
62
61
  };
63
62
  var UpgradeGateRejectedError = class extends Error {
@@ -3147,51 +3146,82 @@ function createRuntimeUnitImplementationRegistry(implementations = []) {
3147
3146
  };
3148
3147
  }
3149
3148
 
3150
- // src/transport/serviceBridge.ts
3151
- function makeRequestId() {
3152
- try {
3153
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
3154
- return `service-request:${crypto.randomUUID()}`;
3155
- }
3156
- } catch {
3157
- }
3158
- return `service-request:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
3159
- }
3160
- function mergeSignals2(...signals) {
3161
- const activeSignals = signals.filter((signal) => signal !== void 0);
3162
- if (activeSignals.length === 0) {
3163
- return { signal: new AbortController().signal, dispose: () => void 0 };
3164
- }
3165
- if (activeSignals.length === 1) {
3166
- const [signal] = activeSignals;
3167
- if (signal) return { signal, dispose: () => void 0 };
3149
+ // src/runtime/runtimeTypes.ts
3150
+ var RuntimeInitializationError = class extends Error {
3151
+ code = "runtime_initialization_failed";
3152
+ details;
3153
+ constructor(details) {
3154
+ super(
3155
+ `Runtime initialization failed${details.pluginId ? ` for ${details.pluginId}` : ""} during ${details.phase}: ${details.error}`
3156
+ );
3157
+ this.name = "RuntimeInitializationError";
3158
+ this.details = details;
3168
3159
  }
3169
- const alreadyAborted = activeSignals.find((signal) => signal.aborted);
3170
- if (alreadyAborted) {
3171
- const controller2 = new AbortController();
3172
- controller2.abort(alreadyAborted.reason);
3173
- return { signal: controller2.signal, dispose: () => void 0 };
3160
+ };
3161
+ var RuntimeUnavailableError = class extends Error {
3162
+ code = "transport_unavailable";
3163
+ constructor(message = "Runtime is unavailable") {
3164
+ super(message);
3165
+ this.name = "RuntimeUnavailableError";
3174
3166
  }
3175
- const controller = new AbortController();
3176
- const abort = (source) => {
3177
- try {
3178
- controller.abort(source.reason);
3179
- } catch {
3180
- controller.abort();
3181
- }
3182
- };
3183
- const listeners = activeSignals.map((signal) => {
3184
- const listener = () => abort(signal);
3185
- signal.addEventListener("abort", listener, { once: true });
3186
- return { signal, listener };
3167
+ };
3168
+
3169
+ // src/runtime/runtimeProtocol.ts
3170
+ var RUNTIME_PROTOCOL_VERSION = "webloom.runtime.v2";
3171
+ var RUNTIME_SNAPSHOT_TYPE = "webloom.runtime.snapshot";
3172
+ var RUNTIME_ERROR_TYPE = "webloom.runtime.error";
3173
+ function isRecord2(input) {
3174
+ return typeof input === "object" && input !== null;
3175
+ }
3176
+ function isNonEmptyString(input) {
3177
+ return typeof input === "string" && input.length > 0;
3178
+ }
3179
+ function isRuntimeKind2(input) {
3180
+ return input === "window-main" || input === "shared-worker";
3181
+ }
3182
+ function isPluginStateKind(input) {
3183
+ return input === "registered" || input === "starting" || input === "stopping" || input === "enabled" || input === "disabled" || input === "blocked" || input === "error-disabled" || input === "cleanup-pending" || input === "unknown";
3184
+ }
3185
+ function isRemoteServiceReference(input) {
3186
+ if (!isRecord2(input)) return false;
3187
+ const reference = input;
3188
+ return isNonEmptyString(reference.capabilityId) && isNonEmptyString(reference.contractVersion) && isRuntimeKind2(reference.runtime) && isNonEmptyString(reference.runtimeInstanceId) && isNonEmptyString(reference.serviceInstanceId) && (reference.status === "starting" || reference.status === "ready" || reference.status === "unavailable" || reference.status === "failed") && isRecord2(reference.attributes) && !Array.isArray(reference.attributes) && (reference.grantId === void 0 || isNonEmptyString(reference.grantId)) && (reference.authorizationRevision === void 0 || Number.isSafeInteger(reference.authorizationRevision) && reference.authorizationRevision >= 0);
3189
+ }
3190
+ function isRuntimeSnapshotUnit(input) {
3191
+ if (!isRecord2(input)) return false;
3192
+ const unit = input;
3193
+ return isNonEmptyString(unit.pluginId) && isNonEmptyString(unit.unitId) && isRuntimeKind2(unit.runtime) && isPluginStateKind(unit.state) && (unit.instanceId === void 0 || isNonEmptyString(unit.instanceId));
3194
+ }
3195
+ function createRuntimeMessageCodec() {
3196
+ return createRemoteServiceMessageCodec({
3197
+ prefix: "webloom.runtime",
3198
+ protocolVersion: RUNTIME_PROTOCOL_VERSION
3187
3199
  });
3200
+ }
3201
+ function isRuntimeSnapshot(input) {
3202
+ if (!isRecord2(input)) return false;
3203
+ const message = input;
3204
+ return message.type === RUNTIME_SNAPSHOT_TYPE && isNonEmptyString(message.protocolVersion) && isNonEmptyString(message.runtimeId) && isRuntimeKind2(message.runtimeKind) && isNonEmptyString(message.runtimeInstanceId) && Number.isSafeInteger(message.revision) && message.revision >= 0 && Array.isArray(message.units) && message.units.every(isRuntimeSnapshotUnit) && Array.isArray(message.services) && message.services.every(isRemoteServiceReference) && (message.state === "starting" || message.state === "ready" || message.state === "stopping" || message.state === "failed" || message.state === "disposed");
3205
+ }
3206
+ function isRuntimeSnapshotProtocol(input) {
3207
+ return input.protocolVersion === RUNTIME_PROTOCOL_VERSION;
3208
+ }
3209
+ function isRuntimeError(input) {
3210
+ if (!isRecord2(input)) return false;
3211
+ const message = input;
3212
+ return message.type === RUNTIME_ERROR_TYPE && isNonEmptyString(message.protocolVersion) && (message.code === "runtime_initialization_failed" || message.code === "protocol_mismatch" || message.code === "transport_unavailable") && isNonEmptyString(message.message) && (message.pluginId === void 0 || isNonEmptyString(message.pluginId)) && (message.unitId === void 0 || isNonEmptyString(message.unitId)) && (message.phase === void 0 || message.phase === "startup" || message.phase === "snapshot");
3213
+ }
3214
+ function unitSnapshotFromState(state, runtime) {
3188
3215
  return {
3189
- signal: controller.signal,
3190
- dispose: () => {
3191
- for (const item of listeners) item.signal.removeEventListener("abort", item.listener);
3192
- }
3216
+ pluginId: state.pluginId,
3217
+ unitId: state.unitId,
3218
+ runtime,
3219
+ ...state.instanceId !== void 0 ? { instanceId: state.instanceId } : {},
3220
+ state: state.kind
3193
3221
  };
3194
3222
  }
3223
+
3224
+ // src/transport/serviceBridge.ts
3195
3225
  function stableAttributes(value) {
3196
3226
  const normalize = (input) => {
3197
3227
  if (Array.isArray(input)) return input.map(normalize);
@@ -3202,34 +3232,127 @@ function stableAttributes(value) {
3202
3232
  };
3203
3233
  return JSON.stringify(normalize(value));
3204
3234
  }
3235
+ function isPlainRecord(value) {
3236
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3237
+ const prototype = Object.getPrototypeOf(value);
3238
+ return prototype === Object.prototype || prototype === null;
3239
+ }
3240
+ function isSafeAttributeValue(value, seen) {
3241
+ if (value === null || value === void 0 || typeof value === "string" || typeof value === "boolean") return true;
3242
+ if (typeof value === "number") return Number.isFinite(value);
3243
+ if (typeof value !== "object") return false;
3244
+ if (seen.has(value)) return false;
3245
+ seen.add(value);
3246
+ const valid = Array.isArray(value) ? value.every((item) => isSafeAttributeValue(item, seen)) : isPlainRecord(value) && !Reflect.ownKeys(value).some((key) => typeof key !== "string") && Object.values(value).every((item) => isSafeAttributeValue(item, seen));
3247
+ seen.delete(value);
3248
+ return valid;
3249
+ }
3250
+ function isValidAttributes(value) {
3251
+ return isPlainRecord(value) && isSafeAttributeValue(value, /* @__PURE__ */ new Set());
3252
+ }
3253
+ function isValidSnapshot(snapshot) {
3254
+ if (!snapshot || typeof snapshot !== "object" || typeof snapshot.protocolVersion !== "string" || snapshot.protocolVersion.length === 0 || typeof snapshot.runtimeInstanceId !== "string" || snapshot.runtimeInstanceId.length === 0 || !Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0 || !Array.isArray(snapshot.services)) return false;
3255
+ if (snapshot.runtimeId !== void 0 && (typeof snapshot.runtimeId !== "string" || snapshot.runtimeId.length === 0)) return false;
3256
+ if (snapshot.runtimeKind !== void 0 && snapshot.runtimeKind !== "window-main" && snapshot.runtimeKind !== "shared-worker") return false;
3257
+ if (snapshot.state !== void 0 && snapshot.state !== "starting" && snapshot.state !== "ready" && snapshot.state !== "stopping" && snapshot.state !== "failed" && snapshot.state !== "disposed") return false;
3258
+ const bindingKeys = /* @__PURE__ */ new Set();
3259
+ const serviceCapabilityKeys = /* @__PURE__ */ new Set();
3260
+ const readyLookupKeys = /* @__PURE__ */ new Set();
3261
+ for (const service of snapshot.services) {
3262
+ if (!service || typeof service !== "object" || typeof service.capabilityId !== "string" || service.capabilityId.length === 0 || typeof service.contractVersion !== "string" || service.contractVersion.length === 0 || service.runtime !== "window-main" && service.runtime !== "shared-worker" || service.runtimeInstanceId !== snapshot.runtimeInstanceId || typeof service.serviceInstanceId !== "string" || service.serviceInstanceId.length === 0 || service.status !== "starting" && service.status !== "ready" && service.status !== "unavailable" && service.status !== "failed" || !isValidAttributes(service.attributes) || service.grantId !== void 0 && (typeof service.grantId !== "string" || service.grantId.length === 0) || service.authorizationRevision !== void 0 && (!Number.isSafeInteger(service.authorizationRevision) || service.authorizationRevision < 0)) return false;
3263
+ if (snapshot.runtimeKind !== void 0 && service.runtime !== snapshot.runtimeKind) return false;
3264
+ const key = bindingKey(service);
3265
+ if (bindingKeys.has(key)) return false;
3266
+ bindingKeys.add(key);
3267
+ const serviceCapabilityKey = `${service.capabilityId}\0${service.serviceInstanceId}`;
3268
+ if (serviceCapabilityKeys.has(serviceCapabilityKey)) return false;
3269
+ serviceCapabilityKeys.add(serviceCapabilityKey);
3270
+ if (service.status === "ready") {
3271
+ const lookupKey = `${service.capabilityId}\0${service.contractVersion}\0${service.runtime}`;
3272
+ if (readyLookupKeys.has(lookupKey)) return false;
3273
+ readyLookupKeys.add(lookupKey);
3274
+ }
3275
+ }
3276
+ return true;
3277
+ }
3205
3278
  function referenceKey(reference) {
3206
3279
  return [
3207
3280
  reference.capabilityId,
3208
- reference.providerInstanceId,
3209
- reference.runtime,
3210
3281
  reference.contractVersion,
3211
- reference.authorityInstanceId,
3212
- reference.connectionId ?? "local",
3213
- reference.scopeId,
3214
- reference.handoverGeneration,
3282
+ reference.runtime,
3283
+ reference.runtimeInstanceId,
3284
+ reference.serviceInstanceId,
3285
+ reference.status,
3215
3286
  stableAttributes(reference.attributes),
3216
3287
  reference.grantId ?? "null",
3217
- reference.authorizationRevision ?? "null",
3218
- // 代理绑定的是某一份权威目录快照;目录修订后即使 Provider 实例未变,
3219
- // 旧代理也不能继续使用旧授权视图,必须重新取得当前引用。
3220
- reference.snapshotRevision
3288
+ reference.authorizationRevision ?? "null"
3289
+ ].join("\0");
3290
+ }
3291
+ function bindingKey(reference) {
3292
+ return [
3293
+ reference.capabilityId,
3294
+ reference.contractVersion,
3295
+ reference.runtime,
3296
+ reference.runtimeInstanceId,
3297
+ reference.serviceInstanceId
3221
3298
  ].join("\0");
3222
3299
  }
3223
3300
  function lookupMatches(reference, lookup) {
3224
- return reference.status === "ready" && reference.capabilityId === lookup.capabilityId && reference.contractVersion === lookup.contractVersion && (lookup.runtime === void 0 || reference.runtime === lookup.runtime) && (lookup.scopeId === void 0 || reference.scopeId === lookup.scopeId);
3301
+ return reference.capabilityId === lookup.capabilityId && reference.contractVersion === lookup.contractVersion && (lookup.runtime === void 0 || reference.runtime === lookup.runtime);
3302
+ }
3303
+ function ensureTimeout(value, label) {
3304
+ const timeout = value ?? 3e4;
3305
+ if (!Number.isFinite(timeout) || timeout <= 0) {
3306
+ throw new TypeError(`${label} must be a finite number greater than zero`);
3307
+ }
3308
+ return timeout;
3309
+ }
3310
+ function mergeSignals2(...signals) {
3311
+ const active = signals.filter((signal) => signal !== void 0);
3312
+ if (active.length === 0) return { signal: new AbortController().signal, dispose: () => void 0 };
3313
+ const alreadyAborted = active.find((signal) => signal.aborted);
3314
+ if (alreadyAborted) {
3315
+ const controller2 = new AbortController();
3316
+ controller2.abort(alreadyAborted.reason);
3317
+ return { signal: controller2.signal, dispose: () => void 0 };
3318
+ }
3319
+ const controller = new AbortController();
3320
+ const listeners = active.map((signal) => {
3321
+ const listener = () => {
3322
+ try {
3323
+ controller.abort(signal.reason);
3324
+ } catch {
3325
+ controller.abort();
3326
+ }
3327
+ };
3328
+ signal.addEventListener("abort", listener, { once: true });
3329
+ return { signal, listener };
3330
+ });
3331
+ return {
3332
+ signal: controller.signal,
3333
+ dispose: () => {
3334
+ for (const { signal, listener } of listeners) signal.removeEventListener("abort", listener);
3335
+ }
3336
+ };
3337
+ }
3338
+ function abortedError(signal, fallback = "request_cancelled") {
3339
+ const reason = signal.reason;
3340
+ if (reason instanceof Error && typeof reason.code === "string") return reason;
3341
+ if (reason instanceof Error && fallback === "request_cancelled") {
3342
+ return new RemoteServiceError("request_cancelled", reason.message);
3343
+ }
3344
+ return new RemoteServiceError(fallback, fallback === "call_timeout" ? "Remote service call timed out" : "Remote service request was cancelled");
3345
+ }
3346
+ function callTimeoutError(timeoutMs) {
3347
+ return new RemoteServiceError("call_timeout", `Remote service call exceeded its ${timeoutMs}ms deadline`, { timeoutMs });
3225
3348
  }
3226
3349
  function createServiceBridge(options) {
3227
- let currentState = "disconnected";
3228
- let currentConnectionId;
3229
- let currentAuthorityInstanceId;
3230
- let lastRevision;
3231
- let hasBaseline = false;
3232
- const references = /* @__PURE__ */ new Map();
3350
+ const defaultCallTimeoutMs = ensureTimeout(options.defaultCallTimeoutMs, "defaultCallTimeoutMs");
3351
+ let currentState = "empty";
3352
+ let currentRuntimeInstanceId;
3353
+ let currentRevision;
3354
+ let currentServices = [];
3355
+ let terminalError;
3233
3356
  const proxies = /* @__PURE__ */ new Set();
3234
3357
  const listeners = /* @__PURE__ */ new Set();
3235
3358
  const notify = () => {
@@ -3240,155 +3363,162 @@ function createServiceBridge(options) {
3240
3363
  }
3241
3364
  }
3242
3365
  };
3243
- const revokeProxy = (proxy, reason) => {
3244
- if (proxy.revoked) return;
3245
- proxy.revoked = true;
3246
- proxy.reason = reason;
3366
+ const revokeProxy = (record, reason, code = "service_revoked") => {
3367
+ if (record.revoked) return;
3368
+ record.revoked = true;
3369
+ record.reason = reason;
3247
3370
  try {
3248
- proxy.controller.abort(new RemoteServiceUnavailableError(reason));
3371
+ record.controller.abort(new RemoteServiceError(code, reason));
3249
3372
  } catch {
3250
- proxy.controller.abort();
3373
+ record.controller.abort();
3251
3374
  }
3252
3375
  };
3253
- const invalidateProxies = (reason, keep) => {
3254
- for (const proxy of proxies) {
3255
- if (!keep?.has(referenceKey(proxy.reference))) {
3256
- revokeProxy(proxy, reason);
3257
- proxies.delete(proxy);
3258
- }
3376
+ const revokeAll = (reason, code = "service_revoked") => {
3377
+ for (const record of proxies) revokeProxy(record, reason, code);
3378
+ };
3379
+ const currentMatches = (lookup) => currentServices.filter((reference) => reference.status === "ready" && lookupMatches(reference, lookup));
3380
+ const findBinding = (record, lookup) => {
3381
+ if (record.boundReference) return record.boundReference;
3382
+ if (terminalError) throw terminalError;
3383
+ const matches = currentMatches(lookup);
3384
+ if (matches.length > 1) {
3385
+ throw new RemoteServiceError(
3386
+ "capability_unavailable",
3387
+ `Service "${lookup.capabilityId}" version "${lookup.contractVersion}" has multiple ready instances`
3388
+ );
3389
+ }
3390
+ const reference = matches[0];
3391
+ if (reference) {
3392
+ record.boundReference = Object.freeze({
3393
+ ...reference,
3394
+ attributes: Object.freeze({ ...reference.attributes })
3395
+ });
3396
+ return record.boundReference;
3259
3397
  }
3398
+ return void 0;
3260
3399
  };
3261
- const clearReferences = (reason) => {
3262
- references.clear();
3263
- invalidateProxies(reason);
3400
+ const waitForBinding = (record, lookup, signal) => {
3401
+ const immediate = findBinding(record, lookup);
3402
+ if (immediate) return Promise.resolve(immediate);
3403
+ if (record.revoked) return Promise.reject(new RemoteServiceError("service_revoked", record.reason));
3404
+ if (signal.aborted) return Promise.reject(abortedError(signal));
3405
+ return new Promise((resolve, reject) => {
3406
+ let settled = false;
3407
+ const finish = (callback) => {
3408
+ if (settled) return;
3409
+ settled = true;
3410
+ removeListener();
3411
+ callback();
3412
+ };
3413
+ const check = () => {
3414
+ if (record.revoked) {
3415
+ finish(() => reject(new RemoteServiceError("service_revoked", record.reason)));
3416
+ return;
3417
+ }
3418
+ if (signal.aborted) {
3419
+ finish(() => reject(abortedError(signal)));
3420
+ return;
3421
+ }
3422
+ try {
3423
+ const reference = findBinding(record, lookup);
3424
+ if (reference) finish(() => resolve(reference));
3425
+ } catch (error) {
3426
+ finish(() => reject(error));
3427
+ }
3428
+ };
3429
+ const onAbort = () => check();
3430
+ const listener = () => check();
3431
+ const removeListener = () => {
3432
+ signal.removeEventListener("abort", onAbort);
3433
+ listeners.delete(listener);
3434
+ };
3435
+ signal.addEventListener("abort", onAbort, { once: true });
3436
+ listeners.add(listener);
3437
+ check();
3438
+ });
3264
3439
  };
3265
3440
  const bridge = {
3266
3441
  get state() {
3267
3442
  return currentState;
3268
3443
  },
3269
- get connectionId() {
3270
- return currentConnectionId;
3444
+ get runtimeInstanceId() {
3445
+ return currentRuntimeInstanceId;
3271
3446
  },
3272
- get authorityInstanceId() {
3273
- return currentAuthorityInstanceId;
3274
- },
3275
- handshake(input) {
3276
- if (input.protocolVersion !== options.protocolVersion) {
3277
- clearReferences("service bridge protocol mismatch");
3278
- currentConnectionId = void 0;
3279
- currentAuthorityInstanceId = void 0;
3280
- lastRevision = void 0;
3281
- hasBaseline = false;
3282
- currentState = "disconnected";
3283
- notify();
3284
- return { accepted: false, reason: "protocol-mismatch" };
3285
- }
3286
- const isNewBinding = currentConnectionId !== input.connectionId || currentAuthorityInstanceId !== input.authorityInstanceId;
3287
- if (isNewBinding) {
3288
- clearReferences("remote service authority or connection changed");
3289
- lastRevision = void 0;
3290
- hasBaseline = false;
3291
- }
3292
- currentConnectionId = input.connectionId;
3293
- currentAuthorityInstanceId = input.authorityInstanceId;
3294
- currentState = "handshaking";
3295
- notify();
3296
- return { accepted: true };
3447
+ get defaultCallTimeoutMs() {
3448
+ return defaultCallTimeoutMs;
3297
3449
  },
3298
3450
  applySnapshot(snapshot) {
3299
- if (snapshot.connectionId !== currentConnectionId) {
3300
- return {
3301
- accepted: false,
3302
- reason: "wrong-connection",
3303
- receivedRevision: snapshot.snapshotRevision
3304
- };
3451
+ if (currentState === "disposed") return { accepted: false, reason: "disposed" };
3452
+ if (snapshot.protocolVersion !== options.protocolVersion) {
3453
+ return { accepted: false, reason: "protocol-mismatch", receivedRevision: snapshot.revision };
3305
3454
  }
3306
- if (snapshot.authorityInstanceId !== currentAuthorityInstanceId) {
3307
- return {
3308
- accepted: false,
3309
- reason: "wrong-authority",
3310
- receivedRevision: snapshot.snapshotRevision
3311
- };
3455
+ if (!isValidSnapshot(snapshot)) {
3456
+ return { accepted: false, reason: "invalid-snapshot", receivedRevision: snapshot.revision };
3312
3457
  }
3313
- if (!Number.isSafeInteger(snapshot.snapshotRevision) || snapshot.snapshotRevision < 0) {
3314
- return {
3315
- accepted: false,
3316
- reason: "stale-revision",
3317
- receivedRevision: snapshot.snapshotRevision
3318
- };
3458
+ if (currentRuntimeInstanceId === snapshot.runtimeInstanceId && currentRevision !== void 0 && snapshot.revision <= currentRevision) {
3459
+ return { accepted: false, reason: "stale-revision", receivedRevision: snapshot.revision };
3319
3460
  }
3320
- if (!snapshot.baseline && !hasBaseline) {
3321
- currentState = "stale";
3322
- clearReferences("service bridge baseline required");
3323
- notify();
3324
- return {
3325
- accepted: false,
3326
- reason: "baseline-required",
3327
- receivedRevision: snapshot.snapshotRevision
3328
- };
3329
- }
3330
- if (lastRevision !== void 0 && snapshot.snapshotRevision <= lastRevision) {
3331
- return {
3332
- accepted: false,
3333
- reason: "stale-revision",
3334
- receivedRevision: snapshot.snapshotRevision
3335
- };
3461
+ const runtimeChanged = currentRuntimeInstanceId !== void 0 && currentRuntimeInstanceId !== snapshot.runtimeInstanceId;
3462
+ if (runtimeChanged) revokeAll("Runtime instance changed; proxy cannot be rebound", "service_stale");
3463
+ const nextServices = snapshot.services.map((service) => Object.freeze({
3464
+ ...service,
3465
+ attributes: Object.freeze({ ...service.attributes })
3466
+ }));
3467
+ const nextReady = new Map(nextServices.filter((service) => service.status === "ready").map((service) => [bindingKey(service), service]));
3468
+ if (!runtimeChanged) {
3469
+ for (const record of proxies) {
3470
+ const bound = record.boundReference;
3471
+ if (!bound) continue;
3472
+ const replacement = nextReady.get(bindingKey(bound));
3473
+ if (!replacement || referenceKey(replacement) !== referenceKey(bound)) {
3474
+ revokeProxy(record, "Bound service instance was replaced or revoked", "service_revoked");
3475
+ }
3476
+ }
3336
3477
  }
3337
- if (!snapshot.baseline && lastRevision !== void 0 && snapshot.snapshotRevision !== lastRevision + 1) {
3478
+ currentRuntimeInstanceId = snapshot.runtimeInstanceId;
3479
+ currentRevision = snapshot.revision;
3480
+ currentServices = Object.freeze(nextServices);
3481
+ terminalError = void 0;
3482
+ if (snapshot.state === "failed") {
3338
3483
  currentState = "stale";
3339
- clearReferences("service bridge revision gap");
3340
- hasBaseline = false;
3341
- notify();
3342
- return {
3343
- accepted: false,
3344
- reason: "revision-gap",
3345
- expectedRevision: lastRevision + 1,
3346
- receivedRevision: snapshot.snapshotRevision
3347
- };
3348
- }
3349
- const next = /* @__PURE__ */ new Map();
3350
- for (const reference of snapshot.services) {
3351
- if (reference.authorityInstanceId !== currentAuthorityInstanceId || reference.connectionId !== void 0 && reference.connectionId !== snapshot.connectionId || reference.snapshotRevision !== snapshot.snapshotRevision) continue;
3352
- next.set(referenceKey(reference), Object.freeze({ ...reference }));
3484
+ terminalError = new RemoteServiceError("runtime_initialization_failed", "Remote Runtime initialization failed");
3485
+ } else if (snapshot.state === "disposed" || snapshot.state === "stopping") {
3486
+ currentState = "stale";
3487
+ terminalError = new RemoteServiceError("transport_unavailable", "Remote Runtime is no longer accepting calls");
3488
+ revokeAll("Remote Runtime is no longer accepting calls", "transport_unavailable");
3489
+ } else if (snapshot.state === "ready") {
3490
+ currentState = "ready";
3491
+ } else {
3492
+ currentState = "empty";
3353
3493
  }
3354
- references.clear();
3355
- for (const [key, reference] of next) references.set(key, reference);
3356
- const validKeys = new Set(
3357
- [...next.values()].filter((reference) => reference.status === "ready").map(referenceKey)
3358
- );
3359
- invalidateProxies("remote service reference is no longer ready", validKeys);
3360
- lastRevision = snapshot.snapshotRevision;
3361
- hasBaseline = true;
3362
- currentState = "ready";
3363
3494
  notify();
3364
- return {
3365
- accepted: true,
3366
- state: currentState,
3367
- snapshotRevision: snapshot.snapshotRevision
3368
- };
3495
+ return { accepted: true, state: currentState, revision: snapshot.revision };
3496
+ },
3497
+ markProtocolMismatch(reason = "Remote Runtime protocol version mismatch") {
3498
+ if (currentState === "disposed") return;
3499
+ terminalError = new RemoteServiceError("protocol_mismatch", reason);
3500
+ revokeAll(reason, "protocol_mismatch");
3501
+ currentState = "stale";
3502
+ notify();
3503
+ },
3504
+ markInitializationFailed(reason = "Remote Runtime initialization failed") {
3505
+ if (currentState === "disposed") return;
3506
+ terminalError = new RemoteServiceError("runtime_initialization_failed", reason);
3507
+ revokeAll(reason, "runtime_initialization_failed");
3508
+ currentState = "stale";
3509
+ notify();
3369
3510
  },
3370
3511
  getProxy(lookup, scope) {
3371
- if (currentState !== "ready") return void 0;
3372
- const matches = [...references.values()].filter((candidate) => lookupMatches(candidate, lookup));
3373
- if (matches.length > 1) {
3374
- throw new RemoteServiceUnavailableError(
3375
- `Service reference is ambiguous for "${lookup.capabilityId}" version "${lookup.contractVersion}"`
3376
- );
3377
- }
3378
- const reference = matches[0];
3379
- if (!reference) return void 0;
3380
3512
  const record = {
3381
- reference,
3382
3513
  revoked: false,
3383
- reason: "remote service proxy revoked",
3514
+ reason: "Remote service proxy revoked",
3384
3515
  controller: new AbortController()
3385
3516
  };
3386
3517
  proxies.add(record);
3387
3518
  let removeScopeRevoke;
3388
3519
  let removeScopeDispose;
3389
- let proxy;
3390
3520
  const revokeFromScope = (reason) => {
3391
- revokeProxy(record, reason);
3521
+ revokeProxy(record, reason, "service_revoked");
3392
3522
  proxies.delete(record);
3393
3523
  removeScopeRevoke?.();
3394
3524
  removeScopeDispose?.();
@@ -3396,70 +3526,68 @@ function createServiceBridge(options) {
3396
3526
  if (scope) {
3397
3527
  try {
3398
3528
  removeScopeRevoke = scope.onRevoke(revokeFromScope);
3399
- removeScopeDispose = scope.onDispose(revokeFromScope, `service-proxy:${reference.capabilityId}`);
3529
+ removeScopeDispose = scope.onDispose(revokeFromScope, `service-proxy:${lookup.capabilityId}`);
3400
3530
  } catch {
3401
- record.revoked = true;
3531
+ revokeFromScope("Remote service proxy scope is already revoked");
3402
3532
  }
3403
3533
  }
3404
- if (record.revoked) {
3405
- proxies.delete(record);
3406
- removeScopeRevoke?.();
3407
- removeScopeDispose?.();
3408
- return void 0;
3409
- }
3410
- proxy = {
3411
- reference,
3534
+ const proxy = {
3535
+ get reference() {
3536
+ return record.boundReference;
3537
+ },
3412
3538
  get revoked() {
3413
3539
  return record.revoked;
3414
3540
  },
3415
3541
  async call(request, callOptions = {}) {
3416
- if (record.revoked) throw new RemoteServiceUnavailableError(record.reason);
3417
- const current = references.get(referenceKey(reference));
3418
- if (currentState !== "ready" || !current || current.status !== "ready") {
3419
- record.revoked = true;
3420
- record.reason = "Remote service reference is no longer ready";
3421
- throw new RemoteServiceUnavailableError(record.reason);
3422
- }
3423
- const merged = mergeSignals2(scope?.signal, record.controller.signal, callOptions.signal);
3424
- const connectionId = currentConnectionId;
3425
- if (!connectionId) {
3426
- merged.dispose();
3427
- throw new RemoteServiceUnavailableError("Remote service connection is no longer active");
3428
- }
3429
- const context = {
3430
- // requestId 只作为旧调用方的业务 operationId 别名;真正的
3431
- // MessagePort callId 由传输层每次调用单独生成。
3432
- operationId: callOptions.operationId ?? callOptions.requestId ?? makeRequestId(),
3433
- connectionId,
3434
- reference,
3435
- grantId: reference.grantId,
3436
- signal: merged.signal
3437
- };
3438
- let result;
3439
- try {
3440
- result = Promise.resolve(options.transport.call(request, context));
3441
- } catch (error) {
3442
- merged.dispose();
3443
- throw error;
3444
- }
3542
+ if (record.revoked) throw new RemoteServiceError("service_revoked", record.reason);
3543
+ const timeoutMs = ensureTimeout(callOptions.timeoutMs ?? defaultCallTimeoutMs, "timeoutMs");
3544
+ const deadlineAt = Date.now() + timeoutMs;
3545
+ const timeoutController = new AbortController();
3546
+ const timeout = setTimeout(() => {
3547
+ try {
3548
+ timeoutController.abort(callTimeoutError(timeoutMs));
3549
+ } catch {
3550
+ timeoutController.abort();
3551
+ }
3552
+ }, timeoutMs);
3553
+ const merged = mergeSignals2(scope?.signal, record.controller.signal, callOptions.signal, timeoutController.signal);
3445
3554
  try {
3555
+ const reference = await waitForBinding(record, lookup, merged.signal);
3556
+ if (merged.signal.aborted) throw abortedError(merged.signal);
3557
+ const context = {
3558
+ operationId: callOptions.operationId ?? callOptions.requestId,
3559
+ reference,
3560
+ grantId: reference.grantId,
3561
+ signal: merged.signal,
3562
+ deadlineAt,
3563
+ timeoutMs
3564
+ };
3565
+ let result;
3566
+ try {
3567
+ result = Promise.resolve(options.transport.call(request, context));
3568
+ } catch (error) {
3569
+ throw error;
3570
+ }
3446
3571
  const value = await Promise.race([
3447
3572
  result,
3448
3573
  new Promise((_, reject) => {
3449
3574
  if (merged.signal.aborted) {
3450
- reject(merged.signal.reason ?? new RemoteServiceUnavailableError("Remote service request aborted"));
3575
+ reject(abortedError(merged.signal));
3451
3576
  return;
3452
3577
  }
3453
- const onAbort = () => reject(merged.signal.reason ?? new RemoteServiceUnavailableError("Remote service request aborted"));
3578
+ const onAbort = () => reject(abortedError(merged.signal));
3454
3579
  merged.signal.addEventListener("abort", onAbort, { once: true });
3455
3580
  result.finally(() => merged.signal.removeEventListener("abort", onAbort)).catch(() => void 0);
3456
3581
  })
3457
3582
  ]);
3458
- if (record.revoked || currentState !== "ready" || references.get(referenceKey(reference))?.status !== "ready") {
3459
- throw new RemoteServiceUnavailableError("Remote service request belongs to an invalidated proxy");
3460
- }
3583
+ if (record.revoked) throw new RemoteServiceError("service_revoked", record.reason);
3461
3584
  return value;
3585
+ } catch (error) {
3586
+ if (merged.signal.aborted) throw abortedError(merged.signal);
3587
+ if (error instanceof RemoteServiceError) throw error;
3588
+ throw error;
3462
3589
  } finally {
3590
+ clearTimeout(timeout);
3463
3591
  merged.dispose();
3464
3592
  if (record.revoked) {
3465
3593
  proxies.delete(record);
@@ -3468,124 +3596,203 @@ function createServiceBridge(options) {
3468
3596
  }
3469
3597
  }
3470
3598
  },
3471
- revoke(reason = "remote service proxy revoked") {
3472
- revokeProxy(record, reason);
3473
- proxies.delete(record);
3474
- removeScopeRevoke?.();
3475
- removeScopeDispose?.();
3599
+ revoke(reason = "Remote service proxy revoked") {
3600
+ revokeFromScope(reason);
3476
3601
  }
3477
3602
  };
3478
3603
  return proxy;
3479
3604
  },
3480
3605
  requireProxy(lookup, scope) {
3481
- const proxy = bridge.getProxy(lookup, scope);
3482
- if (!proxy) {
3483
- throw new RemoteServiceUnavailableError(
3484
- `Service "${lookup.capabilityId}" version "${lookup.contractVersion}" is not ready`
3485
- );
3486
- }
3487
- return proxy;
3606
+ return bridge.getProxy(lookup, scope);
3488
3607
  },
3489
- invalidate(reason = "remote service bridge invalidated") {
3490
- clearReferences(reason);
3608
+ invalidate(reason = "Remote service directory invalidated") {
3609
+ if (currentState === "disposed") return;
3610
+ currentServices = [];
3611
+ currentRevision = void 0;
3612
+ terminalError = void 0;
3613
+ revokeAll(reason, "service_revoked");
3491
3614
  currentState = "stale";
3492
- lastRevision = void 0;
3493
- hasBaseline = false;
3494
3615
  notify();
3495
3616
  },
3496
- disconnect(reason = "remote service connection disconnected") {
3497
- clearReferences(reason);
3498
- currentConnectionId = void 0;
3499
- currentAuthorityInstanceId = void 0;
3500
- lastRevision = void 0;
3501
- hasBaseline = false;
3502
- currentState = "disconnected";
3617
+ disconnect(reason = "Remote service transport disconnected") {
3618
+ if (currentState === "disposed") return;
3619
+ currentServices = [];
3620
+ currentRevision = void 0;
3621
+ currentRuntimeInstanceId = void 0;
3622
+ terminalError = new RemoteServiceError("transport_unavailable", reason);
3623
+ revokeAll(reason, "transport_unavailable");
3624
+ currentState = "stale";
3625
+ notify();
3626
+ },
3627
+ dispose(reason = "Remote service bridge disposed") {
3628
+ if (currentState === "disposed") return;
3629
+ currentServices = [];
3630
+ currentRevision = void 0;
3631
+ currentRuntimeInstanceId = void 0;
3632
+ terminalError = new RemoteServiceError("transport_unavailable", reason);
3633
+ revokeAll(reason, "transport_unavailable");
3634
+ currentState = "disposed";
3503
3635
  notify();
3636
+ proxies.clear();
3504
3637
  },
3505
3638
  subscribe(listener) {
3506
3639
  listeners.add(listener);
3507
3640
  return () => listeners.delete(listener);
3508
3641
  },
3509
3642
  services() {
3510
- return [...references.values()];
3643
+ return currentServices;
3511
3644
  }
3512
3645
  };
3513
3646
  return bridge;
3514
3647
  }
3515
- var createRemoteServiceBridge = createServiceBridge;
3516
3648
 
3517
3649
  // src/transport/messagePortServiceTransport.ts
3518
3650
  var nextTransportId = 0;
3519
3651
  var nextCallSequence = 0;
3520
- function makeTransportId() {
3521
- nextTransportId += 1;
3522
- return nextTransportId;
3523
- }
3524
3652
  function makeCallId(transportId) {
3525
3653
  nextCallSequence += 1;
3526
3654
  return `remote-call:${transportId}:${nextCallSequence}`;
3527
3655
  }
3656
+ function finiteTimeout(value) {
3657
+ const timeout = value ?? 3e4;
3658
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new TypeError("Remote service timeout must be finite and greater than zero");
3659
+ return timeout;
3660
+ }
3528
3661
  function errorFromWire(input) {
3529
- const error = new Error(
3530
- typeof input?.message === "string" ? input.message : "Remote service call failed"
3662
+ return new RemoteServiceError(
3663
+ typeof input?.code === "string" && input.code.length > 0 ? input.code : "handler_failed",
3664
+ typeof input?.message === "string" ? input.message : "Remote service call failed",
3665
+ input?.details
3531
3666
  );
3532
- if (typeof input?.name === "string" && input.name.length > 0) error.name = input.name;
3533
- if (typeof input?.code === "string" && input.code.length > 0) {
3534
- Object.defineProperty(error, "code", {
3535
- configurable: true,
3536
- enumerable: true,
3537
- value: input.code,
3538
- writable: false
3539
- });
3540
- }
3541
- return error;
3542
3667
  }
3543
3668
  function abortReason(signal) {
3544
- return signal.reason ?? new RemoteServiceUnavailableError("Remote service request aborted");
3669
+ const reason = signal.reason;
3670
+ if (reason instanceof Error && typeof reason.code === "string") return reason;
3671
+ if (reason instanceof Error) return new RemoteServiceError("request_cancelled", reason.message);
3672
+ return new RemoteServiceError("request_cancelled", "Remote service request was cancelled");
3673
+ }
3674
+ function addMessageListener(port, listener) {
3675
+ port.addEventListener("message", listener);
3676
+ return () => port.removeEventListener("message", listener);
3677
+ }
3678
+ function postBestEffort(port, message, transfer = []) {
3679
+ try {
3680
+ port.postMessage(message, [...transfer]);
3681
+ } catch {
3682
+ }
3683
+ }
3684
+ function postRequest(port, message, transfer = []) {
3685
+ try {
3686
+ port.postMessage(message, [...transfer]);
3687
+ } catch (error) {
3688
+ const name = error && typeof error === "object" && typeof error.name === "string" ? error.name : "DataCloneError";
3689
+ const messageText = error instanceof Error ? error.message : String(error);
3690
+ throw new RemoteServiceError(
3691
+ "request_clone_failed",
3692
+ `Remote service request could not be posted: ${messageText}`,
3693
+ { name, message: messageText }
3694
+ );
3695
+ }
3545
3696
  }
3546
3697
  function isResponseMessage(input, codec) {
3547
3698
  if (!input || typeof input !== "object") return false;
3548
3699
  const message = input;
3549
- return (message.type === codec.type("result") || message.type === codec.type("error")) && typeof message.callId === "string" && typeof message.connectionId === "string" && typeof message.providerInstanceId === "string";
3700
+ return (message.type === codec.type("result") || message.type === codec.type("error")) && typeof message.protocolVersion === "string" && typeof message.callId === "string" && typeof message.serviceInstanceId === "string";
3550
3701
  }
3551
3702
  function createMessagePortServiceTransport(options) {
3552
3703
  const pending = /* @__PURE__ */ new Map();
3553
3704
  let disposed = false;
3554
- const transportId = makeTransportId();
3705
+ const transportId = ++nextTransportId;
3555
3706
  const codec = options.codec ?? createRemoteServiceMessageCodec();
3707
+ const defaultCallTimeoutMs = finiteTimeout(options.defaultCallTimeoutMs);
3708
+ const rejectPending = (error, sendCancel) => {
3709
+ for (const [callId, call] of pending) {
3710
+ pending.delete(callId);
3711
+ call.removeAbort();
3712
+ call.disposeDeadline();
3713
+ if (sendCancel) {
3714
+ postBestEffort(options.port, codec.encode({
3715
+ type: codec.type("cancel"),
3716
+ protocolVersion: codec.protocolVersion,
3717
+ callId,
3718
+ serviceInstanceId: call.serviceInstanceId
3719
+ }));
3720
+ }
3721
+ call.reject(error);
3722
+ }
3723
+ };
3556
3724
  const onMessage = (event) => {
3557
3725
  const decoded = codec.decode(event.data);
3558
3726
  if (!isResponseMessage(decoded, codec)) return;
3559
3727
  const call = pending.get(decoded.callId);
3560
3728
  if (!call) return;
3561
- if (decoded.connectionId !== call.connectionId || decoded.providerInstanceId !== call.providerInstanceId) return;
3729
+ if (decoded.serviceInstanceId !== call.serviceInstanceId) return;
3562
3730
  pending.delete(decoded.callId);
3563
3731
  call.removeAbort();
3732
+ call.disposeDeadline();
3733
+ if (decoded.protocolVersion !== codec.protocolVersion) {
3734
+ call.reject(new RemoteServiceError("protocol_mismatch", "Remote service protocol version mismatch"));
3735
+ return;
3736
+ }
3564
3737
  if (decoded.type === codec.type("error")) {
3565
3738
  call.reject(errorFromWire(decoded.error));
3566
3739
  } else {
3567
3740
  call.resolve(decoded.result);
3568
3741
  }
3569
3742
  };
3570
- options.port.addEventListener("message", onMessage);
3743
+ const onMessageError = () => rejectPending(new RemoteServiceError("transport_unavailable", "Remote service message could not be decoded"), false);
3744
+ const removeMessage = addMessageListener(options.port, onMessage);
3745
+ options.port.addEventListener("messageerror", onMessageError);
3571
3746
  options.port.start();
3572
3747
  const dispose = () => {
3573
3748
  if (disposed) return;
3574
3749
  disposed = true;
3575
- options.port.removeEventListener("message", onMessage);
3576
- for (const [callId, call] of pending) {
3577
- pending.delete(callId);
3578
- call.removeAbort();
3579
- call.reject(new RemoteServiceUnavailableError("Remote service transport disposed"));
3750
+ removeMessage();
3751
+ options.port.removeEventListener("messageerror", onMessageError);
3752
+ rejectPending(new RemoteServiceError("transport_unavailable", "Remote service transport disposed"), true);
3753
+ if (options.closeOnDispose) {
3754
+ try {
3755
+ options.port.close();
3756
+ } catch {
3757
+ }
3580
3758
  }
3581
- if (options.closeOnDispose) options.port.close();
3582
3759
  };
3583
3760
  const transport = {
3584
3761
  call(request, context) {
3585
- if (disposed) return Promise.reject(new RemoteServiceUnavailableError("Remote service transport disposed"));
3762
+ if (disposed) return Promise.reject(new RemoteServiceError("transport_unavailable", "Remote service transport disposed"));
3586
3763
  if (context.signal.aborted) return Promise.reject(abortReason(context.signal));
3587
3764
  const callId = makeCallId(transportId);
3588
- const providerInstanceId = context.reference.providerInstanceId;
3765
+ const serviceInstanceId = context.reference.serviceInstanceId;
3766
+ const timeoutMs = finiteTimeout(context.timeoutMs ?? defaultCallTimeoutMs);
3767
+ const deadlineAt = context.deadlineAt ?? Date.now() + timeoutMs;
3768
+ const timeoutController = new AbortController();
3769
+ const remaining = Math.max(0, deadlineAt - Date.now());
3770
+ const deadlineTimer = setTimeout(() => {
3771
+ try {
3772
+ timeoutController.abort(new RemoteServiceError("call_timeout", "Remote service call timed out"));
3773
+ } catch {
3774
+ timeoutController.abort();
3775
+ }
3776
+ }, remaining);
3777
+ const merged = (() => {
3778
+ const controller = new AbortController();
3779
+ const signals = [context.signal, timeoutController.signal];
3780
+ const listeners = signals.map((signal) => {
3781
+ const listener = () => {
3782
+ try {
3783
+ controller.abort(signal.reason);
3784
+ } catch {
3785
+ controller.abort();
3786
+ }
3787
+ };
3788
+ signal.addEventListener("abort", listener, { once: true });
3789
+ return { signal, listener };
3790
+ });
3791
+ return {
3792
+ signal: controller.signal,
3793
+ dispose: () => listeners.forEach(({ signal, listener }) => signal.removeEventListener("abort", listener))
3794
+ };
3795
+ })();
3589
3796
  return new Promise((resolve, reject) => {
3590
3797
  let settled = false;
3591
3798
  const finish = (callback) => {
@@ -3593,46 +3800,52 @@ function createMessagePortServiceTransport(options) {
3593
3800
  settled = true;
3594
3801
  callback();
3595
3802
  };
3803
+ const sendCancel = () => postBestEffort(options.port, codec.encode({
3804
+ type: codec.type("cancel"),
3805
+ protocolVersion: codec.protocolVersion,
3806
+ callId,
3807
+ serviceInstanceId
3808
+ }));
3596
3809
  const onAbort = () => {
3597
3810
  if (!pending.delete(callId)) return;
3598
- finish(() => reject(abortReason(context.signal)));
3599
- try {
3600
- const cancel = {
3601
- type: codec.type("cancel"),
3602
- callId,
3603
- connectionId: context.connectionId,
3604
- providerInstanceId
3605
- };
3606
- options.port.postMessage(codec.encode(cancel));
3607
- } catch {
3608
- }
3811
+ sendCancel();
3812
+ const reason = merged.signal.reason instanceof RemoteServiceError ? merged.signal.reason : merged.signal.reason?.code === "call_timeout" ? merged.signal.reason : abortReason(merged.signal);
3813
+ finish(() => reject(reason));
3814
+ merged.dispose();
3815
+ clearTimeout(deadlineTimer);
3609
3816
  };
3610
3817
  const pendingCall = {
3611
- connectionId: context.connectionId,
3612
- providerInstanceId,
3818
+ protocolVersion: codec.protocolVersion,
3819
+ serviceInstanceId,
3613
3820
  resolve: (value) => finish(() => resolve(value)),
3614
3821
  reject: (error) => finish(() => reject(error)),
3615
- removeAbort: () => context.signal.removeEventListener("abort", onAbort)
3822
+ removeAbort: () => merged.signal.removeEventListener("abort", onAbort),
3823
+ disposeDeadline: () => {
3824
+ merged.dispose();
3825
+ clearTimeout(deadlineTimer);
3826
+ }
3616
3827
  };
3617
3828
  pending.set(callId, pendingCall);
3618
- context.signal.addEventListener("abort", onAbort, { once: true });
3829
+ merged.signal.addEventListener("abort", onAbort, { once: true });
3830
+ const message = {
3831
+ type: codec.type("call"),
3832
+ protocolVersion: codec.protocolVersion,
3833
+ callId,
3834
+ capabilityId: context.reference.capabilityId,
3835
+ contractVersion: context.reference.contractVersion,
3836
+ serviceInstanceId,
3837
+ ...context.operationId ? { operationId: context.operationId } : {},
3838
+ ...context.grantId ?? context.reference.grantId ? { grantId: context.grantId ?? context.reference.grantId } : {},
3839
+ request
3840
+ };
3619
3841
  try {
3620
- const message = {
3621
- type: codec.type("call"),
3622
- callId,
3623
- connectionId: context.connectionId,
3624
- providerInstanceId,
3625
- ...context.operationId ? { operationId: context.operationId } : {},
3626
- ...context.grantId ?? context.reference.grantId ? { grantId: context.grantId ?? context.reference.grantId } : {},
3627
- reference: context.reference,
3628
- request
3629
- };
3630
3842
  const transfer = options.transferForRequest?.(request, context) ?? [];
3631
- options.port.postMessage(codec.encode(message), [...transfer]);
3632
- if (context.signal.aborted) onAbort();
3843
+ postRequest(options.port, codec.encode(message), transfer);
3844
+ if (merged.signal.aborted) onAbort();
3633
3845
  } catch (error) {
3634
3846
  pending.delete(callId);
3635
3847
  pendingCall.removeAbort();
3848
+ pendingCall.disposeDeadline();
3636
3849
  pendingCall.reject(error);
3637
3850
  }
3638
3851
  });
@@ -3641,6 +3854,252 @@ function createMessagePortServiceTransport(options) {
3641
3854
  };
3642
3855
  return transport;
3643
3856
  }
3857
+ function ensureTimeout2(value) {
3858
+ const timeout = value ?? 3e4;
3859
+ if (!Number.isFinite(timeout) || timeout <= 0) {
3860
+ throw new TypeError("defaultCallTimeoutMs must be a finite number greater than zero");
3861
+ }
3862
+ return timeout;
3863
+ }
3864
+ function addMessageListener2(port, listener) {
3865
+ port.addEventListener("message", listener);
3866
+ return () => port.removeEventListener("message", listener);
3867
+ }
3868
+ function ensureMessageEventTarget(port) {
3869
+ const target = port;
3870
+ if (target.addEventListener && target.removeEventListener) return;
3871
+ const listeners = /* @__PURE__ */ new Set();
3872
+ const original = target.onmessage;
3873
+ target.addEventListener = function add(type, listener) {
3874
+ if (type === "message") listeners.add(listener);
3875
+ };
3876
+ target.removeEventListener = function remove(type, listener) {
3877
+ if (type === "message") listeners.delete(listener);
3878
+ };
3879
+ target.onmessage = (event) => {
3880
+ original?.call(port, event);
3881
+ for (const listener of [...listeners]) listener(event);
3882
+ };
3883
+ }
3884
+ function makeWorker(options) {
3885
+ const workerOptions = {
3886
+ type: "module",
3887
+ ...options.name ? { name: options.name } : {},
3888
+ ...options.credentials ? { credentials: options.credentials } : {}
3889
+ };
3890
+ if (options.workerFactory) return options.workerFactory(options.url, workerOptions);
3891
+ const WorkerConstructor = globalThis.SharedWorker;
3892
+ if (!WorkerConstructor) throw new RuntimeUnavailableError("SharedWorker is not supported by this browser");
3893
+ return new WorkerConstructor(options.url, workerOptions);
3894
+ }
3895
+ function connectSharedWorkerInternal(options) {
3896
+ if (!options || typeof options.id !== "string" || options.id.trim() === "") {
3897
+ throw new Error("SharedWorker runtime id must be a non-empty string");
3898
+ }
3899
+ const defaultCallTimeoutMs = ensureTimeout2(options.defaultCallTimeoutMs);
3900
+ const worker = makeWorker(options);
3901
+ const port = worker.port;
3902
+ if (!port) throw new RuntimeUnavailableError("SharedWorker did not expose a MessagePort");
3903
+ ensureMessageEventTarget(port);
3904
+ const listeners = /* @__PURE__ */ new Set();
3905
+ const codec = createRuntimeMessageCodec();
3906
+ let disposed = false;
3907
+ let runtimeInstanceId;
3908
+ let removeRuntimeMessage;
3909
+ let transport;
3910
+ let restoreWorkerError = () => void 0;
3911
+ let currentSnapshot = {
3912
+ runtimeId: options.id,
3913
+ runtimeKind: "shared-worker",
3914
+ runtimeInstanceId: "",
3915
+ state: "starting",
3916
+ revision: 0,
3917
+ units: [],
3918
+ services: []
3919
+ };
3920
+ const emit = (next) => {
3921
+ currentSnapshot = Object.freeze({
3922
+ ...next,
3923
+ units: Object.freeze([...next.units]),
3924
+ services: Object.freeze([...next.services])
3925
+ });
3926
+ for (const listener of [...listeners]) {
3927
+ try {
3928
+ listener(currentSnapshot);
3929
+ } catch {
3930
+ }
3931
+ }
3932
+ };
3933
+ const bridgeTransport = {
3934
+ call(request, context) {
3935
+ if (!transport) {
3936
+ return Promise.reject(new RuntimeUnavailableError("SharedWorker connection is unavailable"));
3937
+ }
3938
+ return transport.call(request, context);
3939
+ }
3940
+ };
3941
+ const bridge = createServiceBridge({
3942
+ protocolVersion: RUNTIME_PROTOCOL_VERSION,
3943
+ transport: bridgeTransport,
3944
+ defaultCallTimeoutMs
3945
+ });
3946
+ const cleanup = () => {
3947
+ removeRuntimeMessage?.();
3948
+ removeRuntimeMessage = void 0;
3949
+ port.removeEventListener("messageerror", onWorkerError);
3950
+ transport?.dispose();
3951
+ transport = void 0;
3952
+ try {
3953
+ port.close();
3954
+ } catch {
3955
+ }
3956
+ if (worker.removeEventListener) worker.removeEventListener("error", onWorkerError);
3957
+ restoreWorkerError();
3958
+ restoreWorkerError = () => void 0;
3959
+ };
3960
+ const disconnect = (reason, failure) => {
3961
+ if (disposed) return;
3962
+ cleanup();
3963
+ bridge.disconnect(reason);
3964
+ emit({
3965
+ ...currentSnapshot,
3966
+ state: "disconnected",
3967
+ runtimeInstanceId: "",
3968
+ revision: 0,
3969
+ units: [],
3970
+ services: [],
3971
+ error: reason
3972
+ });
3973
+ runtimeInstanceId = void 0;
3974
+ };
3975
+ const onRuntimeError = (message) => {
3976
+ const error = new RuntimeInitializationError({
3977
+ pluginId: message.pluginId,
3978
+ unitId: message.unitId,
3979
+ phase: message.phase === "snapshot" ? "snapshot" : "startup",
3980
+ error: message.message
3981
+ });
3982
+ if (message.code === "protocol_mismatch") bridge.markProtocolMismatch(message.message);
3983
+ else if (message.code === "runtime_initialization_failed") bridge.markInitializationFailed(message.message);
3984
+ else bridge.disconnect(message.message);
3985
+ emit({ ...currentSnapshot, state: "failed", error: error.message, units: [], services: [] });
3986
+ };
3987
+ const onSnapshot = (snapshot) => {
3988
+ if (snapshot.runtimeId !== options.id || snapshot.runtimeKind !== "shared-worker") return;
3989
+ if (!isRuntimeSnapshotProtocol(snapshot)) {
3990
+ bridge.markProtocolMismatch(`Runtime protocol ${snapshot.protocolVersion} is not supported`);
3991
+ emit({
3992
+ ...currentSnapshot,
3993
+ state: "failed",
3994
+ error: `Runtime protocol ${snapshot.protocolVersion} is not supported`,
3995
+ units: [],
3996
+ services: []
3997
+ });
3998
+ return;
3999
+ }
4000
+ const applied = bridge.applySnapshot(snapshot);
4001
+ if (!applied.accepted) return;
4002
+ runtimeInstanceId = snapshot.runtimeInstanceId;
4003
+ emit({
4004
+ runtimeId: snapshot.runtimeId,
4005
+ runtimeKind: snapshot.runtimeKind,
4006
+ runtimeInstanceId: snapshot.runtimeInstanceId,
4007
+ state: snapshot.state === "ready" ? "ready" : snapshot.state,
4008
+ revision: snapshot.revision,
4009
+ units: snapshot.units,
4010
+ services: snapshot.services
4011
+ });
4012
+ };
4013
+ const onWorkerError = (event) => {
4014
+ const detail = event.type ? `SharedWorker error: ${event.type}` : "SharedWorker error";
4015
+ disconnect(detail);
4016
+ };
4017
+ const onMessage = (event) => {
4018
+ if (disposed) return;
4019
+ if (isRuntimeError(event.data)) {
4020
+ onRuntimeError(event.data);
4021
+ return;
4022
+ }
4023
+ if (isRuntimeSnapshot(event.data)) {
4024
+ onSnapshot(event.data);
4025
+ return;
4026
+ }
4027
+ const decoded = codec.decode(event.data);
4028
+ if (decoded?.type === codec.type("error")) ;
4029
+ };
4030
+ try {
4031
+ options.onConnection?.({ worker, port });
4032
+ transport = createMessagePortServiceTransport({
4033
+ port,
4034
+ codec,
4035
+ defaultCallTimeoutMs,
4036
+ closeOnDispose: false
4037
+ });
4038
+ removeRuntimeMessage = addMessageListener2(port, onMessage);
4039
+ port.addEventListener("messageerror", onWorkerError);
4040
+ if (worker.addEventListener) worker.addEventListener("error", onWorkerError);
4041
+ else {
4042
+ const previous = worker.onerror;
4043
+ const fallbackHandler = (event) => {
4044
+ previous?.(event);
4045
+ onWorkerError(event);
4046
+ };
4047
+ worker.onerror = fallbackHandler;
4048
+ restoreWorkerError = () => {
4049
+ if (worker.onerror === fallbackHandler) worker.onerror = previous;
4050
+ };
4051
+ }
4052
+ port.start();
4053
+ } catch (error) {
4054
+ cleanup();
4055
+ throw error;
4056
+ }
4057
+ const handle = {
4058
+ runtimeKind: "shared-worker",
4059
+ runtimeId: options.id,
4060
+ get runtimeInstanceId() {
4061
+ return runtimeInstanceId;
4062
+ },
4063
+ serviceBridge: bridge,
4064
+ state: () => currentSnapshot,
4065
+ capability(capabilityId, capabilityOptions = {}) {
4066
+ const proxy = bridge.requireProxy({
4067
+ capabilityId,
4068
+ contractVersion: capabilityOptions.contractVersion ?? `${capabilityId}.v1`
4069
+ });
4070
+ return proxy;
4071
+ },
4072
+ subscribe(listener) {
4073
+ listeners.add(listener);
4074
+ listener(currentSnapshot);
4075
+ return () => listeners.delete(listener);
4076
+ },
4077
+ dispose(reason = "SharedWorker connection disposed") {
4078
+ if (disposed) return Promise.resolve();
4079
+ disposed = true;
4080
+ emit({ ...currentSnapshot, state: "stopping" });
4081
+ bridge.dispose(reason);
4082
+ cleanup();
4083
+ runtimeInstanceId = void 0;
4084
+ emit({
4085
+ ...currentSnapshot,
4086
+ state: "disposed",
4087
+ runtimeInstanceId: "",
4088
+ revision: 0,
4089
+ units: [],
4090
+ services: []
4091
+ });
4092
+ return Promise.resolve();
4093
+ }
4094
+ };
4095
+ return handle;
4096
+ }
4097
+ function connectSharedWorker(options) {
4098
+ return connectSharedWorkerInternal(options);
4099
+ }
4100
+ function connectSharedWorkerForTesting(options, workerFactory) {
4101
+ return connectSharedWorkerInternal({ ...options, workerFactory });
4102
+ }
3644
4103
 
3645
4104
  // src/lifecycle/pluginIntentController.ts
3646
4105
  function makeAuthorityInstanceId() {
@@ -3797,6 +4256,6 @@ function createPluginIntentController(options = {}) {
3797
4256
  return controller;
3798
4257
  }
3799
4258
 
3800
- export { LIFECYCLE_ERROR_TEXT, LifecycleScopeRevokedError, PermissionDeniedError, PermissionLeaseRevokedError, PluginGraphValidationError, RESOURCE_OWNER, RESOURCE_REGISTRY_CAPABILITY, RUNTIME_MESSAGE_BUS, RemoteServiceUnavailableError, SCOPED_TASK_SCHEDULER_CAPABILITY, StartupCapabilityError, StartupPluginError, UpgradeGateRejectedError, buildPluginGraph, createCapabilityRegistry, createInMemoryPluginConfigStore, createLifecycleScope, createMessageBus, createMessagePortServiceTransport, createPermissionLease, createPluginHost, createPluginIntentController, createRemoteServiceBridge, createRemoteServiceMessageCodec, createResourceRegistry, createResourceScope, createResourceStore, createRuntimeUnitImplementationRegistry, createScopedMessageBus, createScopedTaskScheduler, createServiceBridge, dependenciesOfManifest, lifecycleErrorText, providesOfManifest, registerOwnedResource, reverseDependentsOf, validatePluginGraph, validateRuntimeUnitDependencyContracts };
3801
- //# sourceMappingURL=chunk-URY3E6UH.js.map
3802
- //# sourceMappingURL=chunk-URY3E6UH.js.map
4259
+ export { LIFECYCLE_ERROR_TEXT, LifecycleScopeRevokedError, PermissionDeniedError, PermissionLeaseRevokedError, PluginGraphValidationError, RESOURCE_OWNER, RESOURCE_REGISTRY_CAPABILITY, RUNTIME_ERROR_TYPE, RUNTIME_MESSAGE_BUS, RUNTIME_PROTOCOL_VERSION, RUNTIME_SNAPSHOT_TYPE, RemoteServiceError, RuntimeInitializationError, RuntimeUnavailableError, SCOPED_TASK_SCHEDULER_CAPABILITY, StartupCapabilityError, StartupPluginError, UpgradeGateRejectedError, buildPluginGraph, connectSharedWorker, connectSharedWorkerForTesting, createCapabilityRegistry, createInMemoryPluginConfigStore, createLifecycleScope, createMessageBus, createMessagePortServiceTransport, createPermissionLease, createPluginHost, createPluginIntentController, createRemoteServiceMessageCodec, createResourceRegistry, createResourceScope, createResourceStore, createRuntimeMessageCodec, createRuntimeUnitImplementationRegistry, createScopedMessageBus, createScopedTaskScheduler, createServiceBridge, dependenciesOfManifest, isRuntimeError, isRuntimeSnapshot, isRuntimeSnapshotProtocol, lifecycleErrorText, providesOfManifest, registerOwnedResource, reverseDependentsOf, unitSnapshotFromState, validatePluginGraph, validateRuntimeUnitDependencyContracts };
4260
+ //# sourceMappingURL=chunk-76BGPI6M.js.map
4261
+ //# sourceMappingURL=chunk-76BGPI6M.js.map