webloom-framework 0.2.0 → 0.4.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.
Files changed (41) hide show
  1. package/README.md +74 -43
  2. package/dist/advanced.d.ts +168 -0
  3. package/dist/advanced.js +441 -0
  4. package/dist/advanced.js.map +1 -0
  5. package/dist/chunk-4DSINPZD.js +158 -0
  6. package/dist/chunk-4DSINPZD.js.map +1 -0
  7. package/dist/chunk-JHJZIO2H.js +4018 -0
  8. package/dist/chunk-JHJZIO2H.js.map +1 -0
  9. package/dist/chunk-RAQOFUZY.js +1737 -0
  10. package/dist/chunk-RAQOFUZY.js.map +1 -0
  11. package/dist/chunk-YOIH6H36.js +433 -0
  12. package/dist/chunk-YOIH6H36.js.map +1 -0
  13. package/dist/index.d.ts +38 -443
  14. package/dist/index.js +38 -1967
  15. package/dist/index.js.map +1 -1
  16. package/dist/messageBus-CtrwkjrO.d.ts +5 -0
  17. package/dist/messagePortServiceTransport-B0FyJr43.d.ts +264 -0
  18. package/dist/react.d.ts +41 -28
  19. package/dist/react.js +79 -88
  20. package/dist/react.js.map +1 -1
  21. package/dist/runtimeTypes-DquUCHz-.d.ts +1640 -0
  22. package/dist/sharedWorkerHost-Cb2a1_KD.d.ts +190 -0
  23. package/dist/testing.d.ts +18 -17
  24. package/dist/testing.js +23 -15
  25. package/dist/testing.js.map +1 -1
  26. package/dist/windowRuntime-B6Ue8jso.d.ts +23 -0
  27. package/docs/api.md +149 -111
  28. package/docs/migration-baseline.md +2 -2
  29. package/docs/proposals/browser-runtime-v1/implementation-plan.md +7 -1
  30. package/docs/proposals/browser-runtime-v1/requirements.md +6 -1
  31. package/docs/proposals/browser-runtime-v1/verification.md +20 -13
  32. package/docs/proposals/shared-worker-call-first/SWCF-009-typed-transfer-follow-up.md +34 -0
  33. package/docs/proposals/shared-worker-call-first/implementation-plan.md +567 -0
  34. package/docs/proposals/webloom-v4/implementation-plan.md +443 -0
  35. package/docs/proposals/webloom-v4/requirements.md +555 -0
  36. package/docs/proposals/webloom-v4/verification.md +84 -0
  37. package/package.json +9 -2
  38. package/dist/chunk-URY3E6UH.js +0 -3802
  39. package/dist/chunk-URY3E6UH.js.map +0 -1
  40. package/dist/createPluginHost-ChJNBTsX.d.ts +0 -1339
  41. package/dist/resourceRegistry-MNM1c7od.d.ts +0 -157
package/dist/index.js CHANGED
@@ -1,1993 +1,64 @@
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
+ export { startSharedWorkerApp } from './chunk-YOIH6H36.js';
2
+ export { connectSharedWorker } from './chunk-RAQOFUZY.js';
3
+ import { capabilityDescriptor } from './chunk-JHJZIO2H.js';
4
+ export { LIFECYCLE_ERROR_TEXT, LifecycleScopeRevokedError, PermissionDeniedError, PermissionLeaseRevokedError, RESOURCE_OWNER, RESOURCE_REGISTRY, RuntimeInitializationError, RuntimeUnavailableError, SCOPED_TASK_SCHEDULER_CAPABILITY, UpgradeGateRejectedError, WebLoomError, assertCapability, capabilityDescriptor, capabilityKey, createLifecycleScope, createMessageBus, createResourceScope, createWindowApp, defineCapability, isCapability, isCapabilityDescriptor, lifecycleErrorText } from './chunk-JHJZIO2H.js';
3
5
 
