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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createRemoteServiceMessageCodec, createRuntimeUnitImplementationRegistry, createPluginHost, StartupPluginError, createServiceBridge, createMessagePortServiceTransport, UpgradeGateRejectedError } from './chunk-URY3E6UH.js';
2
- 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 } from './chunk-URY3E6UH.js';
1
+ import { RuntimeInitializationError, createRuntimeUnitImplementationRegistry, createPluginHost, StartupPluginError, createRemoteServiceMessageCodec, createRuntimeMessageCodec, RemoteServiceError, UpgradeGateRejectedError, unitSnapshotFromState, RuntimeUnavailableError, RUNTIME_PROTOCOL_VERSION, RUNTIME_SNAPSHOT_TYPE, RUNTIME_ERROR_TYPE } from './chunk-76BGPI6M.js';
2
+ 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, 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 } from './chunk-76BGPI6M.js';
3
3
 
4
4
  // src/contracts/plugin.ts
5
5
  function runtimeCapabilityContractVersion(capability) {
@@ -156,90 +156,6 @@ function materializePluginDefinitions(inputs, runtime) {
156
156
  return inputs.map((input) => materializePluginDefinition(input, runtime));
157
157
  }
158
158
 
159
- // src/runtime/runtimeTypes.ts
160
- var RuntimeInitializationError = class extends Error {
161
- code = "runtime.initialization_failed";
162
- details;
163
- constructor(details) {
164
- super(
165
- `Runtime initialization failed${details.pluginId ? ` for ${details.pluginId}` : ""} during ${details.phase}: ${details.error}`
166
- );
167
- this.name = "RuntimeInitializationError";
168
- this.details = details;
169
- }
170
- };
171
- var RuntimeUnavailableError = class extends Error {
172
- code = "runtime.unavailable";
173
- constructor(message = "Runtime is unavailable") {
174
- super(message);
175
- this.name = "RuntimeUnavailableError";
176
- }
177
- };
178
-
179
- // src/runtime/runtimeProtocol.ts
180
- var RUNTIME_PROTOCOL_VERSION = "webloom.runtime.v1";
181
- var RUNTIME_HELLO_TYPE = "webloom.runtime.hello";
182
- var RUNTIME_SNAPSHOT_TYPE = "webloom.runtime.snapshot";
183
- var RUNTIME_RESYNC_TYPE = "webloom.runtime.resync";
184
- var RUNTIME_ERROR_TYPE = "webloom.runtime.error";
185
- function isRecord(input) {
186
- return typeof input === "object" && input !== null;
187
- }
188
- function isNonEmptyString(input) {
189
- return typeof input === "string" && input.length > 0;
190
- }
191
- function isRuntimeKind(input) {
192
- return input === "window-main" || input === "shared-worker";
193
- }
194
- function isPluginStateKind(input) {
195
- return input === "registered" || input === "starting" || input === "stopping" || input === "enabled" || input === "disabled" || input === "blocked" || input === "error-disabled" || input === "cleanup-pending" || input === "unknown";
196
- }
197
- function isRemoteServiceReference(input) {
198
- if (!isRecord(input)) return false;
199
- const reference = input;
200
- return isNonEmptyString(reference.capabilityId) && isNonEmptyString(reference.providerInstanceId) && isRuntimeKind(reference.runtime) && isNonEmptyString(reference.contractVersion) && isNonEmptyString(reference.authorityInstanceId) && isNonEmptyString(reference.scopeId) && Number.isSafeInteger(reference.handoverGeneration) && reference.handoverGeneration >= 0 && isRecord(reference.attributes) && !Array.isArray(reference.attributes) && (reference.status === "starting" || reference.status === "ready" || reference.status === "unavailable" || reference.status === "failed") && Number.isSafeInteger(reference.snapshotRevision) && reference.snapshotRevision >= 0 && (reference.connectionId === void 0 || isNonEmptyString(reference.connectionId)) && (reference.grantId === void 0 || isNonEmptyString(reference.grantId)) && (reference.authorizationRevision === void 0 || Number.isSafeInteger(reference.authorizationRevision) && reference.authorizationRevision >= 0);
201
- }
202
- function isRuntimeSnapshotUnit(input) {
203
- if (!isRecord(input)) return false;
204
- const unit = input;
205
- return isNonEmptyString(unit.pluginId) && isNonEmptyString(unit.unitId) && isRuntimeKind(unit.runtime) && isPluginStateKind(unit.state) && (unit.instanceId === void 0 || isNonEmptyString(unit.instanceId));
206
- }
207
- function createRuntimeMessageCodec() {
208
- return createRemoteServiceMessageCodec({
209
- prefix: "webloom.runtime",
210
- protocolVersion: RUNTIME_PROTOCOL_VERSION
211
- });
212
- }
213
- function isRuntimeHello(input) {
214
- if (!isRecord(input)) return false;
215
- const message = input;
216
- return message.type === RUNTIME_HELLO_TYPE && isNonEmptyString(message.protocolVersion) && isNonEmptyString(message.connectionId) && isNonEmptyString(message.runtimeId);
217
- }
218
- function isRuntimeResync(input) {
219
- if (!isRecord(input)) return false;
220
- const message = input;
221
- return message.type === RUNTIME_RESYNC_TYPE && isNonEmptyString(message.connectionId);
222
- }
223
- function isRuntimeSnapshot(input) {
224
- if (!isRecord(input)) return false;
225
- const message = input;
226
- return message.type === RUNTIME_SNAPSHOT_TYPE && message.protocolVersion === RUNTIME_PROTOCOL_VERSION && isNonEmptyString(message.runtimeId) && isRuntimeKind(message.runtimeKind) && isNonEmptyString(message.runtimeInstanceId) && isNonEmptyString(message.connectionId) && Number.isSafeInteger(message.snapshotRevision) && message.snapshotRevision >= 0 && typeof message.baseline === "boolean" && 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");
227
- }
228
- function isRuntimeError(input) {
229
- if (!isRecord(input)) return false;
230
- const message = input;
231
- return message.type === RUNTIME_ERROR_TYPE && isNonEmptyString(message.protocolVersion) && (message.code === "runtime.protocol_mismatch" || message.code === "runtime.initialization_failed" || message.code === "runtime.invalid_connection") && isNonEmptyString(message.message) && (message.pluginId === void 0 || isNonEmptyString(message.pluginId)) && (message.unitId === void 0 || isNonEmptyString(message.unitId)) && (message.phase === void 0 || isNonEmptyString(message.phase));
232
- }
233
- function unitSnapshotFromState(state, runtime) {
234
- return {
235
- pluginId: state.pluginId,
236
- unitId: state.unitId,
237
- runtime,
238
- ...state.instanceId !== void 0 ? { instanceId: state.instanceId } : {},
239
- state: state.kind
240
- };
241
- }
242
-
243
159
  // src/runtime/windowRuntime.ts
244
160
  function makeRuntimeInstanceId(runtimeId) {
245
161
  try {
@@ -256,25 +172,21 @@ function errorMessage(error) {
256
172
  function isStartupPluginError(error) {
257
173
  return error instanceof StartupPluginError && typeof error.details?.pluginId === "string";
258
174
  }
259
- function buildLocalServices(host, manifests, runtimeInstanceId, revision) {
175
+ function buildLocalServices(host, manifests, runtimeInstanceId) {
260
176
  const services = [];
261
177
  for (const manifest of manifests) {
262
178
  const state = host.state(manifest.id);
263
179
  const unit = manifest.units?.find((candidate) => candidate.id === state.unitId) ?? manifest.units?.[0];
264
180
  if (!unit || unit.runtime === void 0 || !state.instanceId || state.kind !== "enabled") continue;
265
- const scopeId = host.scope(manifest.id)?.identity.scopeId ?? `scope:${state.instanceId}`;
266
181
  for (const capability of unit.provides ?? []) {
267
182
  services.push({
268
183
  capabilityId: capability,
269
- providerInstanceId: state.instanceId,
270
- runtime: unit.runtime,
271
184
  contractVersion: unit.providedContracts?.[capability] ?? `${capability}.v1`,
272
- authorityInstanceId: runtimeInstanceId,
273
- scopeId,
274
- handoverGeneration: 0,
275
- attributes: Object.freeze({}),
185
+ runtime: unit.runtime,
186
+ runtimeInstanceId,
187
+ serviceInstanceId: state.instanceId,
276
188
  status: "ready",
277
- snapshotRevision: revision
189
+ attributes: Object.freeze({})
278
190
  });
279
191
  }
280
192
  }
@@ -289,7 +201,7 @@ async function createWindowApp(options) {
289
201
  runtimeKind: "window-main",
290
202
  runtimeInstanceId,
291
203
  state: "starting",
292
- snapshotRevision: 0,
204
+ revision: 0,
293
205
  units: [],
294
206
  services: []
295
207
  };
@@ -332,6 +244,7 @@ async function createWindowApp(options) {
332
244
  }
333
245
  let host;
334
246
  const suppliedHost = options.host;
247
+ const remoteServiceBridge = remoteRuntime ? remoteRuntime.serviceBridge : void 0;
335
248
  try {
336
249
  const {
337
250
  id: _id,
@@ -353,7 +266,7 @@ async function createWindowApp(options) {
353
266
  ...unit.instanceId !== void 0 ? { instanceId: unit.instanceId } : {},
354
267
  state: unit.state
355
268
  })) : hostOptions.runtimeSnapshots?.() ?? [],
356
- serviceBridgeForPlugin: (pluginId, instanceId) => suppliedBridgeFactory?.(pluginId, instanceId) ?? remoteRuntime?.serviceBridge,
269
+ serviceBridgeForPlugin: (pluginId, instanceId) => suppliedBridgeFactory?.(pluginId, instanceId) ?? remoteServiceBridge,
357
270
  rootAttributes: {
358
271
  ...hostOptions.rootAttributes ?? {},
359
272
  runtimeId,
@@ -378,13 +291,13 @@ async function createWindowApp(options) {
378
291
  unit.runtime
379
292
  ));
380
293
  });
381
- const revision = Math.max(currentState.snapshotRevision + 1, host.version());
294
+ const revision = Math.max(currentState.revision + 1, host.version());
382
295
  emit({
383
296
  ...currentState,
384
- snapshotRevision: revision,
297
+ revision,
385
298
  units,
386
299
  services: [
387
- ...buildLocalServices(host, snapshotManifests, runtimeInstanceId, revision),
300
+ ...buildLocalServices(host, snapshotManifests, runtimeInstanceId),
388
301
  ...remoteRuntime?.state().services ?? []
389
302
  ]
390
303
  });
@@ -470,22 +383,46 @@ async function createWindowApp(options) {
470
383
  function isCallMessage(input, codec) {
471
384
  if (!input || typeof input !== "object") return false;
472
385
  const message = input;
473
- return message.type === codec.type("call") && typeof message.callId === "string" && message.callId.length > 0 && typeof message.connectionId === "string" && typeof message.providerInstanceId === "string" && Boolean(message.reference);
386
+ return message.type === codec.type("call") && typeof message.protocolVersion === "string" && typeof message.callId === "string" && message.callId.length > 0 && typeof message.capabilityId === "string" && typeof message.contractVersion === "string" && typeof message.serviceInstanceId === "string";
474
387
  }
475
388
  function isCancelMessage(input, codec) {
476
389
  if (!input || typeof input !== "object") return false;
477
390
  const message = input;
478
- return message.type === codec.type("cancel") && typeof message.callId === "string" && typeof message.connectionId === "string" && typeof message.providerInstanceId === "string";
391
+ return message.type === codec.type("cancel") && typeof message.protocolVersion === "string" && typeof message.callId === "string" && typeof message.serviceInstanceId === "string";
479
392
  }
480
393
  function errorMessage2(error) {
481
394
  const candidate = error && typeof error === "object" ? error : void 0;
395
+ const code = typeof candidate?.code === "string" && candidate.code.length > 0 ? candidate.code : "handler_failed";
482
396
  return {
483
397
  ...typeof candidate?.name === "string" ? { name: candidate.name } : {},
484
398
  message: typeof candidate?.message === "string" ? candidate.message : String(error),
485
- ...typeof candidate?.code === "string" ? { code: candidate.code } : {}
399
+ code,
400
+ ...candidate?.details && typeof candidate.details === "object" && !Array.isArray(candidate.details) ? { details: candidate.details } : {}
486
401
  };
487
402
  }
488
- function post(port, message) {
403
+ function stableValue(value) {
404
+ if (Array.isArray(value)) return value.map(stableValue);
405
+ if (!value || typeof value !== "object") return value;
406
+ return Object.fromEntries(
407
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)])
408
+ );
409
+ }
410
+ function bindingKey(reference) {
411
+ return [
412
+ reference.capabilityId,
413
+ reference.contractVersion,
414
+ reference.runtime,
415
+ reference.runtimeInstanceId,
416
+ reference.serviceInstanceId
417
+ ].join("\0");
418
+ }
419
+ function referenceFingerprint(reference) {
420
+ return JSON.stringify({
421
+ ...reference,
422
+ attributes: stableValue(reference.attributes)
423
+ });
424
+ }
425
+ function postBestEffort(port, message) {
489
426
  try {
490
427
  port.postMessage(message);
491
428
  } catch {
@@ -494,114 +431,151 @@ function post(port, message) {
494
431
  function createMessagePortServiceProvider(options) {
495
432
  const pending = /* @__PURE__ */ new Map();
496
433
  const codec = options.codec ?? createRemoteServiceMessageCodec();
497
- let activeProviderInstanceIds = new Set(
498
- options.snapshot.services.map((service) => service.providerInstanceId)
499
- );
434
+ let services = [...options.services?.() ?? []];
435
+ let bindings = /* @__PURE__ */ new Map();
436
+ let nextBindingEpoch = 0;
437
+ let revoked = false;
500
438
  let disposed = false;
439
+ const buildBindings = (nextServices) => {
440
+ const nextBindings = /* @__PURE__ */ new Map();
441
+ for (const reference of nextServices) {
442
+ const key = bindingKey(reference);
443
+ const previous = bindings.get(key);
444
+ const epoch = previous && referenceFingerprint(previous.reference) === referenceFingerprint(reference) ? previous.epoch : ++nextBindingEpoch;
445
+ nextBindings.set(key, { reference, epoch });
446
+ }
447
+ return nextBindings;
448
+ };
449
+ const isCurrent = (callId, call) => {
450
+ const current = bindings.get(call.bindingKey);
451
+ return !disposed && !revoked && pending.get(callId) === call && current?.epoch === call.bindingEpoch && current.reference.serviceInstanceId === call.serviceInstanceId;
452
+ };
453
+ const revokeReplacedCalls = (nextBindings) => {
454
+ for (const call of pending.values()) {
455
+ const next = nextBindings.get(call.bindingKey);
456
+ if (!next || next.epoch !== call.bindingEpoch) {
457
+ sendError(call.message, new RemoteServiceError("service_revoked", "Remote service binding was replaced"));
458
+ call.controller.abort(new RemoteServiceError("service_revoked", "Remote service binding was replaced"));
459
+ }
460
+ }
461
+ };
462
+ bindings = buildBindings(services);
501
463
  const sendError = (message, error) => {
502
464
  const response = {
503
465
  type: codec.type("error"),
466
+ protocolVersion: codec.protocolVersion,
504
467
  callId: message.callId,
505
- connectionId: message.connectionId,
506
- providerInstanceId: message.providerInstanceId,
468
+ serviceInstanceId: message.serviceInstanceId,
507
469
  error: errorMessage2(error)
508
470
  };
509
- post(options.port, codec.encode(response));
471
+ postBestEffort(options.port, codec.encode(response));
510
472
  };
511
473
  const onMessage = (event) => {
512
474
  if (disposed) return;
513
475
  const decoded = codec.decode(event.data);
514
476
  if (isCancelMessage(decoded, codec)) {
515
- if (decoded.connectionId !== options.handshake.connectionId) return;
516
- const call = pending.get(decoded.callId);
517
- if (call?.providerInstanceId === decoded.providerInstanceId) {
518
- call.controller.abort(new Error("Remote service request cancelled"));
477
+ if (decoded.protocolVersion !== codec.protocolVersion) return;
478
+ const call2 = pending.get(decoded.callId);
479
+ if (call2?.serviceInstanceId === decoded.serviceInstanceId) {
480
+ call2.controller.abort(new RemoteServiceError("request_cancelled", "Remote service request cancelled"));
519
481
  }
520
482
  return;
521
483
  }
522
484
  if (!isCallMessage(decoded, codec)) return;
523
485
  const message = decoded;
524
- if (message.connectionId !== options.handshake.connectionId) {
525
- sendError(message, Object.assign(new Error("Remote service connection mismatch"), { code: "service.connection_mismatch" }));
486
+ if (message.protocolVersion !== codec.protocolVersion) {
487
+ sendError(message, new RemoteServiceError("protocol_mismatch", "Remote service protocol version mismatch"));
488
+ return;
489
+ }
490
+ if (revoked) {
491
+ sendError(message, new RemoteServiceError("service_revoked", "Remote service Provider has been revoked"));
526
492
  return;
527
493
  }
528
- if (!activeProviderInstanceIds.has(message.providerInstanceId)) {
529
- sendError(message, Object.assign(new Error("Remote service provider mismatch"), { code: "service.provider_mismatch" }));
494
+ const reference = services.find((candidate) => candidate.status === "ready" && candidate.capabilityId === message.capabilityId && candidate.contractVersion === message.contractVersion && candidate.serviceInstanceId === message.serviceInstanceId);
495
+ if (!reference) {
496
+ sendError(message, new RemoteServiceError("service_stale", "Remote service instance is stale or unavailable"));
530
497
  return;
531
498
  }
532
499
  if (pending.has(message.callId)) {
533
- sendError(message, Object.assign(new Error("Remote service callId is duplicated"), { code: "service.duplicate_call" }));
500
+ sendError(message, new RemoteServiceError("handler_failed", "Remote service callId is duplicated"));
534
501
  return;
535
502
  }
536
503
  const controller = new AbortController();
537
- pending.set(message.callId, { controller, providerInstanceId: message.providerInstanceId });
504
+ const currentBindingKey = bindingKey(reference);
505
+ const currentBinding = bindings.get(currentBindingKey);
506
+ if (!currentBinding || currentBinding.reference !== reference) {
507
+ sendError(message, new RemoteServiceError("service_stale", "Remote service binding changed"));
508
+ return;
509
+ }
510
+ const call = {
511
+ message,
512
+ controller,
513
+ serviceInstanceId: message.serviceInstanceId,
514
+ bindingKey: currentBindingKey,
515
+ bindingEpoch: currentBinding.epoch
516
+ };
517
+ pending.set(message.callId, call);
538
518
  void (async () => {
539
519
  try {
540
- const result = await options.handleCall({ message, signal: controller.signal });
541
- if (controller.signal.aborted || disposed) return;
520
+ const result = await options.handleCall({ message, reference, signal: controller.signal });
521
+ if (controller.signal.aborted || !isCurrent(message.callId, call)) return;
542
522
  const response = {
543
523
  type: codec.type("result"),
524
+ protocolVersion: codec.protocolVersion,
544
525
  callId: message.callId,
545
- connectionId: message.connectionId,
546
- providerInstanceId: message.providerInstanceId,
526
+ serviceInstanceId: message.serviceInstanceId,
547
527
  result
548
528
  };
549
- post(options.port, codec.encode(response));
529
+ postBestEffort(options.port, codec.encode(response));
550
530
  } catch (error) {
551
- if (disposed) return;
531
+ if (controller.signal.aborted || !isCurrent(message.callId, call)) return;
552
532
  sendError(message, error);
553
533
  } finally {
554
- pending.delete(message.callId);
534
+ if (pending.get(message.callId) === call) pending.delete(message.callId);
555
535
  }
556
536
  })();
557
537
  };
558
538
  options.port.addEventListener("message", onMessage);
559
539
  options.port.start();
560
- post(options.port, codec.encode({
561
- type: codec.type("handshake"),
562
- handshake: options.handshake
563
- }));
564
- post(options.port, codec.encode({
565
- type: codec.type("snapshot"),
566
- snapshot: options.snapshot
567
- }));
568
- const dispose = () => {
569
- if (disposed) return;
570
- disposed = true;
571
- options.port.removeEventListener("message", onMessage);
572
- for (const { controller } of pending.values()) controller.abort(new Error("Remote service provider disposed"));
573
- pending.clear();
574
- if (options.closeOnDispose !== false) options.port.close();
575
- };
576
540
  const provider = {
577
- publishSnapshot(snapshot) {
541
+ setServices(nextServices) {
578
542
  if (disposed) return;
579
- activeProviderInstanceIds = new Set(
580
- snapshot.services.map((service) => service.providerInstanceId)
581
- );
582
- post(options.port, codec.encode({
583
- type: codec.type("snapshot"),
584
- snapshot
585
- }));
543
+ const next = [...nextServices];
544
+ const nextBindings = buildBindings(next);
545
+ revoked = true;
546
+ revokeReplacedCalls(nextBindings);
547
+ services = next;
548
+ bindings = nextBindings;
549
+ revoked = false;
586
550
  },
587
- invalidate(reason = "Remote service invalidated") {
551
+ revoke(reason = "Remote service Provider revoked") {
588
552
  if (disposed) return;
589
- activeProviderInstanceIds = /* @__PURE__ */ new Set();
590
- for (const { controller } of pending.values()) controller.abort(new Error(reason));
591
- post(options.port, codec.encode({
592
- type: codec.type("invalidate"),
593
- reason
594
- }));
553
+ revoked = true;
554
+ for (const call of pending.values()) {
555
+ sendError(call.message, new RemoteServiceError("service_revoked", reason));
556
+ call.controller.abort(new RemoteServiceError("service_revoked", reason));
557
+ }
558
+ services = [];
559
+ bindings = /* @__PURE__ */ new Map();
595
560
  },
596
- disconnect(reason = "Remote service disconnected") {
561
+ dispose() {
597
562
  if (disposed) return;
598
- post(options.port, codec.encode({
599
- type: codec.type("disconnect"),
600
- reason
601
- }));
602
- dispose();
603
- },
604
- dispose
563
+ disposed = true;
564
+ revoked = true;
565
+ options.port.removeEventListener("message", onMessage);
566
+ for (const { controller } of pending.values()) {
567
+ controller.abort(new RemoteServiceError("transport_unavailable", "Remote service Provider disposed"));
568
+ }
569
+ pending.clear();
570
+ services = [];
571
+ bindings = /* @__PURE__ */ new Map();
572
+ if (options.closeOnDispose !== false) {
573
+ try {
574
+ options.port.close();
575
+ } catch {
576
+ }
577
+ }
578
+ }
605
579
  };
606
580
  return provider;
607
581
  }
@@ -621,69 +595,18 @@ function errorMessage3(error) {
621
595
  }
622
596
  function startupDetails(error) {
623
597
  if (error instanceof StartupPluginError) {
624
- return {
625
- pluginId: error.details.pluginId,
626
- unitId: error.details.unitId,
627
- message: error.details.error ?? error.message
628
- };
598
+ return { pluginId: error.details.pluginId, unitId: error.details.unitId, message: error.details.error ?? error.message };
629
599
  }
630
600
  if (error instanceof RuntimeInitializationError) {
631
- return {
632
- pluginId: error.details.pluginId,
633
- unitId: error.details.unitId,
634
- message: error.message
635
- };
601
+ return { pluginId: error.details.pluginId, unitId: error.details.unitId, message: error.message };
636
602
  }
637
603
  return { message: errorMessage3(error) };
638
604
  }
639
605
  function addPortListener(port, listener) {
640
- const target = port;
641
- const add = target.addEventListener;
642
- const remove = target.removeEventListener;
643
- if (add && remove) {
644
- const before2 = target.onmessage;
645
- const probe = () => void 0;
646
- add.call(port, "message", probe);
647
- const overwritesProperty = target.onmessage === probe;
648
- remove.call(port, "message", probe);
649
- if (!overwritesProperty) {
650
- add.call(port, "message", listener);
651
- return () => remove.call(port, "message", listener);
652
- }
653
- const listeners2 = /* @__PURE__ */ new Set();
654
- const dispatch2 = (event) => {
655
- before2?.call(port, event);
656
- for (const current of [...listeners2]) current(event);
657
- };
658
- target.addEventListener = function addMessageListener2(type, current) {
659
- if (type === "message") listeners2.add(current);
660
- else add.call(port, type, current);
661
- };
662
- target.removeEventListener = function removeMessageListener(type, current) {
663
- if (type === "message") listeners2.delete(current);
664
- else remove.call(port, type, current);
665
- };
666
- target.onmessage = dispatch2;
667
- listeners2.add(listener);
668
- return () => listeners2.delete(listener);
669
- }
670
- const listeners = /* @__PURE__ */ new Set();
671
- const before = target.onmessage;
672
- const dispatch = (event) => {
673
- before?.call(port, event);
674
- for (const current of [...listeners]) current(event);
675
- };
676
- target.addEventListener = function addMessageListener2(type, current) {
677
- if (type === "message") listeners.add(current);
678
- };
679
- target.removeEventListener = function removeMessageListener(type, current) {
680
- if (type === "message") listeners.delete(current);
681
- };
682
- target.onmessage = dispatch;
683
- listeners.add(listener);
684
- return () => listeners.delete(listener);
606
+ port.addEventListener("message", listener);
607
+ return () => port.removeEventListener("message", listener);
685
608
  }
686
- function post2(port, message) {
609
+ function post(port, message) {
687
610
  try {
688
611
  port.postMessage(message);
689
612
  } catch {
@@ -710,22 +633,38 @@ function startSharedWorkerApp(options) {
710
633
  let host;
711
634
  let manifests = [];
712
635
  let disposed = false;
636
+ let acceptingConnections = true;
713
637
  let disposePromise;
714
- const emit = () => {
715
- const snapshot = currentSnapshot();
716
- for (const listener of [...listeners]) {
717
- try {
718
- listener(snapshot);
719
- } catch {
638
+ let startupError;
639
+ const serviceReferences = () => {
640
+ if (!host || runtimeState !== "ready") return [];
641
+ const services = [];
642
+ for (const manifest of manifests) {
643
+ const state = host.state(manifest.id);
644
+ const unit = manifest.units?.find((candidate) => candidate.id === state.unitId) ?? manifest.units?.[0];
645
+ if (!unit || unit.runtime === void 0 || !state.instanceId || state.kind !== "enabled") continue;
646
+ const scope = host.scope(manifest.id);
647
+ const attributes = Object.freeze({ ...scope?.identity.attributes ?? {} });
648
+ for (const capability of unit.provides ?? []) {
649
+ services.push({
650
+ capabilityId: capability,
651
+ contractVersion: unit.providedContracts?.[capability] ?? `${capability}.v1`,
652
+ runtime: unit.runtime,
653
+ runtimeInstanceId,
654
+ serviceInstanceId: state.instanceId,
655
+ status: "ready",
656
+ attributes
657
+ });
720
658
  }
721
659
  }
660
+ return services;
722
661
  };
723
662
  const currentSnapshot = () => Object.freeze({
724
663
  runtimeId,
725
664
  runtimeKind: "shared-worker",
726
665
  runtimeInstanceId,
727
666
  state: runtimeState,
728
- snapshotRevision: revision,
667
+ revision,
729
668
  units: Object.freeze(host && runtimeState !== "failed" ? manifests.flatMap((manifest) => {
730
669
  const state = host?.state(manifest.id);
731
670
  return (state?.units ?? []).map((unit) => ({
@@ -736,70 +675,42 @@ function startSharedWorkerApp(options) {
736
675
  state: unit.kind
737
676
  }));
738
677
  }) : []),
739
- services: Object.freeze(host && runtimeState !== "failed" ? serviceReferences("", revision) : [])
678
+ services: Object.freeze(serviceReferences())
740
679
  });
741
- const serviceReferences = (connectionId, snapshotRevision) => {
742
- if (!host || runtimeState === "failed" || runtimeState === "disposed") return [];
743
- const services = [];
744
- for (const manifest of manifests) {
745
- const state = host.state(manifest.id);
746
- const unit = manifest.units?.find((candidate) => candidate.id === state.unitId) ?? manifest.units?.[0];
747
- if (!unit || unit.runtime === void 0 || !state.instanceId || state.kind !== "enabled") continue;
748
- const scopeId = host.scope(manifest.id)?.identity.scopeId ?? `scope:${state.instanceId}`;
749
- for (const capability of unit.provides ?? []) {
750
- services.push({
751
- capabilityId: capability,
752
- providerInstanceId: state.instanceId,
753
- runtime: unit.runtime,
754
- contractVersion: unit.providedContracts?.[capability] ?? `${capability}.v1`,
755
- authorityInstanceId: runtimeInstanceId,
756
- scopeId,
757
- handoverGeneration: 0,
758
- attributes: Object.freeze({}),
759
- status: "ready",
760
- snapshotRevision,
761
- // The same service object is never valid through another port.
762
- ...connectionId ? { connectionId } : {}
763
- });
680
+ const emit = () => {
681
+ const snapshot = currentSnapshot();
682
+ for (const listener of [...listeners]) {
683
+ try {
684
+ listener(snapshot);
685
+ } catch {
764
686
  }
765
687
  }
766
- return services;
767
688
  };
768
- const runtimeSnapshotFor = (endpoint, baseline) => ({
689
+ const runtimeSnapshot = () => ({
769
690
  type: RUNTIME_SNAPSHOT_TYPE,
770
691
  protocolVersion: RUNTIME_PROTOCOL_VERSION,
771
692
  runtimeId,
772
693
  runtimeKind: "shared-worker",
773
694
  runtimeInstanceId,
774
- connectionId: endpoint.connectionId,
775
- snapshotRevision: revision,
776
- baseline,
695
+ revision,
777
696
  state: runtimeState,
778
697
  units: currentSnapshot().units,
779
- services: serviceReferences(endpoint.connectionId, revision)
780
- });
781
- const serviceSnapshotFor = (endpoint, baseline) => ({
782
- connectionId: endpoint.connectionId,
783
- authorityInstanceId: runtimeInstanceId,
784
- snapshotRevision: revision,
785
- baseline,
786
- services: serviceReferences(endpoint.connectionId, revision)
698
+ services: serviceReferences()
787
699
  });
788
- const publishEndpoint = (endpoint, baseline) => {
789
- if (endpoint.closed || disposed) return;
790
- const serviceSnapshot = serviceSnapshotFor(endpoint, baseline);
791
- endpoint.provider.publishSnapshot(serviceSnapshot);
792
- post2(endpoint.port, codec.encode(runtimeSnapshotFor(endpoint, baseline)));
700
+ const publishEndpoint = (endpoint) => {
701
+ if (endpoint.closed) return;
702
+ endpoint.provider.setServices(serviceReferences());
703
+ post(endpoint.port, codec.encode(runtimeSnapshot()));
793
704
  };
794
- const publishAll = (baseline = false) => {
795
- for (const endpoint of [...endpoints]) publishEndpoint(endpoint, baseline);
705
+ const publishAll = () => {
706
+ for (const endpoint of [...endpoints]) publishEndpoint(endpoint);
796
707
  };
797
- const closeEndpoint = (endpoint, reason) => {
708
+ const closeEndpoint = (endpoint, _reason) => {
798
709
  if (endpoint.closed) return;
799
710
  endpoint.closed = true;
800
711
  endpoints.delete(endpoint);
801
712
  endpoint.removeMessage();
802
- endpoint.provider.disconnect(reason);
713
+ endpoint.provider.dispose();
803
714
  };
804
715
  const sendError = (port, code, message, details = {}) => {
805
716
  const error = {
@@ -811,31 +722,34 @@ function startSharedWorkerApp(options) {
811
722
  ...details.unitId !== void 0 ? { unitId: details.unitId } : {},
812
723
  ...details.phase !== void 0 ? { phase: details.phase } : {}
813
724
  };
814
- post2(port, error);
725
+ post(port, error);
726
+ };
727
+ const isAcceptingConnections = () => acceptingConnections && runtimeState !== "stopping" && runtimeState !== "disposed" && !disposed;
728
+ const rejectLateConnection = (port) => {
729
+ post(port, codec.encode(runtimeSnapshot()));
730
+ setTimeout(() => {
731
+ try {
732
+ port.close();
733
+ } catch {
734
+ }
735
+ }, 0);
815
736
  };
816
- const handleCall = async (connectionId, input) => {
737
+ const handleCall = async (input) => {
817
738
  if (!host || runtimeState !== "ready") throw new Error("SharedWorker Runtime is not ready");
818
- const message = input.message;
819
- if (message.connectionId !== connectionId) throw new Error("Runtime connection mismatch");
820
- const reference = message.reference;
821
- if (message.providerInstanceId !== reference.providerInstanceId || reference.authorityInstanceId !== runtimeInstanceId || reference.connectionId !== connectionId || reference.runtime !== "shared-worker" || reference.status !== "ready" || reference.snapshotRevision !== revision || reference.handoverGeneration !== 0) {
739
+ const { message, reference } = input;
740
+ if (reference.runtimeInstanceId !== runtimeInstanceId || reference.runtime !== "shared-worker" || reference.status !== "ready") {
822
741
  throw new Error("Runtime service reference is stale");
823
742
  }
824
- const manifest = manifests.find((candidate) => {
825
- const state2 = host?.state(candidate.id);
826
- return state2?.instanceId === reference.providerInstanceId;
827
- });
743
+ const manifest = manifests.find((candidate) => host?.state(candidate.id).instanceId === reference.serviceInstanceId);
828
744
  if (!manifest) throw new Error("Runtime service provider is no longer active");
829
745
  const state = host.state(manifest.id);
830
- if (state.kind !== "enabled" || state.instanceId !== reference.providerInstanceId) {
746
+ if (state.kind !== "enabled" || state.instanceId !== reference.serviceInstanceId) {
831
747
  throw new Error("Runtime service provider is no longer active");
832
748
  }
833
749
  const unit = manifest.units?.find((candidate) => candidate.id === state.unitId);
834
750
  if (!unit || unit.runtime !== "shared-worker" || !unit.provides?.includes(reference.capabilityId)) {
835
751
  throw new Error("Runtime capability is not declared");
836
752
  }
837
- const expectedScopeId = host.scope(manifest.id)?.identity.scopeId ?? `scope:${state.instanceId}`;
838
- if (reference.scopeId !== expectedScopeId) throw new Error("Runtime service scope is stale");
839
753
  const expectedVersion = unit.providedContracts?.[reference.capabilityId] ?? `${reference.capabilityId}.v1`;
840
754
  if (expectedVersion !== reference.contractVersion) throw new Error("Runtime capability contract mismatch");
841
755
  const value = host.capabilities.get(reference.capabilityId);
@@ -858,104 +772,50 @@ function startSharedWorkerApp(options) {
858
772
  };
859
773
  const attachPort = (port) => {
860
774
  let endpoint;
861
- let helloSeen = false;
862
775
  let removeMessage = () => void 0;
776
+ const onMessageError = () => {
777
+ if (endpoint) closeEndpoint(endpoint);
778
+ };
779
+ endpoint = {
780
+ port,
781
+ provider: void 0,
782
+ removeMessage: () => void 0,
783
+ closed: false
784
+ };
785
+ endpoint.provider = createMessagePortServiceProvider({
786
+ port,
787
+ codec,
788
+ services: serviceReferences,
789
+ handleCall
790
+ });
791
+ endpoints.add(endpoint);
863
792
  const onMessage = (event) => {
864
- if (endpoint?.closed) return;
865
- const decoded = codec.decode(event.data);
866
- if (!helloSeen) {
867
- if (!isRuntimeHello(event.data)) return;
868
- const hello = event.data;
869
- helloSeen = true;
870
- if (hello.protocolVersion !== RUNTIME_PROTOCOL_VERSION) {
871
- sendError(port, "runtime.protocol_mismatch", "Runtime protocol version mismatch", { phase: "handshake" });
872
- try {
873
- port.close();
874
- } catch {
875
- }
876
- return;
877
- }
878
- if (hello.runtimeId !== runtimeId) {
879
- sendError(port, "runtime.invalid_connection", "Runtime id mismatch", { phase: "handshake" });
880
- try {
881
- port.close();
882
- } catch {
883
- }
884
- return;
885
- }
886
- if (runtimeState === "failed") {
887
- const detail = startupDetails(startupError);
888
- sendError(port, "runtime.initialization_failed", detail.message, {
889
- pluginId: detail.pluginId,
890
- unitId: detail.unitId,
891
- phase: "startup"
892
- });
893
- try {
894
- port.close();
895
- } catch {
896
- }
897
- return;
898
- }
899
- if (runtimeState === "stopping" || runtimeState === "disposed") {
900
- sendError(port, "runtime.invalid_connection", "SharedWorker Runtime is no longer accepting connections", { phase: "handshake" });
901
- try {
902
- port.close();
903
- } catch {
904
- }
905
- return;
906
- }
907
- if (endpointsHasConnection(hello.connectionId)) {
908
- sendError(port, "runtime.invalid_connection", "Connection id is already active", { phase: "handshake" });
909
- try {
910
- port.close();
911
- } catch {
912
- }
913
- return;
914
- }
915
- const candidate = { port, connectionId: hello.connectionId };
916
- const provider = createMessagePortServiceProvider({
917
- port,
918
- codec,
919
- handshake: {
920
- connectionId: hello.connectionId,
921
- authorityInstanceId: runtimeInstanceId,
922
- protocolVersion: RUNTIME_PROTOCOL_VERSION
923
- },
924
- snapshot: serviceSnapshotFor(candidate, true),
925
- handleCall: (input) => handleCall(hello.connectionId, input)
926
- });
927
- endpoint = { ...candidate, provider, removeMessage, closed: false };
928
- endpoints.add(endpoint);
929
- if (runtimeState === "ready") publishEndpoint(endpoint, true);
930
- return;
931
- }
932
- if (!endpoint) return;
933
- if (isRuntimeResync(event.data)) {
934
- if (event.data.connectionId === endpoint.connectionId) publishEndpoint(endpoint, true);
935
- return;
936
- }
937
- if (decoded?.type === codec.type("disconnect")) {
938
- closeEndpoint(endpoint, typeof decoded.reason === "string" ? decoded.reason : "Window disconnected");
939
- }
940
793
  };
941
794
  removeMessage = addPortListener(port, onMessage);
795
+ endpoint.removeMessage = () => {
796
+ removeMessage();
797
+ port.removeEventListener("messageerror", onMessageError);
798
+ };
799
+ port.addEventListener("messageerror", onMessageError);
942
800
  port.start();
801
+ publishEndpoint(endpoint);
943
802
  };
944
- const endpointsHasConnection = (connectionId) => [...endpoints].some((endpoint) => endpoint.connectionId === connectionId);
945
- let startupError;
946
803
  workerScope.onconnect = (event) => {
804
+ const ports = event.ports ?? [];
805
+ if (!isAcceptingConnections()) {
806
+ for (const port of ports) rejectLateConnection(port);
807
+ return;
808
+ }
947
809
  try {
948
810
  options.onPortConnect?.(event);
949
811
  } catch (error) {
950
812
  const detail = startupDetails(error);
951
- for (const port of event.ports ?? []) {
952
- sendError(port, "runtime.initialization_failed", detail.message, {
813
+ for (const port of ports) {
814
+ sendError(port, "runtime_initialization_failed", detail.message, {
953
815
  pluginId: detail.pluginId,
954
816
  unitId: detail.unitId,
955
- phase: "handshake"
817
+ phase: "startup"
956
818
  });
957
- }
958
- for (const port of event.ports ?? []) {
959
819
  try {
960
820
  port.close();
961
821
  } catch {
@@ -963,39 +823,37 @@ function startSharedWorkerApp(options) {
963
823
  }
964
824
  return;
965
825
  }
966
- for (const port of event.ports ?? []) attachPort(port);
826
+ if (!isAcceptingConnections()) {
827
+ for (const port of ports) rejectLateConnection(port);
828
+ return;
829
+ }
830
+ for (const port of ports) attachPort(port);
967
831
  };
968
832
  let materialized = [];
969
833
  let readyPromise;
970
834
  try {
971
835
  materialized = materializePluginDefinitions(options.plugins, "shared-worker");
972
836
  manifests = materialized.map((item) => item.manifest);
973
- const implementations = createRuntimeUnitImplementationRegistry(
974
- materialized.map((item) => ({
975
- pluginId: item.manifest.id,
976
- unitId: item.unitId,
977
- setup: item.setup
978
- }))
979
- );
837
+ const implementations = createRuntimeUnitImplementationRegistry(materialized.map((item) => ({
838
+ pluginId: item.manifest.id,
839
+ unitId: item.unitId,
840
+ setup: item.setup
841
+ })));
980
842
  const { id: _id, plugins: _plugins, globalScope: _scope, onPortConnect: _onPortConnect, ...hostOptions } = options;
981
843
  host = createPluginHost({
982
844
  ...hostOptions,
983
845
  runtime: "shared-worker",
984
- rootAttributes: {
985
- ...hostOptions.rootAttributes ?? {},
986
- runtimeId,
987
- runtimeInstanceId
988
- },
846
+ rootAttributes: { ...hostOptions.rootAttributes ?? {}, runtimeId, runtimeInstanceId },
989
847
  runtimeUnitImplementationRegistry: implementations
990
848
  });
991
849
  host.subscribe(() => {
992
850
  if (runtimeState !== "ready" || disposed) return;
993
851
  revision += 1;
994
- publishAll(false);
852
+ publishAll();
995
853
  emit();
996
854
  });
997
855
  readyPromise = host.registerAll(manifests).then(() => {
998
- if (disposed) return;
856
+ if (runtimeState === "stopping" || disposed) return;
999
857
  const runtimeHost = host;
1000
858
  if (!runtimeHost) throw new Error("SharedWorker Plugin Host is unavailable");
1001
859
  const failedRequired = manifests.find((manifest) => {
@@ -1009,20 +867,21 @@ function startSharedWorkerApp(options) {
1009
867
  unitId: state.unitId ?? failedRequired.units?.[0]?.id ?? failedRequired.id,
1010
868
  capabilities: [],
1011
869
  state: state.kind,
1012
- error: state.error ?? `Required plugin is ${state.kind}${state.blockedBy ? `: ${state.blockedBy.join(", ")}` : ""}`
870
+ error: state.error ?? `Required plugin is ${state.kind}`
1013
871
  });
1014
872
  }
1015
873
  runtimeState = "ready";
1016
874
  revision = Math.max(1, revision + 1);
1017
- publishAll(true);
875
+ publishAll();
1018
876
  emit();
1019
877
  }).catch((error) => {
878
+ if (runtimeState === "stopping" || disposed) return;
1020
879
  startupError = error;
1021
880
  runtimeState = "failed";
1022
881
  emit();
1023
882
  const detail = startupDetails(error);
1024
883
  for (const endpoint of [...endpoints]) {
1025
- sendError(endpoint.port, "runtime.initialization_failed", detail.message, {
884
+ sendError(endpoint.port, "runtime_initialization_failed", detail.message, {
1026
885
  pluginId: detail.pluginId,
1027
886
  unitId: detail.unitId,
1028
887
  phase: "startup"
@@ -1041,6 +900,7 @@ function startSharedWorkerApp(options) {
1041
900
  runtimeState = "failed";
1042
901
  emit();
1043
902
  readyPromise = Promise.reject(error instanceof RuntimeInitializationError ? error : new RuntimeInitializationError({ phase: "validate", error: errorMessage3(error) }));
903
+ void readyPromise.catch(() => void 0);
1044
904
  }
1045
905
  const app = {
1046
906
  runtimeKind: "shared-worker",
@@ -1052,7 +912,7 @@ function startSharedWorkerApp(options) {
1052
912
  await readyPromise;
1053
913
  return;
1054
914
  }
1055
- if (runtimeState === "failed" || runtimeState === "disposed" || disposed) {
915
+ if (runtimeState === "failed" || runtimeState === "stopping" || runtimeState === "disposed" || disposed) {
1056
916
  await readyPromise;
1057
917
  return;
1058
918
  }
@@ -1066,14 +926,35 @@ function startSharedWorkerApp(options) {
1066
926
  },
1067
927
  dispose(reason = "shared worker runtime disposed") {
1068
928
  if (disposePromise) return disposePromise;
1069
- disposed = true;
929
+ acceptingConnections = false;
1070
930
  runtimeState = "stopping";
931
+ revision += 1;
932
+ publishAll();
1071
933
  emit();
1072
934
  disposePromise = (async () => {
1073
- for (const endpoint of [...endpoints]) closeEndpoint(endpoint, reason);
1074
- const result = host ? await host.dispose(reason) : { scopeId: `runtime:${runtimeInstanceId}`, state: "stopped", attempted: 0, released: 0, pending: [], errors: [], cleanupIncomplete: false };
935
+ let result;
936
+ let failure;
937
+ try {
938
+ result = host ? await host.dispose(reason) : { scopeId: `runtime:${runtimeInstanceId}`, state: "stopped", attempted: 0, released: 0, pending: [], errors: [], cleanupIncomplete: false };
939
+ } catch (error) {
940
+ failure = error;
941
+ result = {
942
+ scopeId: `runtime:${runtimeInstanceId}`,
943
+ state: "stopped",
944
+ attempted: 0,
945
+ released: 0,
946
+ pending: [],
947
+ errors: [{ resourceId: "runtime.dispose", code: "lifecycle.cleanup_failed", message: errorMessage3(error) }],
948
+ cleanupIncomplete: true
949
+ };
950
+ }
1075
951
  runtimeState = "disposed";
952
+ revision += 1;
953
+ publishAll();
1076
954
  emit();
955
+ disposed = true;
956
+ for (const endpoint of [...endpoints]) closeEndpoint(endpoint);
957
+ if (failure) throw failure;
1077
958
  return result;
1078
959
  })();
1079
960
  return disposePromise;
@@ -1082,495 +963,6 @@ function startSharedWorkerApp(options) {
1082
963
  return app;
1083
964
  }
1084
965
 
1085
- // src/runtime/connectSharedWorker.ts
1086
- function makeConnectionId(runtimeId) {
1087
- try {
1088
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1089
- return `${runtimeId}:connection:${crypto.randomUUID()}`;
1090
- }
1091
- } catch {
1092
- }
1093
- return `${runtimeId}:connection:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
1094
- }
1095
- function errorMessage4(error) {
1096
- return error instanceof Error ? error.message : String(error);
1097
- }
1098
- function createDeferred() {
1099
- let resolvePromise;
1100
- let rejectPromise;
1101
- const deferred = {
1102
- promise: new Promise((resolve, reject) => {
1103
- resolvePromise = resolve;
1104
- rejectPromise = reject;
1105
- }),
1106
- settled: false,
1107
- resolve(value) {
1108
- if (deferred.settled) return;
1109
- deferred.settled = true;
1110
- resolvePromise(value);
1111
- },
1112
- reject(error) {
1113
- if (deferred.settled) return;
1114
- deferred.settled = true;
1115
- rejectPromise(error);
1116
- }
1117
- };
1118
- return deferred;
1119
- }
1120
- function post3(port, message) {
1121
- try {
1122
- port.postMessage(message);
1123
- } catch {
1124
- }
1125
- }
1126
- function addMessageListener(port, listener) {
1127
- const before = port.onmessage;
1128
- port.addEventListener("message", listener);
1129
- if (port.onmessage === listener && before && before !== listener) {
1130
- port.removeEventListener("message", listener);
1131
- const composed = (event) => {
1132
- before.call(port, event);
1133
- listener(event);
1134
- };
1135
- port.addEventListener("message", composed);
1136
- return () => port.removeEventListener("message", composed);
1137
- }
1138
- return () => port.removeEventListener("message", listener);
1139
- }
1140
- function ensureMessageEventTarget(port) {
1141
- const target = port;
1142
- const add = target.addEventListener;
1143
- const remove = target.removeEventListener;
1144
- if (add && remove) {
1145
- const before = target.onmessage;
1146
- const probe = () => void 0;
1147
- add.call(port, "message", probe);
1148
- const overwritesProperty = target.onmessage === probe;
1149
- remove.call(port, "message", probe);
1150
- if (!overwritesProperty) return;
1151
- const listeners2 = /* @__PURE__ */ new Set();
1152
- const dispatch2 = (event) => {
1153
- before?.call(port, event);
1154
- for (const listener of [...listeners2]) listener(event);
1155
- };
1156
- target.addEventListener = function addMessageListener2(type, listener) {
1157
- if (type === "message") listeners2.add(listener);
1158
- else add.call(port, type, listener);
1159
- };
1160
- target.removeEventListener = function removeMessageListener(type, listener) {
1161
- if (type === "message") listeners2.delete(listener);
1162
- else remove.call(port, type, listener);
1163
- };
1164
- target.onmessage = dispatch2;
1165
- return;
1166
- }
1167
- const listeners = /* @__PURE__ */ new Set();
1168
- const original = target.onmessage;
1169
- const dispatch = (event) => {
1170
- original?.call(port, event);
1171
- for (const listener of [...listeners]) listener(event);
1172
- };
1173
- target.addEventListener = function addMessageListener2(type, listener) {
1174
- if (type === "message") listeners.add(listener);
1175
- };
1176
- target.removeEventListener = function removeMessageListener(type, listener) {
1177
- if (type === "message") listeners.delete(listener);
1178
- };
1179
- target.onmessage = dispatch;
1180
- }
1181
- function makeWorker(options) {
1182
- const workerOptions = {
1183
- type: "module",
1184
- ...options.name ? { name: options.name } : {}
1185
- };
1186
- if (options.workerFactory) return options.workerFactory(options.url, workerOptions);
1187
- const WorkerConstructor = globalThis.SharedWorker;
1188
- if (!WorkerConstructor) throw new RuntimeUnavailableError("SharedWorker is not supported by this browser");
1189
- return new WorkerConstructor(options.url, workerOptions);
1190
- }
1191
- async function connectSharedWorker(options) {
1192
- if (!options || typeof options.id !== "string" || options.id.trim() === "") {
1193
- throw new Error("SharedWorker runtime id must be a non-empty string");
1194
- }
1195
- const codec = createRuntimeMessageCodec();
1196
- const listeners = /* @__PURE__ */ new Set();
1197
- const autoReconnect = options.autoReconnect ?? true;
1198
- const reconnectDelayMs = Math.max(0, options.reconnectDelayMs ?? 25);
1199
- const handshakeTimeoutMs = Math.max(1, options.handshakeTimeoutMs ?? 1e4);
1200
- let disposed = false;
1201
- let currentWorker;
1202
- let currentPort;
1203
- let currentTransport;
1204
- let removeMessage;
1205
- let removeWorkerError;
1206
- let handshakeTimer;
1207
- let reconnectTimer;
1208
- let connectionGeneration = 0;
1209
- let currentConnection;
1210
- let readyDeferred = createDeferred();
1211
- let initialConnection = true;
1212
- let runtimeInstanceId;
1213
- let connectionId;
1214
- let currentSnapshot = {
1215
- runtimeId: options.id,
1216
- runtimeKind: "shared-worker",
1217
- runtimeInstanceId: "",
1218
- state: "connecting",
1219
- snapshotRevision: 0,
1220
- units: [],
1221
- services: []
1222
- };
1223
- const bridgeTransport = {
1224
- call(request, context) {
1225
- if (!currentTransport) return Promise.reject(new RuntimeUnavailableError("SharedWorker connection is unavailable"));
1226
- return currentTransport.call(request, context);
1227
- }
1228
- };
1229
- const bridge = createServiceBridge({ protocolVersion: RUNTIME_PROTOCOL_VERSION, transport: bridgeTransport });
1230
- const emit = (next) => {
1231
- currentSnapshot = Object.freeze({
1232
- ...next,
1233
- units: Object.freeze([...next.units]),
1234
- services: Object.freeze([...next.services])
1235
- });
1236
- for (const listener of [...listeners]) {
1237
- try {
1238
- listener(currentSnapshot);
1239
- } catch {
1240
- }
1241
- }
1242
- };
1243
- const rejectReady = (error) => {
1244
- readyDeferred.reject(error);
1245
- void readyDeferred.promise.catch(() => void 0);
1246
- };
1247
- const beginReadinessGeneration = (reason) => {
1248
- rejectReady(new RuntimeUnavailableError(reason));
1249
- readyDeferred = createDeferred();
1250
- };
1251
- const replaceWithRejectedReadiness = (error) => {
1252
- rejectReady(error);
1253
- readyDeferred = createDeferred();
1254
- rejectReady(error);
1255
- };
1256
- const scheduleReconnect = () => {
1257
- if (disposed || !autoReconnect || reconnectTimer !== void 0) return;
1258
- reconnectTimer = setTimeout(() => {
1259
- reconnectTimer = void 0;
1260
- void openConnection().catch((error) => {
1261
- if (disposed) return;
1262
- if (currentSnapshot.state !== "disconnected") {
1263
- emit({ ...currentSnapshot, state: "disconnected", error: errorMessage4(error), units: [], services: [] });
1264
- }
1265
- rejectReady(error);
1266
- beginReadinessGeneration("SharedWorker reconnecting");
1267
- scheduleReconnect();
1268
- });
1269
- }, reconnectDelayMs);
1270
- };
1271
- const cleanupConnection = (connection) => {
1272
- if (!connection || connection.cleaned) return;
1273
- connection.cleaned = true;
1274
- const timer = connection.handshakeTimer;
1275
- const removeMessageForConnection = connection.removeMessage;
1276
- const removeWorkerErrorForConnection = connection.removeWorkerError;
1277
- const transport = connection.transport;
1278
- if (timer !== void 0) clearTimeout(timer);
1279
- connection.handshakeTimer = void 0;
1280
- removeMessageForConnection?.();
1281
- connection.removeMessage = void 0;
1282
- removeWorkerErrorForConnection?.();
1283
- connection.removeWorkerError = void 0;
1284
- transport?.dispose();
1285
- connection.transport = void 0;
1286
- if (currentConnection === connection) currentConnection = void 0;
1287
- if (currentWorker === connection.worker) currentWorker = void 0;
1288
- if (currentPort === connection.port) currentPort = void 0;
1289
- if (currentTransport === transport) currentTransport = void 0;
1290
- if (removeMessage === removeMessageForConnection) removeMessage = void 0;
1291
- if (removeWorkerError === removeWorkerErrorForConnection) removeWorkerError = void 0;
1292
- if (handshakeTimer === timer) handshakeTimer = void 0;
1293
- try {
1294
- connection.port.close();
1295
- } catch {
1296
- }
1297
- };
1298
- const disconnect = (reason, failure, fatal = false) => {
1299
- if (disposed) return;
1300
- const connection = currentConnection;
1301
- connection?.port ?? currentPort;
1302
- cleanupConnection(connection);
1303
- currentTransport = void 0;
1304
- currentWorker = void 0;
1305
- currentPort = void 0;
1306
- connectionId = void 0;
1307
- bridge.disconnect(reason);
1308
- emit({
1309
- ...currentSnapshot,
1310
- state: fatal ? "failed" : "disconnected",
1311
- error: failure ? errorMessage4(failure) : reason,
1312
- runtimeInstanceId: "",
1313
- connectionId: void 0,
1314
- units: [],
1315
- services: []
1316
- });
1317
- runtimeInstanceId = void 0;
1318
- const unavailable = failure ?? new RuntimeUnavailableError(reason);
1319
- if (!initialConnection && !fatal && autoReconnect) {
1320
- beginReadinessGeneration("SharedWorker reconnecting");
1321
- scheduleReconnect();
1322
- } else {
1323
- replaceWithRejectedReadiness(unavailable);
1324
- }
1325
- };
1326
- const onRuntimeError = (message) => {
1327
- const error = new RuntimeInitializationError({
1328
- pluginId: message.pluginId,
1329
- unitId: message.unitId,
1330
- phase: message.phase === "handshake" ? "handshake" : "startup",
1331
- error: message.message
1332
- });
1333
- const fatal = message.code === "runtime.protocol_mismatch" || message.code === "runtime.initialization_failed";
1334
- disconnect(message.message, error, fatal);
1335
- };
1336
- const onSnapshot = (snapshot) => {
1337
- if (snapshot.runtimeId !== options.id || snapshot.connectionId !== connectionId) return;
1338
- if (snapshot.runtimeKind !== "shared-worker") {
1339
- onRuntimeError({
1340
- code: "runtime.protocol_mismatch",
1341
- message: "Connected Runtime is not a SharedWorker Runtime",
1342
- phase: "handshake"
1343
- });
1344
- return;
1345
- }
1346
- const isNewAuthority = runtimeInstanceId !== snapshot.runtimeInstanceId;
1347
- if (isNewAuthority || bridge.state === "disconnected") {
1348
- const result = bridge.handshake({
1349
- connectionId: snapshot.connectionId,
1350
- authorityInstanceId: snapshot.runtimeInstanceId,
1351
- protocolVersion: snapshot.protocolVersion
1352
- });
1353
- if (!result.accepted) {
1354
- onRuntimeError({
1355
- code: "runtime.protocol_mismatch",
1356
- message: "Runtime protocol version mismatch",
1357
- phase: "handshake"
1358
- });
1359
- return;
1360
- }
1361
- }
1362
- runtimeInstanceId = snapshot.runtimeInstanceId;
1363
- const applied = bridge.applySnapshot({
1364
- connectionId: snapshot.connectionId,
1365
- authorityInstanceId: snapshot.runtimeInstanceId,
1366
- snapshotRevision: snapshot.snapshotRevision,
1367
- baseline: snapshot.baseline,
1368
- services: snapshot.services
1369
- });
1370
- if (!applied.accepted) {
1371
- if (applied.reason === "revision-gap" || applied.reason === "baseline-required") {
1372
- beginReadinessGeneration(`Runtime snapshot ${applied.reason}`);
1373
- emit({
1374
- ...currentSnapshot,
1375
- state: "disconnected",
1376
- error: `Runtime snapshot ${applied.reason}`,
1377
- units: [],
1378
- services: []
1379
- });
1380
- const port = currentPort;
1381
- if (port && connectionId) post3(port, { type: "webloom.runtime.resync", connectionId });
1382
- }
1383
- return;
1384
- }
1385
- emit({
1386
- runtimeId: snapshot.runtimeId,
1387
- runtimeKind: snapshot.runtimeKind,
1388
- runtimeInstanceId: snapshot.runtimeInstanceId,
1389
- state: snapshot.state === "ready" ? "ready" : snapshot.state,
1390
- snapshotRevision: snapshot.snapshotRevision,
1391
- connectionId: snapshot.connectionId,
1392
- units: snapshot.units,
1393
- services: snapshot.services
1394
- });
1395
- if (snapshot.baseline && snapshot.state === "ready" && bridge.state === "ready") {
1396
- if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
1397
- handshakeTimer = void 0;
1398
- readyDeferred.resolve(void 0);
1399
- initialConnection = false;
1400
- }
1401
- };
1402
- async function openConnection() {
1403
- if (disposed) return;
1404
- const generation = ++connectionGeneration;
1405
- if (readyDeferred.settled) readyDeferred = createDeferred();
1406
- let connection;
1407
- try {
1408
- const worker = makeWorker(options);
1409
- const port = worker.port;
1410
- const nextConnectionId = makeConnectionId(options.id);
1411
- connection = {
1412
- generation,
1413
- worker,
1414
- port,
1415
- connectionId: nextConnectionId,
1416
- cleaned: false
1417
- };
1418
- currentConnection = connection;
1419
- currentWorker = worker;
1420
- currentPort = port;
1421
- connectionId = nextConnectionId;
1422
- emit({
1423
- ...currentSnapshot,
1424
- state: "connecting",
1425
- connectionId: nextConnectionId,
1426
- error: void 0
1427
- });
1428
- await options.onConnection?.({
1429
- worker,
1430
- port,
1431
- connectionId: nextConnectionId
1432
- });
1433
- if (disposed || currentConnection !== connection) {
1434
- cleanupConnection(connection);
1435
- return;
1436
- }
1437
- ensureMessageEventTarget(port);
1438
- connection.transport = createMessagePortServiceTransport({ port, codec });
1439
- currentTransport = connection.transport;
1440
- const onWorkerError = (event) => {
1441
- if (disposed || generation !== connectionGeneration || worker !== currentWorker) return;
1442
- disconnect(`SharedWorker error${event.type ? `: ${event.type}` : ""}`);
1443
- };
1444
- if (worker.addEventListener) {
1445
- worker.addEventListener("error", onWorkerError);
1446
- connection.removeWorkerError = () => worker.removeEventListener?.("error", onWorkerError);
1447
- removeWorkerError = connection.removeWorkerError;
1448
- } else {
1449
- const workerWithOnError = worker;
1450
- const previousOnError = workerWithOnError.onerror;
1451
- const composedOnError = (event) => {
1452
- previousOnError?.(event);
1453
- onWorkerError(event);
1454
- };
1455
- workerWithOnError.onerror = composedOnError;
1456
- connection.removeWorkerError = () => {
1457
- if (workerWithOnError.onerror === composedOnError) workerWithOnError.onerror = previousOnError;
1458
- };
1459
- removeWorkerError = connection.removeWorkerError;
1460
- }
1461
- const onMessage = (event) => {
1462
- if (disposed || generation !== connectionGeneration || port !== currentPort) return;
1463
- if (isRuntimeError(event.data)) {
1464
- onRuntimeError(event.data);
1465
- return;
1466
- }
1467
- if (isSnapshot(event.data)) {
1468
- onSnapshot(event.data);
1469
- return;
1470
- }
1471
- const decoded = codec.decode(event.data);
1472
- if (decoded?.type === codec.type("disconnect")) {
1473
- disconnect(typeof decoded.reason === "string" ? decoded.reason : "SharedWorker disconnected");
1474
- }
1475
- };
1476
- connection.removeMessage = addMessageListener(port, onMessage);
1477
- removeMessage = connection.removeMessage;
1478
- port.start();
1479
- const timer = setTimeout(() => {
1480
- if (disposed || generation !== connectionGeneration || port !== currentPort) return;
1481
- const error = new RuntimeInitializationError({
1482
- phase: "handshake",
1483
- error: `SharedWorker handshake timed out after ${handshakeTimeoutMs}ms`
1484
- });
1485
- disconnect(error.message, error, false);
1486
- }, handshakeTimeoutMs);
1487
- connection.handshakeTimer = timer;
1488
- handshakeTimer = timer;
1489
- post3(port, {
1490
- type: RUNTIME_HELLO_TYPE,
1491
- protocolVersion: RUNTIME_PROTOCOL_VERSION,
1492
- connectionId: nextConnectionId,
1493
- runtimeId: options.id
1494
- });
1495
- await readyDeferred.promise;
1496
- } catch (error) {
1497
- if (connection && !connection.cleaned && currentConnection === connection) {
1498
- disconnect("SharedWorker connection setup failed", error, false);
1499
- } else if (connection && !connection.cleaned) {
1500
- cleanupConnection(connection);
1501
- } else if (!connection && !disposed && initialConnection) {
1502
- replaceWithRejectedReadiness(error);
1503
- }
1504
- throw error;
1505
- }
1506
- }
1507
- function isSnapshot(input) {
1508
- return isRuntimeSnapshot(input);
1509
- }
1510
- const app = {
1511
- runtimeKind: "shared-worker",
1512
- runtimeId: options.id,
1513
- serviceBridge: bridge,
1514
- get runtimeInstanceId() {
1515
- return runtimeInstanceId;
1516
- },
1517
- get connectionId() {
1518
- return connectionId;
1519
- },
1520
- state: () => currentSnapshot,
1521
- ready: () => readyDeferred.promise,
1522
- capability(capabilityId, capabilityOptions = {}) {
1523
- if (disposed || currentSnapshot.state !== "ready") {
1524
- throw new RuntimeUnavailableError(`Capability "${capabilityId}" is not ready`);
1525
- }
1526
- return bridge.requireProxy({
1527
- capabilityId,
1528
- contractVersion: capabilityOptions.contractVersion ?? `${capabilityId}.v1`
1529
- });
1530
- },
1531
- subscribe(listener) {
1532
- listeners.add(listener);
1533
- listener(currentSnapshot);
1534
- return () => listeners.delete(listener);
1535
- },
1536
- dispose(reason = "SharedWorker connection disposed") {
1537
- if (disposed) return Promise.resolve();
1538
- disposed = true;
1539
- if (reconnectTimer !== void 0) clearTimeout(reconnectTimer);
1540
- replaceWithRejectedReadiness(new RuntimeUnavailableError(reason));
1541
- const connection = currentConnection;
1542
- const port = connection?.port ?? currentPort;
1543
- if (port) post3(port, codec.encode({ type: codec.type("disconnect"), reason }));
1544
- cleanupConnection(connection);
1545
- currentWorker = void 0;
1546
- currentPort = void 0;
1547
- connectionId = void 0;
1548
- runtimeInstanceId = void 0;
1549
- currentTransport?.dispose();
1550
- currentTransport = void 0;
1551
- bridge.disconnect(reason);
1552
- emit({
1553
- ...currentSnapshot,
1554
- state: "disposed",
1555
- runtimeInstanceId: "",
1556
- connectionId: void 0,
1557
- units: [],
1558
- services: []
1559
- });
1560
- return Promise.resolve();
1561
- }
1562
- };
1563
- try {
1564
- await openConnection();
1565
- } catch (error) {
1566
- if (!disposed) {
1567
- emit({ ...currentSnapshot, state: "failed", error: errorMessage4(error) });
1568
- }
1569
- throw error;
1570
- }
1571
- return app;
1572
- }
1573
-
1574
966
  // src/lifecycle/permissionVerifier.ts
1575
967
  function verifyPermissionLease(options) {
1576
968
  options.lease.assert(options.permission);
@@ -1988,6 +1380,6 @@ function createUpgradeGate(options) {
1988
1380
  };
1989
1381
  }
1990
1382
 
1991
- export { RUNTIME_ERROR_TYPE, RUNTIME_HELLO_TYPE, RUNTIME_PROTOCOL_VERSION, RUNTIME_RESYNC_TYPE, RUNTIME_SNAPSHOT_TYPE, RuntimeInitializationError, RuntimeUnavailableError, connectSharedWorker, createMessagePortServiceProvider, createRuntimeMessageCodec, createScopedRegistryFacade, createUpgradeGate, createWindowApp, definePlugin, defineRuntimeUnitDependencies, defineRuntimeUnitProvidedContracts, isRuntimeError, isRuntimeHello, isRuntimeResync, isRuntimeSnapshot, materializePluginDefinition, materializePluginDefinitions, runtimeCapabilityContractVersion, startSharedWorkerApp, unitSnapshotFromState, verifyPermissionLease };
1383
+ export { createMessagePortServiceProvider, createScopedRegistryFacade, createUpgradeGate, createWindowApp, definePlugin, defineRuntimeUnitDependencies, defineRuntimeUnitProvidedContracts, materializePluginDefinition, materializePluginDefinitions, runtimeCapabilityContractVersion, startSharedWorkerApp, verifyPermissionLease };
1992
1384
  //# sourceMappingURL=index.js.map
1993
1385
  //# sourceMappingURL=index.js.map