4
- // src/contracts/plugin.ts
5
- function runtimeCapabilityContractVersion(capability) {
6
- return `${capability}.v1`;
6
+ // src/contracts/messageBus.ts
7
+ var RUNTIME_MESSAGE_BUS = "webloom.messageBus";
8
+
9
+ // src/authoring/definePlugin.ts
10
+ function nonEmpty(value, label) {
11
+ if (value.trim() === "") throw new TypeError(`${label} must be a non-empty string`);
12
+ return value;
7
13
  }
8
- function defineRuntimeUnitDependencies(dependencies, defaults) {
14
+ function normalizeDependencies(dependencies, runtime) {
9
15
  return dependencies.map((dependency) => ({
10
- capability: dependency.capability,
11
- contractVersion: runtimeCapabilityContractVersion(dependency.capability),
12
- sourceRuntime: defaults.sourceRuntime,
16
+ capability: capabilityDescriptor(dependency.capability),
17
+ ...dependency.source === "peer" ? { source: "peer" } : { sourceRuntime: dependency.sourceRuntime ?? runtime },
13
18
  ...dependency.reason !== void 0 ? { reason: dependency.reason } : {},
14
19
  ...dependency.optional !== void 0 ? { optional: dependency.optional } : {}
15
20
  }));
16
21
  }
17
- function defineRuntimeUnitProvidedContracts(capabilities) {
18
- return Object.fromEntries(
19
- capabilities.map((capability) => [capability, runtimeCapabilityContractVersion(capability)])
20
- );
21
- }
22
-
23
- // src/authoring/definePlugin.ts
24
- function normalizeDependency(dependency) {
25
- const contractVersion = dependency.contractVersion ?? runtimeCapabilityContractVersion(dependency.capability);
26
- return {
27
- capability: dependency.capability,
28
- contractVersion,
29
- ...dependency.sourceRuntime !== void 0 ? { sourceRuntime: dependency.sourceRuntime } : {},
30
- ...dependency.reason !== void 0 ? { reason: dependency.reason } : {},
31
- ...dependency.optional !== void 0 ? { optional: dependency.optional } : {}
32
- };
33
- }
34
22
  function definePlugin(options) {
35
- if (!options || typeof options.id !== "string" || options.id.trim() === "") {
36
- throw new Error("Plugin id must be a non-empty string");
37
- }
38
- if (typeof options.setup !== "function") throw new Error(`Plugin "${options.id}" setup must be a function`);
39
- const unitId = options.unitId ?? options.id;
40
- if (unitId.trim() === "") throw new Error(`Plugin "${options.id}" unitId must be a non-empty string`);
41
- const provides = [...new Set(options.provides ?? [])];
42
- const dependencies = (options.dependencies ?? []).map(normalizeDependency);
43
- const providedContracts = {
44
- ...defineRuntimeUnitProvidedContracts(provides),
45
- ...options.providedContracts ?? {}
46
- };
23
+ if (!options || typeof options.id !== "string") throw new TypeError("Plugin id must be a non-empty string");
24
+ nonEmpty(options.id, "Plugin id");
25
+ if (typeof options.setup !== "function") throw new TypeError(`Plugin "${options.id}" setup must be a function`);
26
+ const unitId = nonEmpty(options.unitId ?? options.id, `Plugin "${options.id}" unitId`);
27
+ const startup = options.startup ?? "optional";
28
+ const defaultEnabled = options.defaultEnabled ?? true;
29
+ const canDisable = options.canDisable ?? startup !== "required";
30
+ if (startup === "required" && (!defaultEnabled || canDisable)) {
31
+ throw new TypeError(`Plugin "${options.id}" required startup policy is inconsistent`);
32
+ }
33
+ const provides = [...options.provides ?? []];
34
+ const dependencies = [...options.dependencies ?? []];
35
+ const runtime = options.runtime;
47
36
  const descriptor = {
48
37
  id: unitId,
49
- ...options.runtime !== void 0 ? { runtime: options.runtime } : {},
50
- ...dependencies.length > 0 ? { dependencies } : {},
51
- ...provides.length > 0 ? { provides } : {},
52
- ...Object.keys(providedContracts).length > 0 ? { providedContracts } : {},
53
- ...options.contribution !== void 0 ? { contribution: options.contribution } : {},
38
+ ...runtime !== void 0 ? { runtime } : {},
39
+ ...provides.length > 0 ? { provides: provides.map((capability) => capabilityDescriptor(capability)) } : {},
40
+ ...dependencies.length > 0 ? { dependencies: normalizeDependencies(dependencies, runtime ?? "window-main") } : {},
54
41
  ...options.permissions !== void 0 ? { permissions: [...options.permissions] } : {},
55
- ...options.config !== void 0 ? { config: options.config } : {}
42
+ ...options.config !== void 0 ? { config: options.config } : {},
43
+ ...options.contribution !== void 0 ? { contribution: options.contribution } : {}
56
44
  };
57
- const metaInput = options.meta ?? {};
58
- const required = options.required === true || options.startup === "required" || metaInput.startup === "required";
59
- const meta = {
60
- ...metaInput,
61
- defaultEnabled: options.defaultEnabled ?? metaInput.defaultEnabled ?? true,
62
- canDisable: options.canDisable ?? metaInput.canDisable ?? !required,
63
- startup: required ? "required" : options.startup ?? metaInput.startup ?? "optional"
64
- };
65
- if (meta.startup === "required" && (meta.canDisable || !meta.defaultEnabled)) {
66
- throw new Error(`Plugin "${options.id}" required metadata is inconsistent`);
67
- }
68
45
  const manifest = {
69
46
  id: options.id,
70
47
  name: options.name ?? options.id,
71
48
  ...options.description !== void 0 ? { description: options.description } : {},
72
- meta,
49
+ startup,
50
+ defaultEnabled,
51
+ canDisable,
73
52
  units: [descriptor]
74
53
  };
75
54
  return Object.freeze({
76
55
  manifest: Object.freeze(manifest),
77
56
  descriptor: Object.freeze(descriptor),
78
- setup: options.setup
57
+ setup: options.setup,
58
+ capabilities: Object.freeze([...provides, ...dependencies.map((dependency) => dependency.capability)])
79
59
  });
80
60
  }
81
61
 
82
- // src/runtime/pluginDefinitions.ts
83
- function normalizeProductDependencies(dependencies, runtime) {
84
- return dependencies?.map((dependency) => ({
85
- capability: dependency.capability,
86
- contractVersion: dependency.contractVersion ?? runtimeCapabilityContractVersion(dependency.capability),
87
- sourceRuntime: dependency.sourceRuntime ?? runtime,
88
- ...dependency.reason !== void 0 ? { reason: dependency.reason } : {},
89
- ...dependency.optional !== void 0 ? { optional: dependency.optional } : {}
90
- }));
91
- }
92
- function materializePluginDefinition(input, runtime) {
93
- const manifest = input.manifest;
94
- const setup = input.setup;
95
- if (!manifest || typeof manifest.id !== "string" || manifest.id.trim() === "") {
96
- throw new Error("Plugin manifest id must be a non-empty string");
97
- }
98
- const existingUnits = manifest.units ?? [];
99
- const requestedUnitId = "unitId" in input ? input.unitId : void 0;
100
- const matchingUnits = existingUnits.filter((unit) => unit.runtime === void 0 || unit.runtime === runtime);
101
- let implementationUnitId;
102
- if (existingUnits.length === 0) {
103
- implementationUnitId = manifest.id;
104
- } else if (requestedUnitId !== void 0) {
105
- const selected = existingUnits.find((unit) => unit.id === requestedUnitId);
106
- if (!selected) throw new Error(`Plugin "${manifest.id}" does not declare unit "${requestedUnitId}"`);
107
- if (selected.runtime !== void 0 && selected.runtime !== runtime) {
108
- throw new Error(`Plugin "${manifest.id}" unit "${requestedUnitId}" targets ${selected.runtime}, not ${runtime}`);
109
- }
110
- implementationUnitId = selected.id;
111
- } else if (matchingUnits.length === 1 && matchingUnits[0]) {
112
- implementationUnitId = matchingUnits[0].id;
113
- } else {
114
- throw new Error(
115
- `Plugin "${manifest.id}" has ${matchingUnits.length} implementation units for ${runtime}; specify unitId explicitly`
116
- );
117
- }
118
- const selectedUnit = existingUnits.length > 0 ? existingUnits.find((unit) => unit.id === implementationUnitId) : void 0;
119
- const units = selectedUnit ? [(() => {
120
- const unitRuntime = selectedUnit.runtime ?? runtime;
121
- const provides = [...selectedUnit.provides ?? []];
122
- const dependencies = selectedUnit.dependencies?.map((dependency) => ({
123
- capability: dependency.capability,
124
- contractVersion: dependency.contractVersion ?? runtimeCapabilityContractVersion(dependency.capability),
125
- sourceRuntime: dependency.sourceRuntime ?? unitRuntime,
126
- ...dependency.reason !== void 0 ? { reason: dependency.reason } : {},
127
- ...dependency.optional !== void 0 ? { optional: dependency.optional } : {}
128
- }));
129
- return {
130
- ...selectedUnit,
131
- runtime: unitRuntime,
132
- dependencies,
133
- ...provides.length > 0 ? { providedContracts: { ...defineRuntimeUnitProvidedContracts(provides), ...selectedUnit.providedContracts ?? {} } } : {}
134
- };
135
- })()] : [{
136
- id: manifest.id,
137
- runtime,
138
- dependencies: normalizeProductDependencies(manifest.dependencies, runtime),
139
- ...manifest.provides && manifest.provides.length > 0 ? {
140
- provides: [...manifest.provides],
141
- providedContracts: defineRuntimeUnitProvidedContracts(manifest.provides)
142
- } : {},
143
- ...manifest.permissions !== void 0 ? { permissions: [...manifest.permissions] } : {},
144
- ...manifest.config !== void 0 ? { config: manifest.config } : {},
145
- ...manifest.contribution !== void 0 ? { contribution: manifest.contribution } : {}
146
- }];
147
- const normalized = {
148
- ...manifest,
149
- units
150
- // Product-level declarations are retained only for compatibility with
151
- // low-level manifests; the selected unit is the source of truth in Host.
152
- };
153
- return { manifest: normalized, unitId: implementationUnitId, setup };
154
- }
155
- function materializePluginDefinitions(inputs, runtime) {
156
- return inputs.map((input) => materializePluginDefinition(input, runtime));
157
- }
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
- // src/runtime/windowRuntime.ts
244
- function makeRuntimeInstanceId(runtimeId) {
245
- try {
246
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
247
- return `${runtimeId}:${crypto.randomUUID()}`;
248
- }
249
- } catch {
250
- }
251
- return `${runtimeId}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
252
- }
253
- function errorMessage(error) {
254
- return error instanceof Error ? error.message : String(error);
255
- }
256
- function isStartupPluginError(error) {
257
- return error instanceof StartupPluginError && typeof error.details?.pluginId === "string";
258
- }
259
- function buildLocalServices(host, manifests, runtimeInstanceId, revision) {
260
- const services = [];
261
- for (const manifest of manifests) {
262
- const state = host.state(manifest.id);
263
- const unit = manifest.units?.find((candidate) => candidate.id === state.unitId) ?? manifest.units?.[0];
264
- 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
- for (const capability of unit.provides ?? []) {
267
- services.push({
268
- capabilityId: capability,
269
- providerInstanceId: state.instanceId,
270
- runtime: unit.runtime,
271
- contractVersion: unit.providedContracts?.[capability] ?? `${capability}.v1`,
272
- authorityInstanceId: runtimeInstanceId,
273
- scopeId,
274
- handoverGeneration: 0,
275
- attributes: Object.freeze({}),
276
- status: "ready",
277
- snapshotRevision: revision
278
- });
279
- }
280
- }
281
- return services;
282
- }
283
- async function createWindowApp(options) {
284
- const runtimeId = options.id ?? "window-main";
285
- const remoteRuntime = options.remoteRuntime;
286
- const runtimeInstanceId = makeRuntimeInstanceId(runtimeId);
287
- let currentState = {
288
- runtimeId,
289
- runtimeKind: "window-main",
290
- runtimeInstanceId,
291
- state: "starting",
292
- snapshotRevision: 0,
293
- units: [],
294
- services: []
295
- };
296
- const listeners = /* @__PURE__ */ new Set();
297
- let disposed = false;
298
- let disposePromise;
299
- const emit = (next) => {
300
- currentState = Object.freeze({
301
- ...next,
302
- units: Object.freeze([...next.units]),
303
- services: Object.freeze([...next.services])
304
- });
305
- for (const listener of [...listeners]) {
306
- try {
307
- listener(currentState);
308
- } catch {
309
- }
310
- }
311
- };
312
- let materialized;
313
- try {
314
- materialized = materializePluginDefinitions(options.plugins, "window-main");
315
- } catch (error) {
316
- throw new RuntimeInitializationError({ phase: "validate", error: errorMessage(error) });
317
- }
318
- const manifests = materialized.map((item) => item.manifest);
319
- let implementations;
320
- try {
321
- const duplicate = manifests.find((manifest, index) => manifests.findIndex((candidate) => candidate.id === manifest.id) !== index);
322
- if (duplicate) throw new Error(`Plugin "${duplicate.id}" is declared more than once`);
323
- implementations = createRuntimeUnitImplementationRegistry(
324
- materialized.map((item) => ({
325
- pluginId: item.manifest.id,
326
- unitId: item.unitId,
327
- setup: item.setup
328
- }))
329
- );
330
- } catch (error) {
331
- throw new RuntimeInitializationError({ phase: "validate", error: errorMessage(error) });
332
- }
333
- let host;
334
- const suppliedHost = options.host;
335
- try {
336
- const {
337
- id: _id,
338
- plugins: _plugins,
339
- host: _host,
340
- remoteRuntime: _remoteRuntime,
341
- ...hostOptions
342
- } = options;
343
- const suppliedBridgeFactory = hostOptions.serviceBridgeForPlugin;
344
- host = suppliedHost ?? createPluginHost({
345
- ...hostOptions,
346
- runtime: "window-main",
347
- externalRuntimeDependencies: remoteRuntime !== void 0 || hostOptions.externalRuntimeDependencies,
348
- remoteServiceReferences: () => remoteRuntime?.state().services ?? hostOptions.remoteServiceReferences?.() ?? [],
349
- runtimeSnapshots: () => remoteRuntime ? remoteRuntime.state().units.map((unit) => ({
350
- pluginId: unit.pluginId,
351
- unitId: unit.unitId,
352
- runtime: unit.runtime,
353
- ...unit.instanceId !== void 0 ? { instanceId: unit.instanceId } : {},
354
- state: unit.state
355
- })) : hostOptions.runtimeSnapshots?.() ?? [],
356
- serviceBridgeForPlugin: (pluginId, instanceId) => suppliedBridgeFactory?.(pluginId, instanceId) ?? remoteRuntime?.serviceBridge,
357
- rootAttributes: {
358
- ...hostOptions.rootAttributes ?? {},
359
- runtimeId,
360
- runtimeInstanceId
361
- },
362
- runtimeUnitImplementationRegistry: implementations
363
- });
364
- } catch (error) {
365
- throw new RuntimeInitializationError({ phase: "validate", error: errorMessage(error) });
366
- }
367
- let removeRemoteSubscription;
368
- const manifestsForSnapshot = () => {
369
- if (!suppliedHost) return manifests;
370
- return suppliedHost.manifests().map((pluginId) => suppliedHost.getManifest(pluginId)).filter((manifest) => manifest !== void 0);
371
- };
372
- const refresh = () => {
373
- const snapshotManifests = manifestsForSnapshot();
374
- const units = snapshotManifests.flatMap((manifest) => {
375
- const state = host.state(manifest.id);
376
- return (state.units ?? []).map((unit) => unitSnapshotFromState(
377
- unit,
378
- unit.runtime
379
- ));
380
- });
381
- const revision = Math.max(currentState.snapshotRevision + 1, host.version());
382
- emit({
383
- ...currentState,
384
- snapshotRevision: revision,
385
- units,
386
- services: [
387
- ...buildLocalServices(host, snapshotManifests, runtimeInstanceId, revision),
388
- ...remoteRuntime?.state().services ?? []
389
- ]
390
- });
391
- };
392
- const removeHostSubscription = host.subscribe(refresh);
393
- try {
394
- if (!suppliedHost) await host.registerAll(manifests);
395
- const failedRequired = suppliedHost ? void 0 : manifests.find((manifest) => {
396
- const required = manifest.meta.startup === "required" || manifest.meta.canDisable === false;
397
- return required && host.state(manifest.id).kind !== "enabled";
398
- });
399
- if (failedRequired) {
400
- const state = host.state(failedRequired.id);
401
- throw new StartupPluginError({
402
- pluginId: failedRequired.id,
403
- unitId: state.unitId ?? failedRequired.units?.[0]?.id ?? failedRequired.id,
404
- capabilities: [],
405
- state: state.kind,
406
- error: state.error ?? `Required plugin is ${state.kind}${state.blockedBy ? `: ${state.blockedBy.join(", ")}` : ""}`
407
- });
408
- }
409
- } catch (error) {
410
- refresh();
411
- const pluginId = isStartupPluginError(error) ? error.details.pluginId : void 0;
412
- const manifest = pluginId ? manifests.find((candidate) => candidate.id === pluginId) : void 0;
413
- const required = manifest?.meta.startup === "required" || manifest?.meta.canDisable === false;
414
- if (required || !isStartupPluginError(error)) {
415
- removeHostSubscription();
416
- removeRemoteSubscription?.();
417
- await host.dispose("window runtime initialization failed").catch(() => void 0);
418
- if (error instanceof RuntimeInitializationError) throw error;
419
- throw new RuntimeInitializationError({
420
- pluginId,
421
- unitId: isStartupPluginError(error) ? error.details.unitId : manifest?.units?.[0]?.id,
422
- phase: "startup",
423
- error: errorMessage(error)
424
- });
425
- }
426
- }
427
- refresh();
428
- emit({ ...currentState, state: "ready" });
429
- removeRemoteSubscription = remoteRuntime?.subscribe(() => {
430
- host.refreshRuntimeUnitSnapshots();
431
- void host.reconcile().catch(() => void 0);
432
- });
433
- const app = {
434
- runtimeKind: "window-main",
435
- runtimeId,
436
- runtimeInstanceId,
437
- host,
438
- state: () => currentState,
439
- capability(capabilityId) {
440
- if (disposed || currentState.state === "disposed" || currentState.state === "stopping") {
441
- throw new RuntimeUnavailableError("Window Runtime has been disposed");
442
- }
443
- return host.capabilities.get(capabilityId);
444
- },
445
- subscribe(listener) {
446
- listeners.add(listener);
447
- listener(currentState);
448
- return () => listeners.delete(listener);
449
- },
450
- dispose(reason = "window runtime disposed") {
451
- if (disposePromise) return disposePromise;
452
- disposed = true;
453
- emit({ ...currentState, state: "stopping" });
454
- removeHostSubscription();
455
- removeRemoteSubscription?.();
456
- disposePromise = host.dispose(reason).then((result) => {
457
- emit({ ...currentState, state: "disposed" });
458
- return result;
459
- }, (error) => {
460
- emit({ ...currentState, state: "failed", error: errorMessage(error) });
461
- throw error;
462
- });
463
- return disposePromise;
464
- }
465
- };
466
- return app;
467
- }
468
-
469
- // src/transport/messagePortServiceProvider.ts
470
- function isCallMessage(input, codec) {
471
- if (!input || typeof input !== "object") return false;
472
- 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);
474
- }
475
- function isCancelMessage(input, codec) {
476
- if (!input || typeof input !== "object") return false;
477
- const message = input;
478
- return message.type === codec.type("cancel") && typeof message.callId === "string" && typeof message.connectionId === "string" && typeof message.providerInstanceId === "string";
479
- }
480
- function errorMessage2(error) {
481
- const candidate = error && typeof error === "object" ? error : void 0;
482
- return {
483
- ...typeof candidate?.name === "string" ? { name: candidate.name } : {},
484
- message: typeof candidate?.message === "string" ? candidate.message : String(error),
485
- ...typeof candidate?.code === "string" ? { code: candidate.code } : {}
486
- };
487
- }
488
- function post(port, message) {
489
- try {
490
- port.postMessage(message);
491
- } catch {
492
- }
493
- }
494
- function createMessagePortServiceProvider(options) {
495
- const pending = /* @__PURE__ */ new Map();
496
- const codec = options.codec ?? createRemoteServiceMessageCodec();
497
- let activeProviderInstanceIds = new Set(
498
- options.snapshot.services.map((service) => service.providerInstanceId)
499
- );
500
- let disposed = false;
501
- const sendError = (message, error) => {
502
- const response = {
503
- type: codec.type("error"),
504
- callId: message.callId,
505
- connectionId: message.connectionId,
506
- providerInstanceId: message.providerInstanceId,
507
- error: errorMessage2(error)
508
- };
509
- post(options.port, codec.encode(response));
510
- };
511
- const onMessage = (event) => {
512
- if (disposed) return;
513
- const decoded = codec.decode(event.data);
514
- 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"));
519
- }
520
- return;
521
- }
522
- if (!isCallMessage(decoded, codec)) return;
523
- 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" }));
526
- return;
527
- }
528
- if (!activeProviderInstanceIds.has(message.providerInstanceId)) {
529
- sendError(message, Object.assign(new Error("Remote service provider mismatch"), { code: "service.provider_mismatch" }));
530
- return;
531
- }
532
- if (pending.has(message.callId)) {
533
- sendError(message, Object.assign(new Error("Remote service callId is duplicated"), { code: "service.duplicate_call" }));
534
- return;
535
- }
536
- const controller = new AbortController();
537
- pending.set(message.callId, { controller, providerInstanceId: message.providerInstanceId });
538
- void (async () => {
539
- try {
540
- const result = await options.handleCall({ message, signal: controller.signal });
541
- if (controller.signal.aborted || disposed) return;
542
- const response = {
543
- type: codec.type("result"),
544
- callId: message.callId,
545
- connectionId: message.connectionId,
546
- providerInstanceId: message.providerInstanceId,
547
- result
548
- };
549
- post(options.port, codec.encode(response));
550
- } catch (error) {
551
- if (disposed) return;
552
- sendError(message, error);
553
- } finally {
554
- pending.delete(message.callId);
555
- }
556
- })();
557
- };
558
- options.port.addEventListener("message", onMessage);
559
- 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
- const provider = {
577
- publishSnapshot(snapshot) {
578
- 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
- }));
586
- },
587
- invalidate(reason = "Remote service invalidated") {
588
- 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
- }));
595
- },
596
- disconnect(reason = "Remote service disconnected") {
597
- if (disposed) return;
598
- post(options.port, codec.encode({
599
- type: codec.type("disconnect"),
600
- reason
601
- }));
602
- dispose();
603
- },
604
- dispose
605
- };
606
- return provider;
607
- }
608
-
609
- // src/runtime/sharedWorkerHost.ts
610
- function makeRuntimeInstanceId2(runtimeId) {
611
- try {
612
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
613
- return `${runtimeId}:${crypto.randomUUID()}`;
614
- }
615
- } catch {
616
- }
617
- return `${runtimeId}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
618
- }
619
- function errorMessage3(error) {
620
- return error instanceof Error ? error.message : String(error);
621
- }
622
- function startupDetails(error) {
623
- if (error instanceof StartupPluginError) {
624
- return {
625
- pluginId: error.details.pluginId,
626
- unitId: error.details.unitId,
627
- message: error.details.error ?? error.message
628
- };
629
- }
630
- if (error instanceof RuntimeInitializationError) {
631
- return {
632
- pluginId: error.details.pluginId,
633
- unitId: error.details.unitId,
634
- message: error.message
635
- };
636
- }
637
- return { message: errorMessage3(error) };
638
- }
639
- 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);
685
- }
686
- function post2(port, message) {
687
- try {
688
- port.postMessage(message);
689
- } catch {
690
- }
691
- }
692
- function startSharedWorkerApp(options) {
693
- if (!options || typeof options.id !== "string" || options.id.trim() === "") {
694
- throw new Error("SharedWorker runtime id must be a non-empty string");
695
- }
696
- const workerScope = options.globalScope ?? ("onconnect" in globalThis ? globalThis : void 0);
697
- if (!workerScope) {
698
- throw new RuntimeInitializationError({
699
- phase: "validate",
700
- error: "startSharedWorkerApp must run in a SharedWorkerGlobalScope"
701
- });
702
- }
703
- const runtimeId = options.id;
704
- const runtimeInstanceId = makeRuntimeInstanceId2(runtimeId);
705
- const listeners = /* @__PURE__ */ new Set();
706
- const endpoints = /* @__PURE__ */ new Set();
707
- const codec = createRuntimeMessageCodec();
708
- let runtimeState = "starting";
709
- let revision = 0;
710
- let host;
711
- let manifests = [];
712
- let disposed = false;
713
- let disposePromise;
714
- const emit = () => {
715
- const snapshot = currentSnapshot();
716
- for (const listener of [...listeners]) {
717
- try {
718
- listener(snapshot);
719
- } catch {
720
- }
721
- }
722
- };
723
- const currentSnapshot = () => Object.freeze({
724
- runtimeId,
725
- runtimeKind: "shared-worker",
726
- runtimeInstanceId,
727
- state: runtimeState,
728
- snapshotRevision: revision,
729
- units: Object.freeze(host && runtimeState !== "failed" ? manifests.flatMap((manifest) => {
730
- const state = host?.state(manifest.id);
731
- return (state?.units ?? []).map((unit) => ({
732
- pluginId: unit.pluginId,
733
- unitId: unit.unitId,
734
- runtime: unit.runtime,
735
- ...unit.instanceId !== void 0 ? { instanceId: unit.instanceId } : {},
736
- state: unit.kind
737
- }));
738
- }) : []),
739
- services: Object.freeze(host && runtimeState !== "failed" ? serviceReferences("", revision) : [])
740
- });
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
- });
764
- }
765
- }
766
- return services;
767
- };
768
- const runtimeSnapshotFor = (endpoint, baseline) => ({
769
- type: RUNTIME_SNAPSHOT_TYPE,
770
- protocolVersion: RUNTIME_PROTOCOL_VERSION,
771
- runtimeId,
772
- runtimeKind: "shared-worker",
773
- runtimeInstanceId,
774
- connectionId: endpoint.connectionId,
775
- snapshotRevision: revision,
776
- baseline,
777
- state: runtimeState,
778
- 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)
787
- });
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)));
793
- };
794
- const publishAll = (baseline = false) => {
795
- for (const endpoint of [...endpoints]) publishEndpoint(endpoint, baseline);
796
- };
797
- const closeEndpoint = (endpoint, reason) => {
798
- if (endpoint.closed) return;
799
- endpoint.closed = true;
800
- endpoints.delete(endpoint);
801
- endpoint.removeMessage();
802
- endpoint.provider.disconnect(reason);
803
- };
804
- const sendError = (port, code, message, details = {}) => {
805
- const error = {
806
- type: RUNTIME_ERROR_TYPE,
807
- protocolVersion: RUNTIME_PROTOCOL_VERSION,
808
- code,
809
- message,
810
- ...details.pluginId !== void 0 ? { pluginId: details.pluginId } : {},
811
- ...details.unitId !== void 0 ? { unitId: details.unitId } : {},
812
- ...details.phase !== void 0 ? { phase: details.phase } : {}
813
- };
814
- post2(port, error);
815
- };
816
- const handleCall = async (connectionId, input) => {
817
- 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) {
822
- throw new Error("Runtime service reference is stale");
823
- }
824
- const manifest = manifests.find((candidate) => {
825
- const state2 = host?.state(candidate.id);
826
- return state2?.instanceId === reference.providerInstanceId;
827
- });
828
- if (!manifest) throw new Error("Runtime service provider is no longer active");
829
- const state = host.state(manifest.id);
830
- if (state.kind !== "enabled" || state.instanceId !== reference.providerInstanceId) {
831
- throw new Error("Runtime service provider is no longer active");
832
- }
833
- const unit = manifest.units?.find((candidate) => candidate.id === state.unitId);
834
- if (!unit || unit.runtime !== "shared-worker" || !unit.provides?.includes(reference.capabilityId)) {
835
- throw new Error("Runtime capability is not declared");
836
- }
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
- const expectedVersion = unit.providedContracts?.[reference.capabilityId] ?? `${reference.capabilityId}.v1`;
840
- if (expectedVersion !== reference.contractVersion) throw new Error("Runtime capability contract mismatch");
841
- const value = host.capabilities.get(reference.capabilityId);
842
- if (input.signal.aborted) throw input.signal.reason ?? new Error("Runtime service request cancelled");
843
- const request = message.request;
844
- if (typeof value === "function") return await value(request, input.signal);
845
- if (value && typeof value === "object") {
846
- const object = value;
847
- if (typeof object.handle === "function") return await object.handle(request, input.signal);
848
- if (request && typeof request === "object" && typeof request.method === "string") {
849
- const method = request.method;
850
- const methodValue = object[method];
851
- if (typeof methodValue === "function") {
852
- const args = request.args;
853
- return await methodValue(...Array.isArray(args) ? args : []);
854
- }
855
- }
856
- }
857
- throw new Error(`Runtime capability "${reference.capabilityId}" does not expose an RPC handler`);
858
- };
859
- const attachPort = (port) => {
860
- let endpoint;
861
- let helloSeen = false;
862
- let removeMessage = () => void 0;
863
- 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
- };
941
- removeMessage = addPortListener(port, onMessage);
942
- port.start();
943
- };
944
- const endpointsHasConnection = (connectionId) => [...endpoints].some((endpoint) => endpoint.connectionId === connectionId);
945
- let startupError;
946
- workerScope.onconnect = (event) => {
947
- try {
948
- options.onPortConnect?.(event);
949
- } catch (error) {
950
- const detail = startupDetails(error);
951
- for (const port of event.ports ?? []) {
952
- sendError(port, "runtime.initialization_failed", detail.message, {
953
- pluginId: detail.pluginId,
954
- unitId: detail.unitId,
955
- phase: "handshake"
956
- });
957
- }
958
- for (const port of event.ports ?? []) {
959
- try {
960
- port.close();
961
- } catch {
962
- }
963
- }
964
- return;
965
- }
966
- for (const port of event.ports ?? []) attachPort(port);
967
- };
968
- let materialized = [];
969
- let readyPromise;
970
- try {
971
- materialized = materializePluginDefinitions(options.plugins, "shared-worker");
972
- 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
- );
980
- const { id: _id, plugins: _plugins, globalScope: _scope, onPortConnect: _onPortConnect, ...hostOptions } = options;
981
- host = createPluginHost({
982
- ...hostOptions,
983
- runtime: "shared-worker",
984
- rootAttributes: {
985
- ...hostOptions.rootAttributes ?? {},
986
- runtimeId,
987
- runtimeInstanceId
988
- },
989
- runtimeUnitImplementationRegistry: implementations
990
- });
991
- host.subscribe(() => {
992
- if (runtimeState !== "ready" || disposed) return;
993
- revision += 1;
994
- publishAll(false);
995
- emit();
996
- });
997
- readyPromise = host.registerAll(manifests).then(() => {
998
- if (disposed) return;
999
- const runtimeHost = host;
1000
- if (!runtimeHost) throw new Error("SharedWorker Plugin Host is unavailable");
1001
- const failedRequired = manifests.find((manifest) => {
1002
- const required = manifest.meta.startup === "required" || manifest.meta.canDisable === false;
1003
- return required && runtimeHost.state(manifest.id).kind !== "enabled";
1004
- });
1005
- if (failedRequired) {
1006
- const state = runtimeHost.state(failedRequired.id);
1007
- throw new StartupPluginError({
1008
- pluginId: failedRequired.id,
1009
- unitId: state.unitId ?? failedRequired.units?.[0]?.id ?? failedRequired.id,
1010
- capabilities: [],
1011
- state: state.kind,
1012
- error: state.error ?? `Required plugin is ${state.kind}${state.blockedBy ? `: ${state.blockedBy.join(", ")}` : ""}`
1013
- });
1014
- }
1015
- runtimeState = "ready";
1016
- revision = Math.max(1, revision + 1);
1017
- publishAll(true);
1018
- emit();
1019
- }).catch((error) => {
1020
- startupError = error;
1021
- runtimeState = "failed";
1022
- emit();
1023
- const detail = startupDetails(error);
1024
- for (const endpoint of [...endpoints]) {
1025
- sendError(endpoint.port, "runtime.initialization_failed", detail.message, {
1026
- pluginId: detail.pluginId,
1027
- unitId: detail.unitId,
1028
- phase: "startup"
1029
- });
1030
- closeEndpoint(endpoint, "Runtime initialization failed");
1031
- }
1032
- throw new RuntimeInitializationError({
1033
- pluginId: detail.pluginId,
1034
- unitId: detail.unitId,
1035
- phase: "startup",
1036
- error: detail.message
1037
- });
1038
- });
1039
- } catch (error) {
1040
- startupError = error;
1041
- runtimeState = "failed";
1042
- emit();
1043
- readyPromise = Promise.reject(error instanceof RuntimeInitializationError ? error : new RuntimeInitializationError({ phase: "validate", error: errorMessage3(error) }));
1044
- }
1045
- const app = {
1046
- runtimeKind: "shared-worker",
1047
- runtimeId,
1048
- runtimeInstanceId,
1049
- ready: () => readyPromise,
1050
- async reconcile() {
1051
- if (!host) {
1052
- await readyPromise;
1053
- return;
1054
- }
1055
- if (runtimeState === "failed" || runtimeState === "disposed" || disposed) {
1056
- await readyPromise;
1057
- return;
1058
- }
1059
- await host.reconcile();
1060
- },
1061
- state: currentSnapshot,
1062
- subscribe(listener) {
1063
- listeners.add(listener);
1064
- listener(currentSnapshot());
1065
- return () => listeners.delete(listener);
1066
- },
1067
- dispose(reason = "shared worker runtime disposed") {
1068
- if (disposePromise) return disposePromise;
1069
- disposed = true;
1070
- runtimeState = "stopping";
1071
- emit();
1072
- 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 };
1075
- runtimeState = "disposed";
1076
- emit();
1077
- return result;
1078
- })();
1079
- return disposePromise;
1080
- }
1081
- };
1082
- return app;
1083
- }
1084
-
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
- // src/lifecycle/permissionVerifier.ts
1575
- function verifyPermissionLease(options) {
1576
- options.lease.assert(options.permission);
1577
- if (options.binding) options.lease.assertBinding(options.binding);
1578
- }
1579
-
1580
- // src/lifecycle/scopedRegistry.ts
1581
- function definitionId(value) {
1582
- if (!value || typeof value !== "object") return void 0;
1583
- const id = value.id;
1584
- return typeof id === "string" && id.length > 0 ? id : void 0;
1585
- }
1586
- function defaultRegistrationRules() {
1587
- return [{ method: "register", idArgument: 0, unregisterMethod: "unregister" }];
1588
- }
1589
- function createScopedRegistryFacade(target, scope, options) {
1590
- const rules = options.registrations ?? defaultRegistrationRules();
1591
- const byMethod = new Map(rules.map((rule) => [rule.method, rule]));
1592
- const unregisterMethods = new Set(
1593
- rules.map((rule) => rule.unregisterMethod).filter((method) => Boolean(method))
1594
- );
1595
- const ownedRegistrations = /* @__PURE__ */ new Set();
1596
- const objectTarget = target;
1597
- const remember = (rule, id, result, args) => {
1598
- const unregisterMethod = rule.unregisterMethod;
1599
- if (typeof result !== "function" && (!unregisterMethod || id === void 0)) return result;
1600
- const key = rule.method + ":" + (id ?? "returned");
1601
- let active = true;
1602
- let removeScopeRevoke = () => void 0;
1603
- let removeScopeCleanup = () => void 0;
1604
- const entry = {};
1605
- const clearOwnership = (removeDispose = true) => {
1606
- if (!active) return false;
1607
- active = false;
1608
- ownedRegistrations.delete(entry);
1609
- removeScopeRevoke();
1610
- if (removeDispose) removeScopeCleanup();
1611
- return true;
1612
- };
1613
- const invokeUnregister = (...offArgs) => {
1614
- if (typeof result === "function") {
1615
- return result.apply(target, offArgs);
1616
- }
1617
- const unregister = objectTarget[unregisterMethod];
1618
- if (typeof unregister !== "function") return void 0;
1619
- const unregisterArgument = rule.unregisterArgument ?? rule.idArgument ?? 0;
1620
- const unregisterArgs = [...args];
1621
- unregisterArgs[unregisterArgument] = id;
1622
- return unregister.apply(target, unregisterArgs);
1623
- };
1624
- const cleanup = async (_reason) => {
1625
- if (!active) {
1626
- if (entry.revokedCleanup) await entry.revokedCleanup;
1627
- return;
1628
- }
1629
- if (!clearOwnership()) return;
1630
- await invokeUnregister();
1631
- };
1632
- const revokeNow = (_reason) => {
1633
- if (!clearOwnership(false)) return;
1634
- try {
1635
- const pending = invokeUnregister();
1636
- if (pending && typeof pending.then === "function") {
1637
- const revokedCleanup = Promise.resolve(pending).then(() => void 0);
1638
- entry.revokedCleanup = revokedCleanup;
1639
- revokedCleanup.catch(() => void 0);
1640
- }
1641
- } catch (error) {
1642
- entry.revokedCleanup = Promise.reject(error);
1643
- entry.revokedCleanup.catch(() => void 0);
1644
- }
1645
- };
1646
- entry.id = id;
1647
- entry.unregisterMethod = unregisterMethod;
1648
- entry.revokeNow = revokeNow;
1649
- Object.defineProperty(entry, "active", {
1650
- enumerable: true,
1651
- configurable: false,
1652
- get: () => active,
1653
- set: (value) => {
1654
- active = value;
1655
- }
1656
- });
1657
- entry.removeScopeCleanup = removeScopeCleanup;
1658
- ownedRegistrations.add(entry);
1659
- removeScopeCleanup = scope.onDispose(
1660
- cleanup,
1661
- options.name + ":" + key,
1662
- "after-teardown"
1663
- );
1664
- entry.removeScopeCleanup = removeScopeCleanup;
1665
- removeScopeRevoke = scope.onRevoke(revokeNow);
1666
- if (typeof result === "function") {
1667
- return (...offArgs) => {
1668
- if (!clearOwnership()) return void 0;
1669
- return result.apply(target, offArgs);
1670
- };
1671
- }
1672
- return result;
1673
- };
1674
- return new Proxy(target, {
1675
- get(current, property, receiver) {
1676
- const value = Reflect.get(current, property, receiver);
1677
- if (typeof value !== "function") return value;
1678
- const rule = typeof property === "string" ? byMethod.get(property) : void 0;
1679
- if (rule) {
1680
- return (...args) => {
1681
- scope.assertActive();
1682
- const callArgs = [...args];
1683
- if (rule.bindPluginIdArgument !== void 0 && scope.identity.pluginId) {
1684
- const claimedPluginId = callArgs[rule.bindPluginIdArgument];
1685
- if (claimedPluginId !== void 0 && claimedPluginId !== scope.identity.pluginId) {
1686
- throw new Error(
1687
- 'Registry owner "' + String(claimedPluginId) + '" does not match plugin instance "' + scope.identity.pluginId + '"'
1688
- );
1689
- }
1690
- callArgs[rule.bindPluginIdArgument] = scope.identity.pluginId;
1691
- }
1692
- if (rule.ownerPluginIdProperty && scope.identity.pluginId) {
1693
- const definition = callArgs[rule.idArgument ?? 0];
1694
- if (definition && typeof definition === "object") {
1695
- const claimedPluginId = definition[rule.ownerPluginIdProperty];
1696
- if (claimedPluginId !== void 0 && claimedPluginId !== scope.identity.pluginId) {
1697
- throw new Error(
1698
- 'Registry owner "' + String(claimedPluginId) + '" does not match plugin instance "' + scope.identity.pluginId + '"'
1699
- );
1700
- }
1701
- }
1702
- }
1703
- const result = value.apply(target, callArgs);
1704
- const id = rule.idArgument === void 0 ? void 0 : definitionId(callArgs[rule.idArgument]);
1705
- return remember(rule, id, result, callArgs);
1706
- };
1707
- }
1708
- if (typeof property === "string" && unregisterMethods.has(property)) {
1709
- return (...args) => {
1710
- scope.assertActive();
1711
- const id = definitionId(args[0]) ?? (typeof args[0] === "string" ? args[0] : void 0);
1712
- const owned = [...ownedRegistrations].find(
1713
- (entry) => entry.active && entry.id === id && entry.unregisterMethod === property
1714
- );
1715
- if (!owned) {
1716
- throw new Error(
1717
- 'Registry resource "' + (id ?? "unknown") + '" is not owned by this plugin instance'
1718
- );
1719
- }
1720
- const result = value.apply(target, args);
1721
- owned.active = false;
1722
- ownedRegistrations.delete(owned);
1723
- owned.removeScopeCleanup();
1724
- return result;
1725
- };
1726
- }
1727
- return value.bind(target);
1728
- }
1729
- });
1730
- }
1731
-
1732
- // src/lifecycle/upgradeGate.ts
1733
- function validGeneration(value) {
1734
- return Number.isSafeInteger(value) && value >= 0;
1735
- }
1736
- function uniqueStrings(values) {
1737
- return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
1738
- }
1739
- function makeSessionId() {
1740
- try {
1741
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1742
- return `upgrade-session:${crypto.randomUUID()}`;
1743
- }
1744
- } catch {
1745
- }
1746
- return `upgrade-session:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
1747
- }
1748
- function buildIsCompatible(options, buildId) {
1749
- try {
1750
- if (options.isBuildCompatible) return options.isBuildCompatible(buildId);
1751
- } catch {
1752
- return false;
1753
- }
1754
- return buildId === options.buildId || options.compatibleBuildIds?.has(buildId) === true;
1755
- }
1756
- function createUpgradeGate(options) {
1757
- if (!options.protocolVersion || !options.buildId || !options.authorityInstanceId) {
1758
- throw new Error("\u5347\u7EA7\u95E8\u7981\u7684 protocolVersion\u3001buildId \u548C authorityInstanceId \u5FC5\u987B\u6709\u6548");
1759
- }
1760
- if (!validGeneration(options.handoverGeneration)) {
1761
- throw new Error("\u5347\u7EA7\u95E8\u7981\u7684 handoverGeneration \u5FC5\u987B\u662F\u975E\u8D1F\u5B89\u5168\u6574\u6570");
1762
- }
1763
- const contractVersions = uniqueStrings(options.supportedContractVersions);
1764
- if (contractVersions.length === 0) {
1765
- throw new Error("\u5347\u7EA7\u95E8\u7981\u81F3\u5C11\u9700\u8981\u4E00\u4E2A supportedContractVersions");
1766
- }
1767
- let currentState = "active";
1768
- let closeReason = "upgrade gate closed";
1769
- const leases = /* @__PURE__ */ new Set();
1770
- const sessions = /* @__PURE__ */ new Set();
1771
- const sessionByObject = /* @__PURE__ */ new WeakMap();
1772
- const emptyWaiters = /* @__PURE__ */ new Set();
1773
- const notifyEmpty = () => {
1774
- if (leases.size !== 0) return;
1775
- for (const resolve of [...emptyWaiters]) {
1776
- emptyWaiters.delete(resolve);
1777
- resolve();
1778
- }
1779
- };
1780
- const removeLease = (record) => {
1781
- if (!leases.delete(record)) return;
1782
- record.session.leases.delete(record);
1783
- record.removeExternalAbort?.();
1784
- record.removeExternalAbort = void 0;
1785
- notifyEmpty();
1786
- };
1787
- const revokeSession = (record, reason) => {
1788
- if (record.revoked) return;
1789
- record.revoked = true;
1790
- record.reason = reason;
1791
- try {
1792
- record.controller.abort(new UpgradeGateRejectedError(reason));
1793
- } catch {
1794
- record.controller.abort();
1795
- }
1796
- for (const lease of [...record.leases]) revokeLease(lease, reason);
1797
- sessions.delete(record);
1798
- };
1799
- const revokeLease = (record, reason) => {
1800
- if (record.revoked) return;
1801
- record.revoked = true;
1802
- record.reason = reason;
1803
- try {
1804
- record.controller.abort(new UpgradeGateRejectedError(reason));
1805
- } catch {
1806
- record.controller.abort();
1807
- }
1808
- removeLease(record);
1809
- };
1810
- const assertAccepting = () => {
1811
- if (currentState === "active") return;
1812
- throw new UpgradeGateRejectedError(currentState === "draining" ? "draining" : "closed", closeReason);
1813
- };
1814
- const handshake = (input) => {
1815
- if (currentState !== "active") {
1816
- return { accepted: false, reason: currentState === "draining" ? "draining" : "closed" };
1817
- }
1818
- if (typeof input.connectionId !== "string" || input.connectionId.length === 0 || input.protocolVersion !== options.protocolVersion || typeof input.protocolVersion !== "string" || typeof input.buildId !== "string" || typeof input.authorityInstanceId !== "string" || input.authorityInstanceId.length === 0) {
1819
- return { accepted: false, reason: "protocol-mismatch" };
1820
- }
1821
- if (!buildIsCompatible(options, input.buildId)) {
1822
- return { accepted: false, reason: "build-incompatible" };
1823
- }
1824
- if (!validGeneration(input.handoverGeneration)) {
1825
- return { accepted: false, reason: "stale-generation" };
1826
- }
1827
- if (input.handoverGeneration < options.handoverGeneration) {
1828
- return { accepted: false, reason: "stale-generation" };
1829
- }
1830
- if (input.handoverGeneration > options.handoverGeneration) {
1831
- return { accepted: false, reason: "future-generation" };
1832
- }
1833
- const contractVersion = contractVersions.find((version) => input.supportedContractVersions.includes(version));
1834
- if (!contractVersion) return { accepted: false, reason: "contract-mismatch" };
1835
- const sessionRecord = {
1836
- sessionId: makeSessionId(),
1837
- connectionId: input.connectionId,
1838
- contractVersion,
1839
- controller: new AbortController(),
1840
- revoked: false,
1841
- reason: "upgrade session closed",
1842
- leases: /* @__PURE__ */ new Set()
1843
- };
1844
- const session = {
1845
- connectionId: sessionRecord.connectionId,
1846
- sessionId: sessionRecord.sessionId,
1847
- authorityInstanceId: options.authorityInstanceId,
1848
- handoverGeneration: options.handoverGeneration,
1849
- contractVersion,
1850
- get revoked() {
1851
- return sessionRecord.revoked || !sessions.has(sessionRecord);
1852
- },
1853
- signal: sessionRecord.controller.signal,
1854
- assertActive() {
1855
- if (sessionRecord.revoked || !sessions.has(sessionRecord)) {
1856
- throw new UpgradeGateRejectedError(sessionRecord.reason, "\u5347\u7EA7\u63E1\u624B\u4F1A\u8BDD\u5DF2\u5931\u6548");
1857
- }
1858
- if (currentState === "closed") {
1859
- throw new UpgradeGateRejectedError("closed", closeReason);
1860
- }
1861
- },
1862
- admit(input2) {
1863
- return admitForSession(sessionRecord, input2);
1864
- },
1865
- close(reason = "upgrade session closed") {
1866
- revokeSession(sessionRecord, reason);
1867
- }
1868
- };
1869
- sessionRecord.session = session;
1870
- sessions.add(sessionRecord);
1871
- sessionByObject.set(session, sessionRecord);
1872
- return {
1873
- accepted: true,
1874
- mode: options.mode ?? "cold-switch",
1875
- handoverGeneration: options.handoverGeneration,
1876
- contractVersion,
1877
- connectionId: sessionRecord.connectionId,
1878
- sessionId: sessionRecord.sessionId,
1879
- session
1880
- };
1881
- };
1882
- const admitForSession = (sessionRecord, input) => {
1883
- if (sessionRecord.revoked || !sessions.has(sessionRecord)) {
1884
- throw new UpgradeGateRejectedError(sessionRecord.reason, "\u5347\u7EA7\u63E1\u624B\u4F1A\u8BDD\u5DF2\u5931\u6548");
1885
- }
1886
- assertAccepting();
1887
- if (input.signal?.aborted) {
1888
- throw new UpgradeGateRejectedError("caller-aborted", "I/O \u5728\u53D6\u5F97\u63A5\u7BA1\u79DF\u7EA6\u524D\u5DF2\u53D6\u6D88");
1889
- }
1890
- const record = {
1891
- session: sessionRecord,
1892
- operation: input.operation,
1893
- controller: new AbortController(),
1894
- revoked: false,
1895
- reason: "upgrade I/O lease released"
1896
- };
1897
- leases.add(record);
1898
- sessionRecord.leases.add(record);
1899
- if (input.signal) {
1900
- const onAbort = () => revokeLease(record, "caller-aborted");
1901
- input.signal.addEventListener("abort", onAbort, { once: true });
1902
- record.removeExternalAbort = () => input.signal?.removeEventListener("abort", onAbort);
1903
- }
1904
- const lease = {
1905
- connectionId: sessionRecord.connectionId,
1906
- sessionId: sessionRecord.sessionId,
1907
- authorityInstanceId: options.authorityInstanceId,
1908
- handoverGeneration: options.handoverGeneration,
1909
- contractVersion: sessionRecord.contractVersion,
1910
- operation: record.operation,
1911
- get revoked() {
1912
- return record.revoked || !leases.has(record);
1913
- },
1914
- signal: record.controller.signal,
1915
- assertActive() {
1916
- if (record.revoked || !leases.has(record)) {
1917
- throw new UpgradeGateRejectedError(record.reason, "\u5347\u7EA7 I/O \u79DF\u7EA6\u5DF2\u5931\u6548");
1918
- }
1919
- if (currentState === "closed") {
1920
- throw new UpgradeGateRejectedError("closed", closeReason);
1921
- }
1922
- },
1923
- release() {
1924
- if (record.revoked) return;
1925
- record.revoked = true;
1926
- record.reason = "upgrade I/O lease released";
1927
- removeLease(record);
1928
- }
1929
- };
1930
- return lease;
1931
- };
1932
- const admit = (input) => {
1933
- const sessionRecord = sessionByObject.get(input.session);
1934
- if (!sessionRecord) {
1935
- throw new UpgradeGateRejectedError("invalid-session", "I/O \u5FC5\u987B\u4F7F\u7528\u5F53\u524D\u95E8\u7981\u63E1\u624B\u8FD4\u56DE\u7684\u4F1A\u8BDD");
1936
- }
1937
- return admitForSession(sessionRecord, input);
1938
- };
1939
- const beginDrain = (reason = "upgrade handover draining") => {
1940
- if (currentState !== "active") return;
1941
- closeReason = reason;
1942
- currentState = "draining";
1943
- };
1944
- const drain = async (timeoutMs) => {
1945
- beginDrain();
1946
- if (leases.size === 0) return { state: currentState, drained: true, pending: 0 };
1947
- if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
1948
- throw new Error("\u5347\u7EA7\u6392\u7A7A timeoutMs \u5FC5\u987B\u662F\u975E\u8D1F\u6709\u9650\u6570");
1949
- }
1950
- let timeout;
1951
- let timerResolve;
1952
- const empty = new Promise((resolve) => {
1953
- emptyWaiters.add(resolve);
1954
- timerResolve = resolve;
1955
- });
1956
- const timeoutPromise = timeoutMs === void 0 ? void 0 : new Promise((resolve) => {
1957
- timeout = setTimeout(resolve, timeoutMs);
1958
- });
1959
- if (timeoutPromise) await Promise.race([empty, timeoutPromise]);
1960
- else await empty;
1961
- if (timeout !== void 0) clearTimeout(timeout);
1962
- if (timerResolve) emptyWaiters.delete(timerResolve);
1963
- const pending = leases.size;
1964
- return { state: currentState, drained: pending === 0, pending };
1965
- };
1966
- const close = (reason = "upgrade gate closed") => {
1967
- if (currentState === "closed") return;
1968
- closeReason = reason;
1969
- currentState = "closed";
1970
- for (const session of [...sessions]) revokeSession(session, reason);
1971
- for (const lease of [...leases]) revokeLease(lease, reason);
1972
- notifyEmpty();
1973
- };
1974
- return {
1975
- get state() {
1976
- return currentState;
1977
- },
1978
- mode: options.mode ?? "cold-switch",
1979
- authorityInstanceId: options.authorityInstanceId,
1980
- handoverGeneration: options.handoverGeneration,
1981
- handshake,
1982
- assertAccepting,
1983
- admit,
1984
- beginDrain,
1985
- drain,
1986
- close,
1987
- activeIo: () => leases.size
1988
- };
1989
- }
1990
-
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 };
62
+ export { RUNTIME_MESSAGE_BUS, definePlugin };
1992
63
  //# sourceMappingURL=index.js.map
1993
64
  //# sourceMappingURL=index.js.map