webloom-framework 0.3.0 → 0.4.1
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/README.md +70 -42
- package/dist/advanced.d.ts +168 -0
- package/dist/advanced.js +441 -0
- package/dist/advanced.js.map +1 -0
- package/dist/chunk-4DSINPZD.js +158 -0
- package/dist/chunk-4DSINPZD.js.map +1 -0
- package/dist/chunk-ANA6GBEI.js +1737 -0
- package/dist/chunk-ANA6GBEI.js.map +1 -0
- package/dist/chunk-HJKPKWI7.js +4024 -0
- package/dist/chunk-HJKPKWI7.js.map +1 -0
- package/dist/chunk-SX46RHDI.js +433 -0
- package/dist/chunk-SX46RHDI.js.map +1 -0
- package/dist/index.d.ts +39 -259
- package/dist/index.js +38 -1359
- package/dist/index.js.map +1 -1
- package/dist/messageBus-CtrwkjrO.d.ts +5 -0
- package/dist/messagePortServiceTransport-BYprNvQY.d.ts +264 -0
- package/dist/react.d.ts +41 -28
- package/dist/react.js +79 -88
- package/dist/react.js.map +1 -1
- package/dist/runtimeTypes-DquUCHz-.d.ts +1640 -0
- package/dist/sharedWorkerHost-KI7TIGdX.d.ts +192 -0
- package/dist/testing.d.ts +18 -17
- package/dist/testing.js +23 -15
- package/dist/testing.js.map +1 -1
- package/dist/windowRuntime-BKkLPsAS.d.ts +23 -0
- package/docs/api.md +148 -111
- package/docs/proposals/webloom-v4/implementation-plan.md +443 -0
- package/docs/proposals/webloom-v4/requirements.md +555 -0
- package/docs/proposals/webloom-v4/verification.md +115 -0
- package/package.json +11 -2
- package/dist/chunk-76BGPI6M.js +0 -4261
- package/dist/chunk-76BGPI6M.js.map +0 -1
- package/dist/createPluginHost-BVsUCDKN.d.ts +0 -1318
- package/dist/resourceRegistry-BFnjmeGE.d.ts +0 -267
|
@@ -0,0 +1,4024 @@
|
|
|
1
|
+
// src/contracts/capability.ts
|
|
2
|
+
function assertIdentity(input) {
|
|
3
|
+
if (typeof input.id !== "string" || input.id.trim() === "") {
|
|
4
|
+
throw new TypeError("Capability id must be a non-empty string");
|
|
5
|
+
}
|
|
6
|
+
if (typeof input.version !== "string" || input.version.trim() === "") {
|
|
7
|
+
throw new TypeError(`Capability "${input.id}" version must be a non-empty string`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function assertParser(value, label) {
|
|
11
|
+
if (!value || typeof value !== "object" || typeof value.parse !== "function") {
|
|
12
|
+
throw new TypeError(`${label} must expose parse(value)`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function defineCapability(options) {
|
|
16
|
+
assertIdentity(options);
|
|
17
|
+
if (options.kind === "rpc") {
|
|
18
|
+
assertParser(options.request, "RPC request parser");
|
|
19
|
+
assertParser(options.response, "RPC response parser");
|
|
20
|
+
}
|
|
21
|
+
if (options.kind === "stream") {
|
|
22
|
+
assertParser(options.request, "stream request parser");
|
|
23
|
+
assertParser(options.item, "stream item parser");
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze({ ...options });
|
|
26
|
+
}
|
|
27
|
+
function capabilityDescriptor(capability) {
|
|
28
|
+
assertCapability(capability);
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
kind: capability.kind,
|
|
31
|
+
id: capability.id,
|
|
32
|
+
version: capability.version
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function capabilityKey(capability) {
|
|
36
|
+
return `${capability.kind}\0${capability.id}\0${capability.version}`;
|
|
37
|
+
}
|
|
38
|
+
function isCapabilityDescriptor(value) {
|
|
39
|
+
if (!value || typeof value !== "object") return false;
|
|
40
|
+
const candidate = value;
|
|
41
|
+
return (candidate.kind === "local" || candidate.kind === "rpc" || candidate.kind === "stream") && typeof candidate.id === "string" && candidate.id.trim() !== "" && typeof candidate.version === "string" && candidate.version.trim() !== "";
|
|
42
|
+
}
|
|
43
|
+
function isCapability(value) {
|
|
44
|
+
if (!isCapabilityDescriptor(value)) return false;
|
|
45
|
+
if (value.kind === "local") return true;
|
|
46
|
+
if (value.kind === "rpc") {
|
|
47
|
+
const candidate2 = value;
|
|
48
|
+
return !!candidate2.request && typeof candidate2.request === "object" && typeof candidate2.request.parse === "function" && !!candidate2.response && typeof candidate2.response === "object" && typeof candidate2.response.parse === "function";
|
|
49
|
+
}
|
|
50
|
+
const candidate = value;
|
|
51
|
+
return !!candidate.request && typeof candidate.request === "object" && typeof candidate.request.parse === "function" && !!candidate.item && typeof candidate.item === "object" && typeof candidate.item.parse === "function";
|
|
52
|
+
}
|
|
53
|
+
function assertCapability(value) {
|
|
54
|
+
if (!isCapability(value)) throw new TypeError("Invalid WebLoom capability definition");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/contracts/lifecycle.ts
|
|
58
|
+
var LifecycleScopeRevokedError = class extends Error {
|
|
59
|
+
code = "lifecycle.scope_revoked";
|
|
60
|
+
constructor(message2 = "Lifecycle scope has been revoked") {
|
|
61
|
+
super(message2);
|
|
62
|
+
this.name = "LifecycleScopeRevokedError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var PermissionLeaseRevokedError = class extends Error {
|
|
66
|
+
code = "permission.lease_revoked";
|
|
67
|
+
constructor(message2 = "Permission lease has been revoked") {
|
|
68
|
+
super(message2);
|
|
69
|
+
this.name = "PermissionLeaseRevokedError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var PermissionDeniedError = class extends Error {
|
|
73
|
+
code = "permission.denied";
|
|
74
|
+
permission;
|
|
75
|
+
constructor(permission, message2 = `Permission denied: ${permission}`) {
|
|
76
|
+
super(message2);
|
|
77
|
+
this.name = "PermissionDeniedError";
|
|
78
|
+
this.permission = permission;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var WebLoomError = class extends Error {
|
|
82
|
+
code;
|
|
83
|
+
phase;
|
|
84
|
+
context;
|
|
85
|
+
details;
|
|
86
|
+
constructor(code, message2, phase2 = "execute", context, details) {
|
|
87
|
+
super(message2);
|
|
88
|
+
this.name = "WebLoomError";
|
|
89
|
+
this.code = code;
|
|
90
|
+
this.phase = phase2;
|
|
91
|
+
this.context = context ? Object.freeze({ ...context }) : void 0;
|
|
92
|
+
this.details = details ? Object.freeze({ ...details }) : void 0;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var UpgradeGateRejectedError = class extends Error {
|
|
96
|
+
code = "upgrade.gate_rejected";
|
|
97
|
+
reason;
|
|
98
|
+
constructor(reason, message2 = `Upgrade gate rejected operation: ${reason}`) {
|
|
99
|
+
super(message2);
|
|
100
|
+
this.name = "UpgradeGateRejectedError";
|
|
101
|
+
this.reason = reason;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var SCOPED_TASK_SCHEDULER_CAPABILITY = "runtime.task-scheduler";
|
|
105
|
+
var LIFECYCLE_ERROR_TEXT = Object.freeze({
|
|
106
|
+
"lifecycle.scope_revoked": "\u8FD0\u884C\u5B9E\u4F8B\u5DF2\u505C\u6B62",
|
|
107
|
+
"permission.denied": "\u63D2\u4EF6\u6CA1\u6709\u83B7\u5F97\u8BE5\u64CD\u4F5C\u7684\u6743\u9650",
|
|
108
|
+
"permission.lease_revoked": "\u6388\u6743\u79DF\u7EA6\u5DF2\u64A4\u9500",
|
|
109
|
+
"lifecycle.cleanup_failed": "\u8D44\u6E90\u6E05\u7406\u5931\u8D25\uFF0C\u7B49\u5F85\u91CD\u8BD5",
|
|
110
|
+
"lifecycle.cleanup_timeout": "\u8D44\u6E90\u6E05\u7406\u8D85\u65F6\uFF0C\u4ECD\u5728\u540E\u53F0\u6392\u7A7A",
|
|
111
|
+
"upgrade.gate_rejected": "\u7248\u672C\u63A5\u7BA1\u95E8\u7981\u672A\u901A\u8FC7"
|
|
112
|
+
});
|
|
113
|
+
function lifecycleErrorText(code) {
|
|
114
|
+
return LIFECYCLE_ERROR_TEXT[code] ?? "\u751F\u547D\u5468\u671F\u64CD\u4F5C\u672A\u5B8C\u6210";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/contracts/resource.ts
|
|
118
|
+
var RESOURCE_OWNER = /* @__PURE__ */ Symbol("webloom.resource.owner");
|
|
119
|
+
var RESOURCE_REGISTRY = defineCapability({
|
|
120
|
+
kind: "local",
|
|
121
|
+
id: "webloom.resource.registry",
|
|
122
|
+
version: "1"
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// src/transport/dto.ts
|
|
126
|
+
var DEFAULT_DTO_LIMITS = Object.freeze({
|
|
127
|
+
maxDepth: 32,
|
|
128
|
+
maxNodes: 1e4,
|
|
129
|
+
maxEdges: 2e4,
|
|
130
|
+
maxBudgetBytes: 16 * 1024 * 1024,
|
|
131
|
+
maxStringLength: 1048576,
|
|
132
|
+
maxFieldNameLength: 256
|
|
133
|
+
});
|
|
134
|
+
var ATTRIBUTES_DTO_LIMITS = Object.freeze({
|
|
135
|
+
maxDepth: 8,
|
|
136
|
+
maxNodes: 256,
|
|
137
|
+
maxEdges: 1024,
|
|
138
|
+
maxBudgetBytes: 16 * 1024,
|
|
139
|
+
maxStringLength: 2048,
|
|
140
|
+
maxFieldNameLength: 128
|
|
141
|
+
});
|
|
142
|
+
var DEFAULT_RUNTIME_LIMITS = Object.freeze({
|
|
143
|
+
maxPeers: 32,
|
|
144
|
+
maxPendingCallsPerPeer: 64,
|
|
145
|
+
maxPendingCallsPerRuntime: 512,
|
|
146
|
+
maxActiveStreamsPerPeer: 16,
|
|
147
|
+
maxActiveStreamsPerRuntime: 128,
|
|
148
|
+
maxExecutionSlotsPerPeer: 64,
|
|
149
|
+
maxExecutionSlotsPerRuntime: 512,
|
|
150
|
+
maxMessageBudgetBytes: DEFAULT_DTO_LIMITS.maxBudgetBytes,
|
|
151
|
+
maxRetainedPayloadBytesPerPeer: 64 * 1024 * 1024,
|
|
152
|
+
maxRetainedPayloadBytesPerRuntime: 256 * 1024 * 1024,
|
|
153
|
+
maxSnapshotUnits: 512,
|
|
154
|
+
maxSnapshotServices: 1024,
|
|
155
|
+
maxDtoDepth: DEFAULT_DTO_LIMITS.maxDepth,
|
|
156
|
+
maxDtoNodes: DEFAULT_DTO_LIMITS.maxNodes,
|
|
157
|
+
maxDtoEdges: DEFAULT_DTO_LIMITS.maxEdges,
|
|
158
|
+
maxTransferEntries: 64,
|
|
159
|
+
maxTransfers: 32,
|
|
160
|
+
maxMessagePorts: 8,
|
|
161
|
+
maxStreamCredit: 256
|
|
162
|
+
});
|
|
163
|
+
function createRuntimeBudget(limits) {
|
|
164
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
165
|
+
const budget = {
|
|
166
|
+
limits: normalizeRuntimeLimits(limits),
|
|
167
|
+
pendingCalls: 0,
|
|
168
|
+
activeStreams: 0,
|
|
169
|
+
executionSlots: 0,
|
|
170
|
+
retainedPayloadBytes: 0,
|
|
171
|
+
registerExecutionWaiter(waiter) {
|
|
172
|
+
if (typeof waiter !== "function") throw new TypeError("Runtime execution waiter must be a function");
|
|
173
|
+
waiters.add(waiter);
|
|
174
|
+
let registered = true;
|
|
175
|
+
return () => {
|
|
176
|
+
if (!registered) return;
|
|
177
|
+
registered = false;
|
|
178
|
+
waiters.delete(waiter);
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
releaseExecutionSlot() {
|
|
182
|
+
budget.executionSlots = Math.max(0, budget.executionSlots - 1);
|
|
183
|
+
const waiter = waiters.values().next().value;
|
|
184
|
+
if (waiter === void 0) return;
|
|
185
|
+
waiters.delete(waiter);
|
|
186
|
+
queueMicrotask(() => {
|
|
187
|
+
try {
|
|
188
|
+
waiter();
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
return budget;
|
|
195
|
+
}
|
|
196
|
+
function normalizeRuntimeLimits(input) {
|
|
197
|
+
const result = { ...DEFAULT_RUNTIME_LIMITS, ...input ?? {} };
|
|
198
|
+
for (const [key, value] of Object.entries(result)) {
|
|
199
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
200
|
+
throw new TypeError(`Runtime limit ${key} must be a finite positive integer`);
|
|
201
|
+
}
|
|
202
|
+
const defaultValue = DEFAULT_RUNTIME_LIMITS[key];
|
|
203
|
+
if (value > defaultValue) throw new TypeError(`Runtime limit ${key} cannot exceed the v4 default budget`);
|
|
204
|
+
}
|
|
205
|
+
if (result.maxMessagePorts > result.maxTransfers) throw new TypeError("maxMessagePorts cannot exceed maxTransfers");
|
|
206
|
+
if (result.maxTransferEntries < result.maxTransfers) throw new TypeError("maxTransferEntries cannot be less than maxTransfers");
|
|
207
|
+
if (result.maxStreamCredit > 256) throw new TypeError("maxStreamCredit cannot exceed 256");
|
|
208
|
+
return Object.freeze(result);
|
|
209
|
+
}
|
|
210
|
+
function isObject(value) {
|
|
211
|
+
return value !== null && typeof value === "object";
|
|
212
|
+
}
|
|
213
|
+
function isMessagePort(value) {
|
|
214
|
+
if (!isObject(value)) return false;
|
|
215
|
+
try {
|
|
216
|
+
if (Object.prototype.toString.call(value) === "[object MessagePort]") return true;
|
|
217
|
+
const candidate = value;
|
|
218
|
+
return typeof candidate.postMessage === "function" && typeof candidate.start === "function" && typeof candidate.close === "function" && typeof candidate.addEventListener === "function" && typeof candidate.removeEventListener === "function";
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function isArrayBuffer(value) {
|
|
224
|
+
if (!isObject(value)) return false;
|
|
225
|
+
try {
|
|
226
|
+
return Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function isSharedArrayBuffer(value) {
|
|
232
|
+
if (!isObject(value)) return false;
|
|
233
|
+
try {
|
|
234
|
+
return Object.prototype.toString.call(value) === "[object SharedArrayBuffer]";
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function isBlob(value) {
|
|
240
|
+
if (!isObject(value)) return false;
|
|
241
|
+
try {
|
|
242
|
+
const tag = Object.prototype.toString.call(value);
|
|
243
|
+
return tag === "[object Blob]" || tag === "[object File]";
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function isDate(value) {
|
|
249
|
+
if (!isObject(value)) return false;
|
|
250
|
+
try {
|
|
251
|
+
return Object.prototype.toString.call(value) === "[object Date]";
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function isSupportedTransfer(value) {
|
|
257
|
+
return isArrayBuffer(value) || isMessagePort(value);
|
|
258
|
+
}
|
|
259
|
+
function ownKeys(value) {
|
|
260
|
+
try {
|
|
261
|
+
return Reflect.ownKeys(value);
|
|
262
|
+
} catch {
|
|
263
|
+
throw new Error("DTO property inspection failed");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function rejectCustomKeys(value) {
|
|
267
|
+
for (const key of ownKeys(value)) throw new Error(`Unsupported custom property ${String(key)}`);
|
|
268
|
+
}
|
|
269
|
+
function descriptorValue(value, key, label) {
|
|
270
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
271
|
+
if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) throw new Error(`${label} must contain enumerable data properties`);
|
|
272
|
+
return descriptor.value;
|
|
273
|
+
}
|
|
274
|
+
function arrayIndex(key) {
|
|
275
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(key)) return void 0;
|
|
276
|
+
const index = Number(key);
|
|
277
|
+
return Number.isSafeInteger(index) && index >= 0 ? index : void 0;
|
|
278
|
+
}
|
|
279
|
+
function isTypedArray(value) {
|
|
280
|
+
try {
|
|
281
|
+
return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) !== "[object DataView]";
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function assertFixedBuffer(value) {
|
|
287
|
+
if (isSharedArrayBuffer(value)) throw new Error("SharedArrayBuffer is not supported");
|
|
288
|
+
try {
|
|
289
|
+
if (value.resizable === true) throw new Error("Resizable ArrayBuffer is not supported");
|
|
290
|
+
ArrayBuffer.prototype.slice.call(value, 0, 0);
|
|
291
|
+
const length = value.byteLength;
|
|
292
|
+
if (!Number.isSafeInteger(length) || length < 0) throw new Error("Invalid ArrayBuffer length");
|
|
293
|
+
rejectCustomKeys(value);
|
|
294
|
+
return length;
|
|
295
|
+
} catch (error2) {
|
|
296
|
+
if (error2 instanceof Error && error2.message.includes("not supported")) throw error2;
|
|
297
|
+
throw new Error("Detached or invalid ArrayBuffer");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function assertNoCustomViewProperties(value, length) {
|
|
301
|
+
for (const key of ownKeys(value)) {
|
|
302
|
+
if (typeof key !== "string") throw new Error("TypedArray symbols are not supported");
|
|
303
|
+
const index = arrayIndex(key);
|
|
304
|
+
if (index === void 0 || index >= length) throw new Error("TypedArray has unsupported custom properties");
|
|
305
|
+
descriptorValue(value, key, "TypedArray");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function error(code, message2, phase2 = "validate") {
|
|
309
|
+
throw new WebLoomError(code, message2, phase2 ?? "validate");
|
|
310
|
+
}
|
|
311
|
+
function validateDto(value, options = {}) {
|
|
312
|
+
const limits = { ...DEFAULT_DTO_LIMITS, ...options.limits ?? {} };
|
|
313
|
+
const transferables = options.transferables ?? /* @__PURE__ */ new Set();
|
|
314
|
+
const messagePorts = [];
|
|
315
|
+
const reachableTransferables = [];
|
|
316
|
+
const seen = /* @__PURE__ */ new Map();
|
|
317
|
+
const active = /* @__PURE__ */ new Set();
|
|
318
|
+
let nodes = 0;
|
|
319
|
+
let edges = 0;
|
|
320
|
+
let budgetBytes = 0;
|
|
321
|
+
const charge = (amount) => {
|
|
322
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || budgetBytes > limits.maxBudgetBytes - amount) error("resource_limit_exceeded", "DTO budget exceeded", options.phase);
|
|
323
|
+
budgetBytes += amount;
|
|
324
|
+
};
|
|
325
|
+
const addEdge = () => {
|
|
326
|
+
edges += 1;
|
|
327
|
+
if (edges > limits.maxEdges) error("resource_limit_exceeded", "DTO edge limit exceeded", options.phase);
|
|
328
|
+
charge(16);
|
|
329
|
+
};
|
|
330
|
+
const addNode = () => {
|
|
331
|
+
nodes += 1;
|
|
332
|
+
if (nodes > limits.maxNodes) error("resource_limit_exceeded", "DTO node limit exceeded", options.phase);
|
|
333
|
+
charge(32);
|
|
334
|
+
};
|
|
335
|
+
const child = (childValue, depth) => {
|
|
336
|
+
addEdge();
|
|
337
|
+
return walk(childValue, depth);
|
|
338
|
+
};
|
|
339
|
+
const walk = (current, depth) => {
|
|
340
|
+
if (depth > limits.maxDepth) error("resource_limit_exceeded", "DTO depth limit exceeded", options.phase);
|
|
341
|
+
if (current === null || current === void 0) {
|
|
342
|
+
charge(8);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
switch (typeof current) {
|
|
346
|
+
case "boolean":
|
|
347
|
+
charge(8);
|
|
348
|
+
return 0;
|
|
349
|
+
case "string":
|
|
350
|
+
if (current.length > limits.maxStringLength) error("resource_limit_exceeded", "DTO string limit exceeded", options.phase);
|
|
351
|
+
charge(current.length * 2);
|
|
352
|
+
return 0;
|
|
353
|
+
case "number":
|
|
354
|
+
if (!Number.isFinite(current)) error("invalid_message", "DTO number must be finite", options.phase);
|
|
355
|
+
charge(8);
|
|
356
|
+
return 0;
|
|
357
|
+
case "bigint":
|
|
358
|
+
if (current < -(2n ** 63n) || current > 2n ** 63n - 1n) error("invalid_message", "DTO bigint must fit signed 64-bit", options.phase);
|
|
359
|
+
charge(8);
|
|
360
|
+
return 0;
|
|
361
|
+
case "function":
|
|
362
|
+
case "symbol":
|
|
363
|
+
error("invalid_message", "Unsupported DTO value", options.phase);
|
|
364
|
+
}
|
|
365
|
+
if (!isObject(current)) error("invalid_message", "Unsupported DTO value", options.phase);
|
|
366
|
+
if (active.has(current)) error("invalid_message", "DTO cycles are not supported", options.phase);
|
|
367
|
+
const known = seen.get(current);
|
|
368
|
+
if (known !== void 0) {
|
|
369
|
+
if (depth + known > limits.maxDepth) error("resource_limit_exceeded", "DTO depth limit exceeded", options.phase);
|
|
370
|
+
return known;
|
|
371
|
+
}
|
|
372
|
+
addNode();
|
|
373
|
+
active.add(current);
|
|
374
|
+
let subtreeDepth = 0;
|
|
375
|
+
try {
|
|
376
|
+
if (isMessagePort(current)) {
|
|
377
|
+
if (!options.allowUnlistedMessagePorts && !transferables.has(current)) error("invalid_message", "MessagePort was not declared by the capability", options.phase);
|
|
378
|
+
messagePorts.push(current);
|
|
379
|
+
reachableTransferables.push(current);
|
|
380
|
+
} else if (isSharedArrayBuffer(current)) {
|
|
381
|
+
error("invalid_message", "SharedArrayBuffer is not supported", options.phase);
|
|
382
|
+
} else if (isArrayBuffer(current)) {
|
|
383
|
+
reachableTransferables.push(current);
|
|
384
|
+
charge(assertFixedBuffer(current));
|
|
385
|
+
} else if (ArrayBuffer.isView(current)) {
|
|
386
|
+
const view = current;
|
|
387
|
+
if (isSharedArrayBuffer(view.buffer)) error("invalid_message", "SharedArrayBuffer is not supported", options.phase);
|
|
388
|
+
if (!isArrayBuffer(view.buffer)) error("invalid_message", "Unsupported view backing buffer", options.phase);
|
|
389
|
+
const byteLength = assertFixedBuffer(view.buffer);
|
|
390
|
+
if (!Number.isSafeInteger(view.byteLength) || !Number.isSafeInteger(view.byteOffset) || view.byteLength < 0 || view.byteOffset < 0 || view.byteOffset + view.byteLength > byteLength) error("invalid_message", "Invalid buffer view", options.phase);
|
|
391
|
+
if (isTypedArray(current)) {
|
|
392
|
+
const length = current.length;
|
|
393
|
+
if (!Number.isSafeInteger(length) || length < 0) error("invalid_message", "Invalid TypedArray length", options.phase);
|
|
394
|
+
assertNoCustomViewProperties(current, length);
|
|
395
|
+
} else rejectCustomKeys(current);
|
|
396
|
+
reachableTransferables.push(view.buffer);
|
|
397
|
+
subtreeDepth = Math.max(subtreeDepth, 1 + child(view.buffer, depth + 1));
|
|
398
|
+
charge(32);
|
|
399
|
+
} else if (isDate(current)) {
|
|
400
|
+
rejectCustomKeys(current);
|
|
401
|
+
if (!Number.isFinite(current.getTime())) error("invalid_message", "Invalid Date", options.phase);
|
|
402
|
+
charge(16);
|
|
403
|
+
} else if (isBlob(current)) {
|
|
404
|
+
rejectCustomKeys(current);
|
|
405
|
+
const size = current.size;
|
|
406
|
+
const type = current.type;
|
|
407
|
+
if (!Number.isSafeInteger(size) || size < 0 || type.length > limits.maxStringLength) error("resource_limit_exceeded", "Blob metadata exceeds DTO budget", options.phase);
|
|
408
|
+
charge(size + type.length * 2);
|
|
409
|
+
if (Object.prototype.toString.call(current) === "[object File]") {
|
|
410
|
+
const name = current.name;
|
|
411
|
+
if (name.length > limits.maxStringLength) error("resource_limit_exceeded", "File name exceeds DTO budget", options.phase);
|
|
412
|
+
charge(name.length * 2);
|
|
413
|
+
}
|
|
414
|
+
} else if (Array.isArray(current)) {
|
|
415
|
+
const keys = ownKeys(current);
|
|
416
|
+
if (keys.some((key) => typeof key !== "string")) error("invalid_message", "Array symbols are not supported", options.phase);
|
|
417
|
+
const names = keys;
|
|
418
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(current, "length");
|
|
419
|
+
const length = lengthDescriptor?.value;
|
|
420
|
+
if (!lengthDescriptor || !Number.isSafeInteger(length) || length < 0 || names.length !== length + 1 || !names.includes("length")) error("invalid_message", "Arrays must be dense DTO arrays", options.phase);
|
|
421
|
+
for (let index = 0; index < length; index += 1) {
|
|
422
|
+
const name = String(index);
|
|
423
|
+
if (!names.includes(name)) error("invalid_message", "Arrays must be dense DTO arrays", options.phase);
|
|
424
|
+
const valueAtIndex = descriptorValue(current, name, "Array");
|
|
425
|
+
subtreeDepth = Math.max(subtreeDepth, 1 + child(valueAtIndex, depth + 1));
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
let prototype;
|
|
429
|
+
try {
|
|
430
|
+
prototype = Object.getPrototypeOf(current);
|
|
431
|
+
} catch {
|
|
432
|
+
error("invalid_message", "DTO prototype inspection failed", options.phase);
|
|
433
|
+
}
|
|
434
|
+
if (prototype !== Object.prototype && prototype !== null) error("invalid_message", "DTO records must use Object.prototype or null prototype", options.phase);
|
|
435
|
+
const keys = ownKeys(current);
|
|
436
|
+
for (const key of keys) {
|
|
437
|
+
if (typeof key !== "string") error("invalid_message", "DTO symbols are not supported", options.phase);
|
|
438
|
+
if (key.length > limits.maxFieldNameLength) error("resource_limit_exceeded", "DTO field name limit exceeded", options.phase);
|
|
439
|
+
charge(key.length * 2);
|
|
440
|
+
subtreeDepth = Math.max(subtreeDepth, 1 + child(descriptorValue(current, key, "Record"), depth + 1));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} finally {
|
|
444
|
+
active.delete(current);
|
|
445
|
+
}
|
|
446
|
+
seen.set(current, subtreeDepth);
|
|
447
|
+
return subtreeDepth;
|
|
448
|
+
};
|
|
449
|
+
walk(value, 0);
|
|
450
|
+
return Object.freeze({ nodes, edges, budgetBytes, messagePorts: Object.freeze(messagePorts), reachableTransferables: Object.freeze(reachableTransferables) });
|
|
451
|
+
}
|
|
452
|
+
function validateTransferListWithStats(value, transfer, options = {}) {
|
|
453
|
+
const limits = normalizeRuntimeLimits(options.limits);
|
|
454
|
+
if (transfer === void 0) {
|
|
455
|
+
const stats2 = validateDto(value, {
|
|
456
|
+
limits: {
|
|
457
|
+
maxDepth: limits.maxDtoDepth,
|
|
458
|
+
maxNodes: limits.maxDtoNodes,
|
|
459
|
+
maxEdges: limits.maxDtoEdges,
|
|
460
|
+
maxBudgetBytes: limits.maxMessageBudgetBytes
|
|
461
|
+
},
|
|
462
|
+
transferables: /* @__PURE__ */ new Set(),
|
|
463
|
+
phase: options.phase ?? "validate"
|
|
464
|
+
});
|
|
465
|
+
return { transfer: Object.freeze([]), stats: stats2 };
|
|
466
|
+
}
|
|
467
|
+
if (!Array.isArray(transfer) || transfer.length > limits.maxTransferEntries) {
|
|
468
|
+
throw new WebLoomError("transfer_invalid", "Capability transfer list exceeds its bounded entry limit", options.phase ?? "validate");
|
|
469
|
+
}
|
|
470
|
+
const result = [];
|
|
471
|
+
const seen = /* @__PURE__ */ new Set();
|
|
472
|
+
let ports = 0;
|
|
473
|
+
for (const item of transfer) {
|
|
474
|
+
if (!isSupportedTransfer(item)) throw new WebLoomError("transfer_invalid", "Capability transfer extractor returned an unsupported resource", options.phase ?? "validate");
|
|
475
|
+
if (seen.has(item)) continue;
|
|
476
|
+
seen.add(item);
|
|
477
|
+
result.push(item);
|
|
478
|
+
if (isMessagePort(item)) ports += 1;
|
|
479
|
+
if (result.length > limits.maxTransfers || ports > limits.maxMessagePorts) throw new WebLoomError("transfer_invalid", "Capability transfer list exceeds its bounded resource limit", options.phase ?? "validate");
|
|
480
|
+
}
|
|
481
|
+
const stats = validateDto(value, {
|
|
482
|
+
limits: {
|
|
483
|
+
maxDepth: limits.maxDtoDepth,
|
|
484
|
+
maxNodes: limits.maxDtoNodes,
|
|
485
|
+
maxEdges: limits.maxDtoEdges,
|
|
486
|
+
maxBudgetBytes: limits.maxMessageBudgetBytes
|
|
487
|
+
},
|
|
488
|
+
// MessagePort presence is checked below so a missing declaration is
|
|
489
|
+
// reported as a transfer-contract error, while the walker still validates
|
|
490
|
+
// the complete DTO graph and preserves the actual port identity set.
|
|
491
|
+
allowUnlistedMessagePorts: true,
|
|
492
|
+
transferables: new Set(result),
|
|
493
|
+
phase: options.phase ?? "validate"
|
|
494
|
+
});
|
|
495
|
+
const reachable = new Set(stats.reachableTransferables);
|
|
496
|
+
if (result.some((item) => !reachable.has(item))) throw new WebLoomError("transfer_invalid", "Capability transfer resource is not reachable from its payload", options.phase ?? "validate");
|
|
497
|
+
const declared = new Set(result);
|
|
498
|
+
if (stats.messagePorts.some((port) => !declared.has(port))) throw new WebLoomError("transfer_invalid", "Capability payload contains a MessagePort missing from its transfer declaration", options.phase ?? "validate");
|
|
499
|
+
return { transfer: Object.freeze(result), stats };
|
|
500
|
+
}
|
|
501
|
+
function validateTransferList(value, transfer, options = {}) {
|
|
502
|
+
return validateTransferListWithStats(value, transfer, options).transfer;
|
|
503
|
+
}
|
|
504
|
+
function createReceivePortLedger(ports, options = {}) {
|
|
505
|
+
const limits = normalizeRuntimeLimits(options.limits);
|
|
506
|
+
const source = Array.isArray(ports) ? ports : [];
|
|
507
|
+
let invalid = ports !== void 0 && !Array.isArray(ports);
|
|
508
|
+
invalid = invalid || source.length > limits.maxTransferEntries;
|
|
509
|
+
const unique = [];
|
|
510
|
+
const seen = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const port of source) {
|
|
512
|
+
if (!isMessagePort(port) || seen.has(port)) invalid = true;
|
|
513
|
+
else {
|
|
514
|
+
seen.add(port);
|
|
515
|
+
unique.push(port);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (unique.length > limits.maxMessagePorts) invalid = true;
|
|
519
|
+
let handedOff = false;
|
|
520
|
+
let closed = false;
|
|
521
|
+
const close = () => {
|
|
522
|
+
if (closed || handedOff) return;
|
|
523
|
+
closed = true;
|
|
524
|
+
for (const port of unique) {
|
|
525
|
+
try {
|
|
526
|
+
port.close();
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
if (invalid) close();
|
|
532
|
+
return {
|
|
533
|
+
ports: Object.freeze(unique),
|
|
534
|
+
valid: !invalid,
|
|
535
|
+
handoff() {
|
|
536
|
+
if (!closed) handedOff = true;
|
|
537
|
+
},
|
|
538
|
+
closeUndelivered: close
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function assertReceivedPortSet(ledger, expected, options = {}) {
|
|
542
|
+
const expectedPorts = expected.filter(isMessagePort);
|
|
543
|
+
const actual = new Set(ledger.ports);
|
|
544
|
+
const wanted = new Set(expectedPorts);
|
|
545
|
+
if (actual.size !== wanted.size || [...actual].some((port) => !wanted.has(port))) {
|
|
546
|
+
ledger.closeUndelivered();
|
|
547
|
+
throw new WebLoomError("transfer_invalid", "Received MessagePort set does not match the capability transfer declaration", options.phase ?? "receive");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function validateRawDto(value, limits, phase2 = "receive") {
|
|
551
|
+
const normalized = normalizeRuntimeLimits(limits);
|
|
552
|
+
return validateDto(value, {
|
|
553
|
+
limits: { maxDepth: normalized.maxDtoDepth, maxNodes: normalized.maxDtoNodes, maxEdges: normalized.maxDtoEdges, maxBudgetBytes: normalized.maxMessageBudgetBytes },
|
|
554
|
+
allowUnlistedMessagePorts: true,
|
|
555
|
+
phase: phase2
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function cloneAttributeValue(value, seen) {
|
|
559
|
+
if (value === null || typeof value !== "object") return value;
|
|
560
|
+
const existing = seen.get(value);
|
|
561
|
+
if (existing !== void 0) return existing;
|
|
562
|
+
if (Array.isArray(value)) {
|
|
563
|
+
const result2 = [];
|
|
564
|
+
seen.set(value, result2);
|
|
565
|
+
const length = Object.getOwnPropertyDescriptor(value, "length")?.value ?? 0;
|
|
566
|
+
for (let index = 0; index < length; index += 1) {
|
|
567
|
+
const child = Object.getOwnPropertyDescriptor(value, String(index))?.value;
|
|
568
|
+
result2.push(cloneAttributeValue(child, seen));
|
|
569
|
+
}
|
|
570
|
+
return result2;
|
|
571
|
+
}
|
|
572
|
+
const prototype = Object.getPrototypeOf(value);
|
|
573
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error("Capability attributes must contain only records and arrays");
|
|
574
|
+
const result = Object.create(prototype);
|
|
575
|
+
seen.set(value, result);
|
|
576
|
+
for (const key of Object.keys(value)) {
|
|
577
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
578
|
+
if (!descriptor || !("value" in descriptor)) throw new Error("Capability attributes must contain data properties");
|
|
579
|
+
result[key] = cloneAttributeValue(descriptor.value, seen);
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
function cloneFrozenAttributes(value) {
|
|
584
|
+
const source = value ?? {};
|
|
585
|
+
try {
|
|
586
|
+
const stats = validateDto(source, { limits: ATTRIBUTES_DTO_LIMITS, phase: "validate" });
|
|
587
|
+
if (stats.messagePorts.length > 0 || stats.reachableTransferables.length > 0) throw new Error("Capability attributes cannot contain transferable resources");
|
|
588
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) throw new Error("Capability attributes must be a finite record");
|
|
589
|
+
const clone = cloneAttributeValue(source, /* @__PURE__ */ new Map());
|
|
590
|
+
const clonedStats = validateDto(clone, { limits: ATTRIBUTES_DTO_LIMITS, phase: "validate" });
|
|
591
|
+
if (!clone || typeof clone !== "object" || Array.isArray(clone) || clonedStats.messagePorts.length > 0 || clonedStats.reachableTransferables.length > 0) throw new Error("Capability attributes must be a finite record");
|
|
592
|
+
const freeze = (current, seen) => {
|
|
593
|
+
if (!current || typeof current !== "object" || seen.has(current)) return;
|
|
594
|
+
seen.add(current);
|
|
595
|
+
if (Array.isArray(current)) for (const item of current) freeze(item, seen);
|
|
596
|
+
else for (const key of Object.keys(current)) freeze(current[key], seen);
|
|
597
|
+
Object.freeze(current);
|
|
598
|
+
};
|
|
599
|
+
freeze(clone, /* @__PURE__ */ new Set());
|
|
600
|
+
return clone;
|
|
601
|
+
} catch (error2) {
|
|
602
|
+
if (error2 instanceof WebLoomError) throw error2;
|
|
603
|
+
throw new WebLoomError("invalid_message", "Capability attributes are not a finite cloneable record", "validate");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/runtime/runtimeProtocol.ts
|
|
608
|
+
var RUNTIME_PROTOCOL_VERSION = "webloom.runtime.v1";
|
|
609
|
+
var RUNTIME_SNAPSHOT_TYPE = `${RUNTIME_PROTOCOL_VERSION}.snapshot`;
|
|
610
|
+
var RUNTIME_ERROR_TYPE = `${RUNTIME_PROTOCOL_VERSION}.runtime-error`;
|
|
611
|
+
var RUNTIME_CALL_TYPE = `${RUNTIME_PROTOCOL_VERSION}.call`;
|
|
612
|
+
var RUNTIME_RESULT_TYPE = `${RUNTIME_PROTOCOL_VERSION}.result`;
|
|
613
|
+
var RUNTIME_ERROR_MESSAGE_TYPE = `${RUNTIME_PROTOCOL_VERSION}.error`;
|
|
614
|
+
var RUNTIME_CANCEL_TYPE = `${RUNTIME_PROTOCOL_VERSION}.cancel`;
|
|
615
|
+
var RUNTIME_NEXT_TYPE = `${RUNTIME_PROTOCOL_VERSION}.next`;
|
|
616
|
+
var RUNTIME_CREDIT_TYPE = `${RUNTIME_PROTOCOL_VERSION}.credit`;
|
|
617
|
+
function record(value) {
|
|
618
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
619
|
+
}
|
|
620
|
+
function text(value) {
|
|
621
|
+
return typeof value === "string" && value.length > 0;
|
|
622
|
+
}
|
|
623
|
+
var MAX_CALL_ID_LENGTH = 128;
|
|
624
|
+
var MAX_ID_LENGTH = 256;
|
|
625
|
+
var MAX_VERSION_LENGTH = 64;
|
|
626
|
+
var MAX_ERROR_MESSAGE_LENGTH = 1024;
|
|
627
|
+
var MAX_SNAPSHOT_UNITS = 512;
|
|
628
|
+
var MAX_SNAPSHOT_SERVICES = 1024;
|
|
629
|
+
function boundedText(value, maximum) {
|
|
630
|
+
return text(value) && value.length <= maximum;
|
|
631
|
+
}
|
|
632
|
+
function phase(value) {
|
|
633
|
+
return value === "validate" || value === "wait" || value === "dispatch" || value === "execute" || value === "receive" || value === "dispose";
|
|
634
|
+
}
|
|
635
|
+
function validDetails(value) {
|
|
636
|
+
if (value === void 0) return true;
|
|
637
|
+
if (!record(value)) return false;
|
|
638
|
+
try {
|
|
639
|
+
const prototype = Object.getPrototypeOf(value);
|
|
640
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
641
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
642
|
+
if (typeof key !== "string") return false;
|
|
643
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
644
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) return false;
|
|
645
|
+
}
|
|
646
|
+
} catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
let budget = 0;
|
|
650
|
+
for (const [key, child] of Object.entries(value)) {
|
|
651
|
+
if (key.length > 128) return false;
|
|
652
|
+
if (typeof child === "string") {
|
|
653
|
+
if (child.length > 1024) return false;
|
|
654
|
+
budget += key.length * 2 + child.length * 2;
|
|
655
|
+
} else if (typeof child === "number") {
|
|
656
|
+
if (!Number.isFinite(child)) return false;
|
|
657
|
+
budget += key.length * 2 + 8;
|
|
658
|
+
} else if (typeof child === "boolean" || child === null) budget += key.length * 2 + 8;
|
|
659
|
+
else return false;
|
|
660
|
+
if (budget > 4 * 1024) return false;
|
|
661
|
+
}
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
function validIdentity(message2) {
|
|
665
|
+
return boundedText(message2.protocolVersion, MAX_VERSION_LENGTH) && message2.protocolVersion === RUNTIME_PROTOCOL_VERSION;
|
|
666
|
+
}
|
|
667
|
+
function validServiceIdentity(message2) {
|
|
668
|
+
return boundedText(message2.callId, MAX_CALL_ID_LENGTH) && boundedText(message2.serviceInstanceId, MAX_ID_LENGTH);
|
|
669
|
+
}
|
|
670
|
+
function validateSnapshot(value) {
|
|
671
|
+
if (value.type !== RUNTIME_SNAPSHOT_TYPE || !validIdentity(value) || !boundedText(value.runtimeId, MAX_ID_LENGTH) || !boundedText(value.runtimeInstanceId, MAX_ID_LENGTH) || value.runtimeKind !== "window-main" && value.runtimeKind !== "shared-worker" || !Number.isSafeInteger(value.revision) || value.revision < 0 || !["starting", "ready", "stopping", "failed", "disposed"].includes(String(value.state)) || !Array.isArray(value.units) || !Array.isArray(value.services) || value.units.length > MAX_SNAPSHOT_UNITS || value.services.length > MAX_SNAPSHOT_SERVICES) return false;
|
|
672
|
+
if (value.state !== "ready" && value.services.length !== 0) return false;
|
|
673
|
+
const unitKeys = /* @__PURE__ */ new Set();
|
|
674
|
+
for (const unit of value.units) {
|
|
675
|
+
if (!record(unit) || !boundedText(unit.pluginId, MAX_ID_LENGTH) || !boundedText(unit.unitId, MAX_ID_LENGTH) || unit.runtime !== value.runtimeKind || !["registered", "starting", "stopping", "enabled", "disabled", "blocked", "error-disabled", "cleanup-pending", "unknown"].includes(String(unit.state))) return false;
|
|
676
|
+
if (unit.instanceId !== void 0 && !boundedText(unit.instanceId, MAX_ID_LENGTH)) return false;
|
|
677
|
+
const key = `${unit.pluginId}\0${unit.unitId}`;
|
|
678
|
+
if (unitKeys.has(key)) return false;
|
|
679
|
+
unitKeys.add(key);
|
|
680
|
+
}
|
|
681
|
+
const serviceKeys = /* @__PURE__ */ new Set();
|
|
682
|
+
for (const item of value.services) {
|
|
683
|
+
if (!record(item) || item.kind !== "rpc" && item.kind !== "stream" || !boundedText(item.capabilityId, MAX_ID_LENGTH) || !boundedText(item.contractVersion, MAX_VERSION_LENGTH) || !boundedText(item.serviceInstanceId, MAX_ID_LENGTH) || !record(item.attributes) || Array.isArray(item.attributes)) return false;
|
|
684
|
+
try {
|
|
685
|
+
const stats = validateDto(item.attributes, { limits: ATTRIBUTES_DTO_LIMITS, phase: "validate" });
|
|
686
|
+
if (stats.messagePorts.length > 0 || stats.reachableTransferables.length > 0) return false;
|
|
687
|
+
} catch {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
if (item.grantId !== void 0 && !boundedText(item.grantId, MAX_ID_LENGTH)) return false;
|
|
691
|
+
if (item.authorizationRevision !== void 0 && (!Number.isSafeInteger(item.authorizationRevision) || item.authorizationRevision < 0)) return false;
|
|
692
|
+
const key = `${item.kind}\0${item.capabilityId}\0${item.contractVersion}`;
|
|
693
|
+
if (serviceKeys.has(key)) return false;
|
|
694
|
+
serviceKeys.add(key);
|
|
695
|
+
}
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
function validateMessage(value) {
|
|
699
|
+
if (!record(value) || !text(value.type) || !validIdentity(value)) return false;
|
|
700
|
+
if (value.type === RUNTIME_SNAPSHOT_TYPE) return validateSnapshot(value);
|
|
701
|
+
if (value.type === RUNTIME_ERROR_TYPE) return boundedText(value.code, MAX_ID_LENGTH) && boundedText(value.message, MAX_ERROR_MESSAGE_LENGTH) && phase(value.phase) && (value.pluginId === void 0 || boundedText(value.pluginId, MAX_ID_LENGTH)) && (value.unitId === void 0 || boundedText(value.unitId, MAX_ID_LENGTH));
|
|
702
|
+
if (value.type === RUNTIME_CALL_TYPE) return validServiceIdentity(value) && boundedText(value.capabilityId, MAX_ID_LENGTH) && boundedText(value.contractVersion, MAX_VERSION_LENGTH) && Object.hasOwn(value, "request") && validPayload(value.request) && (value.mode === "unary" || value.mode === "stream") && typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs) && value.timeoutMs > 0 && value.timeoutMs <= 3e5 && (value.operationId === void 0 || boundedText(value.operationId, MAX_ID_LENGTH)) && (value.grantId === void 0 || boundedText(value.grantId, MAX_ID_LENGTH)) && (value.mode !== "stream" ? !Object.hasOwn(value, "initialCredit") : Number.isSafeInteger(value.initialCredit) && value.initialCredit >= 1 && value.initialCredit <= 256);
|
|
703
|
+
if (value.type === RUNTIME_RESULT_TYPE) return validServiceIdentity(value) && ((!Object.hasOwn(value, "result") || validPayload(value.result)) && (value.streamReady === true && !Object.hasOwn(value, "result") && !Object.hasOwn(value, "done") || value.done === true && !Object.hasOwn(value, "result") && !Object.hasOwn(value, "streamReady") || Object.hasOwn(value, "result") && !Object.hasOwn(value, "done") && !Object.hasOwn(value, "streamReady")));
|
|
704
|
+
if (value.type === RUNTIME_ERROR_MESSAGE_TYPE) return validServiceIdentity(value) && record(value.error) && boundedText(value.error.code, MAX_ID_LENGTH) && boundedText(value.error.message, MAX_ERROR_MESSAGE_LENGTH) && phase(value.error.phase) && validDetails(value.error.details);
|
|
705
|
+
if (value.type === RUNTIME_CANCEL_TYPE) return validServiceIdentity(value);
|
|
706
|
+
if (value.type === RUNTIME_NEXT_TYPE) return validServiceIdentity(value) && Object.hasOwn(value, "item") && validPayload(value.item) && Number.isSafeInteger(value.sequence) && value.sequence >= 1;
|
|
707
|
+
if (value.type === RUNTIME_CREDIT_TYPE) return validServiceIdentity(value) && Number.isSafeInteger(value.count) && value.count >= 1 && value.count <= 256;
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
function validPayload(value) {
|
|
711
|
+
try {
|
|
712
|
+
validateDto(value, { allowUnlistedMessagePorts: true, phase: "validate" });
|
|
713
|
+
return true;
|
|
714
|
+
} catch {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function createRuntimeMessageCodec() {
|
|
719
|
+
return {
|
|
720
|
+
encode(message2) {
|
|
721
|
+
if (!validateMessage(message2)) throw new WebLoomError("invalid_snapshot", "Invalid WebLoom runtime message", "validate");
|
|
722
|
+
return message2;
|
|
723
|
+
},
|
|
724
|
+
decode(value) {
|
|
725
|
+
if (!record(value) || value.protocolVersion !== RUNTIME_PROTOCOL_VERSION) throw new WebLoomError("protocol_mismatch", "Unsupported WebLoom runtime protocol", "validate");
|
|
726
|
+
if (!validateMessage(value)) throw new WebLoomError("invalid_snapshot", "Invalid WebLoom runtime message", "validate");
|
|
727
|
+
return value;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function isRuntimeSnapshot(value) {
|
|
732
|
+
return value.type === RUNTIME_SNAPSHOT_TYPE;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/host/capabilityRegistry.ts
|
|
736
|
+
function cloneReference(reference) {
|
|
737
|
+
return Object.freeze({
|
|
738
|
+
...reference,
|
|
739
|
+
attributes: Object.freeze({ ...reference.attributes })
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
function createCapabilityRegistry() {
|
|
743
|
+
const entries = /* @__PURE__ */ new Map();
|
|
744
|
+
const requireCapability = (capability) => {
|
|
745
|
+
const entry = entries.get(capabilityKey(capability));
|
|
746
|
+
if (!entry) throw new Error(`Capability "${capability.id}" version "${capability.version}" is not available`);
|
|
747
|
+
return entry;
|
|
748
|
+
};
|
|
749
|
+
return {
|
|
750
|
+
provide(capability, value, ownerId = "host", scope) {
|
|
751
|
+
if (capability.kind !== "local") throw new TypeError(`Capability "${capability.id}" is not local`);
|
|
752
|
+
const key = capabilityKey(capability);
|
|
753
|
+
if (entries.has(key)) throw new Error(`Capability "${capability.id}" version "${capability.version}" is already provided`);
|
|
754
|
+
const reference = cloneReference({
|
|
755
|
+
kind: "rpc",
|
|
756
|
+
capabilityId: capability.id,
|
|
757
|
+
contractVersion: capability.version,
|
|
758
|
+
runtime: "window-main",
|
|
759
|
+
runtimeInstanceId: "local",
|
|
760
|
+
serviceInstanceId: `local:${ownerId}:${capability.id}:${capability.version}`,
|
|
761
|
+
attributes: {}
|
|
762
|
+
});
|
|
763
|
+
entries.set(key, { capability, ownerId, value, scope, reference });
|
|
764
|
+
},
|
|
765
|
+
handle(capability, handler, ownerId, scope, reference, peerDependencies) {
|
|
766
|
+
if (capability.kind !== "rpc") throw new TypeError(`Capability "${capability.id}" is not an RPC capability`);
|
|
767
|
+
const key = capabilityKey(capability);
|
|
768
|
+
if (entries.has(key)) throw new Error(`Capability "${capability.id}" version "${capability.version}" is already handled`);
|
|
769
|
+
entries.set(key, {
|
|
770
|
+
capability,
|
|
771
|
+
ownerId,
|
|
772
|
+
handler,
|
|
773
|
+
scope,
|
|
774
|
+
...peerDependencies ? { peerDependencies: Object.freeze([...peerDependencies]) } : {},
|
|
775
|
+
reference: cloneReference({ ...reference, kind: "rpc" })
|
|
776
|
+
});
|
|
777
|
+
},
|
|
778
|
+
stream(capability, handler, ownerId, scope, reference, peerDependencies) {
|
|
779
|
+
if (capability.kind !== "stream") throw new TypeError(`Capability "${capability.id}" is not a stream capability`);
|
|
780
|
+
const key = capabilityKey(capability);
|
|
781
|
+
if (entries.has(key)) throw new Error(`Capability "${capability.id}" version "${capability.version}" is already handled`);
|
|
782
|
+
entries.set(key, {
|
|
783
|
+
capability,
|
|
784
|
+
ownerId,
|
|
785
|
+
handler,
|
|
786
|
+
scope,
|
|
787
|
+
...peerDependencies ? { peerDependencies: Object.freeze([...peerDependencies]) } : {},
|
|
788
|
+
reference: cloneReference({ ...reference, kind: "stream" })
|
|
789
|
+
});
|
|
790
|
+
},
|
|
791
|
+
revoke(capability, ownerId) {
|
|
792
|
+
const key = capabilityKey(capability);
|
|
793
|
+
const entry = entries.get(key);
|
|
794
|
+
if (!entry) return;
|
|
795
|
+
if (ownerId !== void 0 && entry.ownerId !== ownerId) return;
|
|
796
|
+
entries.delete(key);
|
|
797
|
+
},
|
|
798
|
+
get(capability) {
|
|
799
|
+
const entry = requireCapability(capability);
|
|
800
|
+
if (capability.kind !== "local" || entry.value === void 0) {
|
|
801
|
+
throw new Error(`Capability "${capability.id}" is not a local value`);
|
|
802
|
+
}
|
|
803
|
+
return entry.value;
|
|
804
|
+
},
|
|
805
|
+
registration: (capability) => entries.get(capabilityKey(capability)),
|
|
806
|
+
has: (capability) => entries.has(capabilityKey(capability)),
|
|
807
|
+
require: requireCapability,
|
|
808
|
+
descriptors: () => Object.freeze([...entries.values()].map((entry) => capabilityDescriptor(entry.capability))),
|
|
809
|
+
registrations: () => Object.freeze([...entries.values()]),
|
|
810
|
+
async invoke(capability, request, call) {
|
|
811
|
+
const entry = requireCapability(capability);
|
|
812
|
+
if (capability.kind !== "rpc" || typeof entry.handler !== "function") {
|
|
813
|
+
throw new Error(`Capability "${capability.id}" is not an RPC handler`);
|
|
814
|
+
}
|
|
815
|
+
return await invokeCapabilityHandler(entry, request, call);
|
|
816
|
+
},
|
|
817
|
+
async openStream(capability, request, call) {
|
|
818
|
+
const entry = requireCapability(capability);
|
|
819
|
+
if (capability.kind !== "stream" || typeof entry.handler !== "function") {
|
|
820
|
+
throw new Error(`Capability "${capability.id}" is not a stream handler`);
|
|
821
|
+
}
|
|
822
|
+
return await invokeCapabilityHandler(entry, request, call);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function invokeCapabilityHandler(registration, request, context) {
|
|
827
|
+
if (registration.capability.kind === "rpc") {
|
|
828
|
+
const capability = registration.capability;
|
|
829
|
+
const handler = registration.handler;
|
|
830
|
+
if (!handler) throw new Error(`Capability "${registration.capability.id}" has no RPC handler`);
|
|
831
|
+
let parsed;
|
|
832
|
+
try {
|
|
833
|
+
parsed = capability.request.parse(request);
|
|
834
|
+
} catch (error2) {
|
|
835
|
+
throw new WebLoomError("request_validation_failed", error2 instanceof Error ? error2.message : String(error2), "validate", { capabilityId: capability.id });
|
|
836
|
+
}
|
|
837
|
+
return handler(parsed, context);
|
|
838
|
+
}
|
|
839
|
+
if (registration.capability.kind === "stream") {
|
|
840
|
+
const capability = registration.capability;
|
|
841
|
+
const handler = registration.handler;
|
|
842
|
+
if (!handler) throw new Error(`Capability "${registration.capability.id}" has no stream handler`);
|
|
843
|
+
let parsed;
|
|
844
|
+
try {
|
|
845
|
+
parsed = capability.request.parse(request);
|
|
846
|
+
} catch (error2) {
|
|
847
|
+
throw new WebLoomError("request_validation_failed", error2 instanceof Error ? error2.message : String(error2), "validate", { capabilityId: capability.id });
|
|
848
|
+
}
|
|
849
|
+
return handler(parsed, context);
|
|
850
|
+
}
|
|
851
|
+
throw new Error(`Capability "${registration.capability.id}" cannot be invoked remotely`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/host/pluginGraph.ts
|
|
855
|
+
function unitCandidates(manifest, runtime) {
|
|
856
|
+
const units = [...manifest.units ?? []];
|
|
857
|
+
if (units.length === 0) {
|
|
858
|
+
return [{ id: manifest.id, ...runtime !== void 0 ? { runtime } : {} }];
|
|
859
|
+
}
|
|
860
|
+
if (runtime === void 0) return units;
|
|
861
|
+
return units.filter((unit) => unit.runtime === void 0 || unit.runtime === runtime);
|
|
862
|
+
}
|
|
863
|
+
function selectRuntimeUnit(manifest, runtime) {
|
|
864
|
+
const units = unitCandidates(manifest, runtime);
|
|
865
|
+
if (units.length !== 1) return void 0;
|
|
866
|
+
const unit = units[0];
|
|
867
|
+
if (!unit) return void 0;
|
|
868
|
+
const selectedRuntime = unit.runtime ?? runtime ?? "window-main";
|
|
869
|
+
if (selectedRuntime === void 0) return void 0;
|
|
870
|
+
return { ...unit, runtime: selectedRuntime };
|
|
871
|
+
}
|
|
872
|
+
function dependenciesOfManifest(manifest, runtime) {
|
|
873
|
+
const unit = selectRuntimeUnit(manifest, runtime);
|
|
874
|
+
return (unit?.dependencies ?? []).filter((dependency) => dependency.source !== "peer");
|
|
875
|
+
}
|
|
876
|
+
function providesOfManifest(manifest, runtime) {
|
|
877
|
+
const unit = selectRuntimeUnit(manifest, runtime);
|
|
878
|
+
return unit?.provides ?? [];
|
|
879
|
+
}
|
|
880
|
+
function descriptorKey(value) {
|
|
881
|
+
return capabilityKey(value);
|
|
882
|
+
}
|
|
883
|
+
function keysForBuiltin(value) {
|
|
884
|
+
const result = /* @__PURE__ */ new Set();
|
|
885
|
+
for (const item of value ?? []) result.add(typeof item === "string" ? item : descriptorKey(item));
|
|
886
|
+
return result;
|
|
887
|
+
}
|
|
888
|
+
function freezeRecord(record2) {
|
|
889
|
+
for (const value of Object.values(record2)) if (Array.isArray(value)) Object.freeze(value);
|
|
890
|
+
return Object.freeze(record2);
|
|
891
|
+
}
|
|
892
|
+
function capabilityLabel(capability) {
|
|
893
|
+
return `${capability.kind}:${capability.id}@${capability.version}`;
|
|
894
|
+
}
|
|
895
|
+
function collectUnits(manifests, runtime) {
|
|
896
|
+
const result = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const manifest of manifests) {
|
|
898
|
+
for (const unit of unitCandidates(manifest, runtime)) {
|
|
899
|
+
const selectedRuntime = unit.runtime ?? runtime;
|
|
900
|
+
if (!selectedRuntime) continue;
|
|
901
|
+
result.set(`${manifest.id}\0${unit.id}`, {
|
|
902
|
+
pluginId: manifest.id,
|
|
903
|
+
unitId: unit.id,
|
|
904
|
+
runtime: selectedRuntime,
|
|
905
|
+
dependencies: Object.freeze([...unit.dependencies ?? []].filter((item) => item.source !== "peer").map((item) => item.capability)),
|
|
906
|
+
provides: Object.freeze([...unit.provides ?? []])
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
function buildPluginGraph(manifests, options = {}) {
|
|
913
|
+
const dependencies = {};
|
|
914
|
+
const optionalDependencies = {};
|
|
915
|
+
const provides = {};
|
|
916
|
+
const providers = {};
|
|
917
|
+
const reverse = {};
|
|
918
|
+
const unitMap = collectUnits(manifests, options.runtime);
|
|
919
|
+
const enabled = options.enabledPluginIds ?? /* @__PURE__ */ new Set();
|
|
920
|
+
for (const manifest of manifests) {
|
|
921
|
+
const deps = [...dependenciesOfManifest(manifest, options.runtime)];
|
|
922
|
+
dependencies[manifest.id] = Object.freeze(deps.filter((item) => !item.optional).map((item) => item.capability));
|
|
923
|
+
optionalDependencies[manifest.id] = Object.freeze(deps.filter((item) => item.optional).map((item) => item.capability));
|
|
924
|
+
const offered = [...providesOfManifest(manifest, options.runtime)];
|
|
925
|
+
provides[manifest.id] = Object.freeze(offered);
|
|
926
|
+
for (const capability of offered) (providers[descriptorKey(capability)] ??= []).push(manifest.id);
|
|
927
|
+
}
|
|
928
|
+
for (const manifest of manifests) {
|
|
929
|
+
const deps = [...dependenciesOfManifest(manifest, options.runtime) ?? []];
|
|
930
|
+
for (const dependency of deps) {
|
|
931
|
+
if (dependency.optional) continue;
|
|
932
|
+
for (const providerId of providers[descriptorKey(dependency.capability)] ?? []) {
|
|
933
|
+
(reverse[providerId] ??= []).push({
|
|
934
|
+
pluginId: manifest.id,
|
|
935
|
+
enabled: enabled.has(manifest.id),
|
|
936
|
+
capabilities: Object.freeze([dependency.capability])
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const cycles = [];
|
|
942
|
+
const byId = new Map(manifests.map((manifest) => [manifest.id, manifest]));
|
|
943
|
+
const visiting = [];
|
|
944
|
+
const visited = /* @__PURE__ */ new Set();
|
|
945
|
+
const visit = (pluginId) => {
|
|
946
|
+
const start = visiting.indexOf(pluginId);
|
|
947
|
+
if (start >= 0) {
|
|
948
|
+
cycles.push(Object.freeze([...visiting.slice(start), pluginId]));
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (visited.has(pluginId)) return;
|
|
952
|
+
const manifest = byId.get(pluginId);
|
|
953
|
+
if (!manifest) return;
|
|
954
|
+
visiting.push(pluginId);
|
|
955
|
+
for (const dependency of dependenciesOfManifest(manifest, options.runtime)) {
|
|
956
|
+
if (dependency.optional) continue;
|
|
957
|
+
for (const providerId of providers[descriptorKey(dependency.capability)] ?? []) visit(providerId);
|
|
958
|
+
}
|
|
959
|
+
visiting.pop();
|
|
960
|
+
visited.add(pluginId);
|
|
961
|
+
};
|
|
962
|
+
for (const manifest of manifests) visit(manifest.id);
|
|
963
|
+
const reverseFrozen = {};
|
|
964
|
+
for (const [pluginId, values] of Object.entries(reverse)) reverseFrozen[pluginId] = Object.freeze(values);
|
|
965
|
+
const providerFrozen = {};
|
|
966
|
+
for (const [key, values] of Object.entries(providers)) providerFrozen[key] = Object.freeze(values);
|
|
967
|
+
const units = {};
|
|
968
|
+
for (const [key, value] of unitMap) units[key] = value;
|
|
969
|
+
return Object.freeze({
|
|
970
|
+
plugins: Object.freeze(manifests.map((manifest) => manifest.id)),
|
|
971
|
+
dependencies: freezeRecord(dependencies),
|
|
972
|
+
optionalDependencies: freezeRecord(optionalDependencies),
|
|
973
|
+
provides: freezeRecord(provides),
|
|
974
|
+
reverse: freezeRecord(reverseFrozen),
|
|
975
|
+
providers: freezeRecord(providerFrozen),
|
|
976
|
+
cycles: Object.freeze(cycles),
|
|
977
|
+
units: Object.freeze(units)
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function validatePluginGraph(manifests, options = {}) {
|
|
981
|
+
const ids = /* @__PURE__ */ new Set();
|
|
982
|
+
const providerRuntime = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const manifest of manifests) {
|
|
984
|
+
if (!manifest || typeof manifest.id !== "string" || manifest.id.trim() === "") throw new TypeError("Plugin id must be a non-empty string");
|
|
985
|
+
if (ids.has(manifest.id)) throw new Error(`Plugin "${manifest.id}" is duplicated`);
|
|
986
|
+
ids.add(manifest.id);
|
|
987
|
+
if (typeof manifest.name !== "string" || manifest.name.trim() === "") throw new TypeError(`Plugin "${manifest.id}" name must be a non-empty string`);
|
|
988
|
+
if (manifest.startup !== "required" && manifest.startup !== "optional") throw new TypeError(`Plugin "${manifest.id}" startup must be required or optional`);
|
|
989
|
+
if (typeof manifest.defaultEnabled !== "boolean" || typeof manifest.canDisable !== "boolean") throw new TypeError(`Plugin "${manifest.id}" startup policy is incomplete`);
|
|
990
|
+
if (manifest.startup === "required" && (!manifest.defaultEnabled || manifest.canDisable)) throw new TypeError(`Plugin "${manifest.id}" required startup policy is inconsistent`);
|
|
991
|
+
const unitIds = /* @__PURE__ */ new Set();
|
|
992
|
+
for (const unit of manifest.units ?? []) {
|
|
993
|
+
if (!unit || typeof unit.id !== "string" || unit.id.trim() === "" || unitIds.has(unit.id)) throw new TypeError(`Plugin "${manifest.id}" has a duplicate or empty unit id`);
|
|
994
|
+
unitIds.add(unit.id);
|
|
995
|
+
if (unit.runtime !== void 0 && unit.runtime !== "window-main" && unit.runtime !== "shared-worker") throw new TypeError(`Plugin "${manifest.id}" unit "${unit.id}" has an invalid Runtime`);
|
|
996
|
+
const seenProvides = /* @__PURE__ */ new Set();
|
|
997
|
+
for (const capability of unit.provides ?? []) {
|
|
998
|
+
if (!isCapabilityDescriptor(capability)) throw new TypeError(`Plugin "${manifest.id}" has an invalid capability descriptor`);
|
|
999
|
+
const key = descriptorKey(capability);
|
|
1000
|
+
if (seenProvides.has(key)) throw new Error(`Plugin "${manifest.id}" provides ${capabilityLabel(capability)} more than once`);
|
|
1001
|
+
seenProvides.add(key);
|
|
1002
|
+
const previousRuntime = providerRuntime.get(key);
|
|
1003
|
+
if (providerRuntime.has(key) && previousRuntime === unit.runtime) throw new Error(`Capability ${capabilityLabel(capability)} has ambiguous providers`);
|
|
1004
|
+
if (!providerRuntime.has(key)) providerRuntime.set(key, unit.runtime);
|
|
1005
|
+
}
|
|
1006
|
+
for (const dependency of unit.dependencies ?? []) {
|
|
1007
|
+
if (!dependency || !isCapabilityDescriptor(dependency.capability)) throw new TypeError(`Plugin "${manifest.id}" has an invalid dependency descriptor`);
|
|
1008
|
+
if (dependency.source !== void 0 && dependency.source !== "peer") throw new TypeError(`Plugin "${manifest.id}" has an invalid dependency source`);
|
|
1009
|
+
if (dependency.source === "peer" && dependency.sourceRuntime !== void 0) throw new TypeError(`Plugin "${manifest.id}" peer dependency cannot declare sourceRuntime`);
|
|
1010
|
+
if (dependency.source !== "peer" && dependency.sourceRuntime !== "window-main" && dependency.sourceRuntime !== "shared-worker") throw new TypeError(`Plugin "${manifest.id}" has an invalid dependency Runtime`);
|
|
1011
|
+
if (dependency.source === "peer" && dependency.capability.kind === "local") {
|
|
1012
|
+
throw new TypeError(`Plugin "${manifest.id}" cannot depend on local capability "${dependency.capability.id}" through a peer`);
|
|
1013
|
+
}
|
|
1014
|
+
if (dependency.source !== "peer" && dependency.capability.kind === "local" && dependency.sourceRuntime !== (unit.runtime ?? options.runtime)) {
|
|
1015
|
+
throw new TypeError(`Plugin "${manifest.id}" cannot depend on local capability "${dependency.capability.id}" from another Runtime`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const graph = buildPluginGraph(manifests, options);
|
|
1021
|
+
if (graph.cycles.length > 0) throw new Error(`Plugin dependency cycle: ${graph.cycles[0]?.join(" -> ") ?? "unknown"}`);
|
|
1022
|
+
const builtin = keysForBuiltin(options.builtinCapabilities);
|
|
1023
|
+
for (const manifest of manifests) {
|
|
1024
|
+
for (const dependency of dependenciesOfManifest(manifest, options.runtime)) {
|
|
1025
|
+
const key = descriptorKey(dependency.capability);
|
|
1026
|
+
const providerCount = graph.providers[key]?.length ?? 0;
|
|
1027
|
+
const external = options.externalRuntimeDependencies === true && dependency.sourceRuntime !== options.runtime;
|
|
1028
|
+
if (providerCount === 0 && !builtin.has(key) && !external && !dependency.optional && options.allowMissingDependencies !== true) throw new Error(`Plugin "${manifest.id}" requires missing capability ${capabilityLabel(dependency.capability)}`);
|
|
1029
|
+
if (providerCount > 1 && !external) throw new Error(`Capability ${capabilityLabel(dependency.capability)} has ambiguous providers`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function reverseDependentsOf(graph, pluginId, enabledPluginIds) {
|
|
1034
|
+
return (graph.reverse[pluginId] ?? []).filter((item) => enabledPluginIds === void 0 || enabledPluginIds.has(item.pluginId));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/lifecycle/resourceScope.ts
|
|
1038
|
+
function makeId(prefix) {
|
|
1039
|
+
try {
|
|
1040
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1041
|
+
return `${prefix}:${crypto.randomUUID()}`;
|
|
1042
|
+
}
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
return `${prefix}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
1046
|
+
}
|
|
1047
|
+
function errorMessage(error2) {
|
|
1048
|
+
if (error2 instanceof Error) return error2.message;
|
|
1049
|
+
return typeof error2 === "string" ? error2 : String(error2);
|
|
1050
|
+
}
|
|
1051
|
+
function isPositiveFiniteNumber(value) {
|
|
1052
|
+
return value !== void 0 && Number.isFinite(value) && value >= 0;
|
|
1053
|
+
}
|
|
1054
|
+
function cloneScopeAttributes(value) {
|
|
1055
|
+
let clone;
|
|
1056
|
+
try {
|
|
1057
|
+
const structuredCloneFn = globalThis.structuredClone;
|
|
1058
|
+
clone = structuredCloneFn ? structuredCloneFn(value ?? {}) : { ...value ?? {} };
|
|
1059
|
+
} catch {
|
|
1060
|
+
throw new TypeError("Lifecycle scope attributes must be structured-cloneable");
|
|
1061
|
+
}
|
|
1062
|
+
if (!clone || typeof clone !== "object" || Array.isArray(clone)) throw new TypeError("Lifecycle scope attributes must be a record");
|
|
1063
|
+
const freeze = (current, seen) => {
|
|
1064
|
+
if (!current || typeof current !== "object" || seen.has(current)) return;
|
|
1065
|
+
seen.add(current);
|
|
1066
|
+
if (Array.isArray(current)) for (const item of current) freeze(item, seen);
|
|
1067
|
+
else for (const item of Object.values(current)) freeze(item, seen);
|
|
1068
|
+
Object.freeze(current);
|
|
1069
|
+
};
|
|
1070
|
+
freeze(clone, /* @__PURE__ */ new Set());
|
|
1071
|
+
return clone;
|
|
1072
|
+
}
|
|
1073
|
+
function createLifecycleScope(options) {
|
|
1074
|
+
const metadata = options.metadata;
|
|
1075
|
+
const identity = {
|
|
1076
|
+
scopeId: options.scopeId ?? makeId(`scope:${options.kind}`),
|
|
1077
|
+
instanceId: options.instanceId ?? makeId("instance"),
|
|
1078
|
+
kind: options.kind,
|
|
1079
|
+
...metadata,
|
|
1080
|
+
attributes: cloneScopeAttributes(metadata?.attributes)
|
|
1081
|
+
};
|
|
1082
|
+
const controller = new AbortController();
|
|
1083
|
+
const revokeListeners = /* @__PURE__ */ new Set();
|
|
1084
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1085
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
1086
|
+
let currentState = "active";
|
|
1087
|
+
let revokeReason = "scope revoked";
|
|
1088
|
+
let disposalPromise;
|
|
1089
|
+
let refreshPublishedDisposeResult;
|
|
1090
|
+
const disposeResultNotifier = options.onDisposeResult;
|
|
1091
|
+
const notifyChange = (scope) => {
|
|
1092
|
+
try {
|
|
1093
|
+
options.onChange?.(scope);
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const publicScope = {};
|
|
1098
|
+
function uniqueResourceId(resourceId) {
|
|
1099
|
+
const base = resourceId && resourceId.length > 0 ? resourceId : makeId("resource");
|
|
1100
|
+
if (!usedIds.has(base)) {
|
|
1101
|
+
usedIds.add(base);
|
|
1102
|
+
return base;
|
|
1103
|
+
}
|
|
1104
|
+
let index = 2;
|
|
1105
|
+
while (usedIds.has(`${base}#${index}`)) index += 1;
|
|
1106
|
+
const unique = `${base}#${index}`;
|
|
1107
|
+
usedIds.add(unique);
|
|
1108
|
+
return unique;
|
|
1109
|
+
}
|
|
1110
|
+
function assertActive() {
|
|
1111
|
+
if (currentState !== "active") {
|
|
1112
|
+
throw new LifecycleScopeRevokedError(
|
|
1113
|
+
`Lifecycle scope "${identity.scopeId}" is ${currentState}`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function addEntry(resourceId, cleanup, state, removeOnFailure = false, phase2 = "before-teardown") {
|
|
1118
|
+
assertActive();
|
|
1119
|
+
const entry = {
|
|
1120
|
+
resourceId: uniqueResourceId(resourceId),
|
|
1121
|
+
state,
|
|
1122
|
+
cleanup,
|
|
1123
|
+
phase: phase2,
|
|
1124
|
+
cleanupStarted: false,
|
|
1125
|
+
cleanupFinished: false,
|
|
1126
|
+
removeOnFailure
|
|
1127
|
+
};
|
|
1128
|
+
entries.set(entry.resourceId, entry);
|
|
1129
|
+
notifyChange(publicScope);
|
|
1130
|
+
return entry;
|
|
1131
|
+
}
|
|
1132
|
+
function releaseEntry(entry, reason) {
|
|
1133
|
+
if (entry.cleanupPromise) return entry.cleanupPromise;
|
|
1134
|
+
entry.cleanupStarted = true;
|
|
1135
|
+
entry.cleanupPromise = Promise.resolve().then(() => entry.cleanup(reason)).then(() => {
|
|
1136
|
+
entry.cleanupFinished = true;
|
|
1137
|
+
if (!entry.lateReleaseFailed) {
|
|
1138
|
+
entry.state = "released";
|
|
1139
|
+
entry.error = void 0;
|
|
1140
|
+
}
|
|
1141
|
+
notifyChange(publicScope);
|
|
1142
|
+
}).catch((error2) => {
|
|
1143
|
+
entry.cleanupFinished = true;
|
|
1144
|
+
entry.state = "pending";
|
|
1145
|
+
entry.error = errorMessage(error2);
|
|
1146
|
+
notifyChange(publicScope);
|
|
1147
|
+
throw error2;
|
|
1148
|
+
});
|
|
1149
|
+
entry.cleanupPromise.catch(() => void 0);
|
|
1150
|
+
return entry.cleanupPromise;
|
|
1151
|
+
}
|
|
1152
|
+
function track(resource, release, resourceId) {
|
|
1153
|
+
if (currentState !== "active") {
|
|
1154
|
+
void Promise.resolve().then(() => release(resource, revokeReason)).catch(() => void 0);
|
|
1155
|
+
throw new LifecycleScopeRevokedError();
|
|
1156
|
+
}
|
|
1157
|
+
let released = false;
|
|
1158
|
+
addEntry(
|
|
1159
|
+
resourceId,
|
|
1160
|
+
async (reason) => {
|
|
1161
|
+
if (released) return;
|
|
1162
|
+
released = true;
|
|
1163
|
+
await release(resource, reason);
|
|
1164
|
+
},
|
|
1165
|
+
"active"
|
|
1166
|
+
);
|
|
1167
|
+
return resource;
|
|
1168
|
+
}
|
|
1169
|
+
function acquire(resourceId, create, release) {
|
|
1170
|
+
assertActive();
|
|
1171
|
+
let resource;
|
|
1172
|
+
let hasResource = false;
|
|
1173
|
+
let released = false;
|
|
1174
|
+
let resolveAcquisition;
|
|
1175
|
+
let rejectAcquisition;
|
|
1176
|
+
let acquisitionSettled = false;
|
|
1177
|
+
const acquisitionDone = new Promise((resolve, reject) => {
|
|
1178
|
+
resolveAcquisition = () => {
|
|
1179
|
+
if (acquisitionSettled) return;
|
|
1180
|
+
acquisitionSettled = true;
|
|
1181
|
+
resolve();
|
|
1182
|
+
};
|
|
1183
|
+
rejectAcquisition = (error2) => {
|
|
1184
|
+
if (acquisitionSettled) return;
|
|
1185
|
+
acquisitionSettled = true;
|
|
1186
|
+
reject(error2);
|
|
1187
|
+
};
|
|
1188
|
+
});
|
|
1189
|
+
acquisitionDone.catch(() => void 0);
|
|
1190
|
+
const entry = addEntry(
|
|
1191
|
+
resourceId,
|
|
1192
|
+
async (reason) => {
|
|
1193
|
+
await acquisitionDone;
|
|
1194
|
+
if (!hasResource || released) return;
|
|
1195
|
+
released = true;
|
|
1196
|
+
await release(resource, reason);
|
|
1197
|
+
},
|
|
1198
|
+
"acquiring",
|
|
1199
|
+
true,
|
|
1200
|
+
"before-teardown"
|
|
1201
|
+
);
|
|
1202
|
+
entry.acquisitionDone = acquisitionDone;
|
|
1203
|
+
return Promise.resolve().then(() => create(controller.signal)).then(async (created) => {
|
|
1204
|
+
resource = created;
|
|
1205
|
+
hasResource = true;
|
|
1206
|
+
if (currentState !== "active" || controller.signal.aborted || entry.cleanupStarted) {
|
|
1207
|
+
if (!released) {
|
|
1208
|
+
released = true;
|
|
1209
|
+
try {
|
|
1210
|
+
await release(created, revokeReason);
|
|
1211
|
+
} catch (error2) {
|
|
1212
|
+
entry.lateReleaseFailed = true;
|
|
1213
|
+
entry.state = "pending";
|
|
1214
|
+
entry.error = errorMessage(error2);
|
|
1215
|
+
notifyChange(publicScope);
|
|
1216
|
+
rejectAcquisition(error2);
|
|
1217
|
+
throw error2;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
entry.state = "released";
|
|
1221
|
+
resolveAcquisition();
|
|
1222
|
+
notifyChange(publicScope);
|
|
1223
|
+
throw new LifecycleScopeRevokedError(
|
|
1224
|
+
`Resource "${entry.resourceId}" completed after scope revoke`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
entry.state = "active";
|
|
1228
|
+
entry.removeOnFailure = false;
|
|
1229
|
+
resolveAcquisition();
|
|
1230
|
+
notifyChange(publicScope);
|
|
1231
|
+
return created;
|
|
1232
|
+
}).catch((error2) => {
|
|
1233
|
+
if (!hasResource) {
|
|
1234
|
+
resolveAcquisition();
|
|
1235
|
+
entries.delete(entry.resourceId);
|
|
1236
|
+
usedIds.delete(entry.resourceId);
|
|
1237
|
+
}
|
|
1238
|
+
throw error2;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function onDispose(cleanup, resourceId, phase2 = "before-teardown") {
|
|
1242
|
+
if (currentState !== "active") {
|
|
1243
|
+
void Promise.resolve().then(() => cleanup(revokeReason)).catch(() => void 0);
|
|
1244
|
+
return () => void 0;
|
|
1245
|
+
}
|
|
1246
|
+
const entry = addEntry(resourceId, cleanup, "active", false, phase2);
|
|
1247
|
+
return () => {
|
|
1248
|
+
if (entry.cleanupStarted || entry.cleanupFinished) return;
|
|
1249
|
+
entries.delete(entry.resourceId);
|
|
1250
|
+
usedIds.delete(entry.resourceId);
|
|
1251
|
+
notifyChange(publicScope);
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function onRevoke(listener) {
|
|
1255
|
+
if (currentState === "active") {
|
|
1256
|
+
revokeListeners.add(listener);
|
|
1257
|
+
return () => revokeListeners.delete(listener);
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
listener(revokeReason);
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
return () => void 0;
|
|
1264
|
+
}
|
|
1265
|
+
function revoke(reason = "scope revoked") {
|
|
1266
|
+
if (currentState !== "active") return;
|
|
1267
|
+
revokeReason = reason;
|
|
1268
|
+
currentState = "stopping";
|
|
1269
|
+
try {
|
|
1270
|
+
controller.abort(new LifecycleScopeRevokedError(reason));
|
|
1271
|
+
} catch {
|
|
1272
|
+
controller.abort();
|
|
1273
|
+
}
|
|
1274
|
+
for (const listener of [...revokeListeners]) {
|
|
1275
|
+
try {
|
|
1276
|
+
listener(reason);
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
notifyChange(publicScope);
|
|
1281
|
+
}
|
|
1282
|
+
async function runEntry(entry, reason, timeoutMs, issues, onLateSuccess, onLateFailure) {
|
|
1283
|
+
if (entry.cleanupFinished && entry.state === "released") return "released";
|
|
1284
|
+
const cleanup = releaseEntry(entry, reason);
|
|
1285
|
+
if (!isPositiveFiniteNumber(timeoutMs)) {
|
|
1286
|
+
try {
|
|
1287
|
+
await cleanup;
|
|
1288
|
+
return entry.state === "released" ? "released" : "pending";
|
|
1289
|
+
} catch (error2) {
|
|
1290
|
+
issues.push({
|
|
1291
|
+
resourceId: entry.resourceId,
|
|
1292
|
+
code: "lifecycle.cleanup_failed",
|
|
1293
|
+
message: errorMessage(error2)
|
|
1294
|
+
});
|
|
1295
|
+
return "pending";
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
let timeoutHandle;
|
|
1299
|
+
const timeout = new Promise((resolve) => {
|
|
1300
|
+
timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
1301
|
+
});
|
|
1302
|
+
const result = await Promise.race([
|
|
1303
|
+
cleanup.then(() => "released", (error2) => ({ error: error2 })),
|
|
1304
|
+
timeout
|
|
1305
|
+
]);
|
|
1306
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1307
|
+
if (result === "timeout") {
|
|
1308
|
+
entry.state = "pending";
|
|
1309
|
+
issues.push({
|
|
1310
|
+
resourceId: entry.resourceId,
|
|
1311
|
+
code: "lifecycle.cleanup_timeout",
|
|
1312
|
+
message: `Cleanup timed out after ${timeoutMs}ms`
|
|
1313
|
+
});
|
|
1314
|
+
void cleanup.then(
|
|
1315
|
+
() => {
|
|
1316
|
+
try {
|
|
1317
|
+
onLateSuccess?.(entry);
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1321
|
+
(error2) => {
|
|
1322
|
+
try {
|
|
1323
|
+
onLateFailure?.(entry, error2);
|
|
1324
|
+
} catch {
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
);
|
|
1328
|
+
return "pending";
|
|
1329
|
+
}
|
|
1330
|
+
if (result === "released") return "released";
|
|
1331
|
+
entry.state = "pending";
|
|
1332
|
+
issues.push({
|
|
1333
|
+
resourceId: entry.resourceId,
|
|
1334
|
+
code: "lifecycle.cleanup_failed",
|
|
1335
|
+
message: errorMessage(result.error)
|
|
1336
|
+
});
|
|
1337
|
+
return "pending";
|
|
1338
|
+
}
|
|
1339
|
+
async function dispose(options2 = {}) {
|
|
1340
|
+
if (disposalPromise) return disposalPromise;
|
|
1341
|
+
revoke(options2.reason ?? "scope disposed");
|
|
1342
|
+
const reason = options2.reason ?? revokeReason;
|
|
1343
|
+
const entriesToRelease = [...entries.values()].reverse();
|
|
1344
|
+
disposalPromise = (async () => {
|
|
1345
|
+
const errors = [];
|
|
1346
|
+
const pending = /* @__PURE__ */ new Set();
|
|
1347
|
+
const lateReleased = /* @__PURE__ */ new Set();
|
|
1348
|
+
let released = 0;
|
|
1349
|
+
let attempted = 0;
|
|
1350
|
+
let resultSnapshot;
|
|
1351
|
+
let resultPublished = false;
|
|
1352
|
+
const publishDisposeResult = () => {
|
|
1353
|
+
if (!resultPublished || !resultSnapshot) return;
|
|
1354
|
+
try {
|
|
1355
|
+
disposeResultNotifier?.(resultSnapshot);
|
|
1356
|
+
} catch {
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
const rebuildResultSnapshot = () => {
|
|
1360
|
+
if (!resultSnapshot) return;
|
|
1361
|
+
const mergedPending = new Set(pending);
|
|
1362
|
+
const mergedErrors = [...errors];
|
|
1363
|
+
let mergedAttempted = attempted;
|
|
1364
|
+
let mergedReleased = released;
|
|
1365
|
+
for (const childEntry of entries.values()) {
|
|
1366
|
+
const childScope = childEntry.childScope;
|
|
1367
|
+
const childResult = childEntry.childResult;
|
|
1368
|
+
if (!childScope || !childResult) continue;
|
|
1369
|
+
const prefix = `child:${childScope.identity.scopeId}:`;
|
|
1370
|
+
mergedAttempted += childResult.attempted;
|
|
1371
|
+
mergedReleased += childResult.released;
|
|
1372
|
+
for (const resourceId of childResult.pending) {
|
|
1373
|
+
mergedPending.add(`${prefix}${resourceId}`);
|
|
1374
|
+
}
|
|
1375
|
+
for (const issue of childResult.errors) {
|
|
1376
|
+
mergedErrors.push({
|
|
1377
|
+
...issue,
|
|
1378
|
+
resourceId: `${prefix}${issue.resourceId}`
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
resultSnapshot.attempted = mergedAttempted;
|
|
1383
|
+
resultSnapshot.released = mergedReleased;
|
|
1384
|
+
resultSnapshot.pending = [...mergedPending];
|
|
1385
|
+
resultSnapshot.errors = mergedErrors;
|
|
1386
|
+
resultSnapshot.cleanupIncomplete = mergedPending.size > 0 || mergedErrors.length > 0;
|
|
1387
|
+
};
|
|
1388
|
+
const projectChildResult = (entry, childResult, lateResourceId, lateError) => {
|
|
1389
|
+
if (!entry.childScope) return;
|
|
1390
|
+
const previous = entry.childResult;
|
|
1391
|
+
entry.childResult = childResult;
|
|
1392
|
+
entry.state = childResult.cleanupIncomplete ? "pending" : "released";
|
|
1393
|
+
entry.error = childResult.cleanupIncomplete ? childResult.errors[0]?.message ?? "Child scope cleanup is still pending" : void 0;
|
|
1394
|
+
rebuildResultSnapshot();
|
|
1395
|
+
publishDisposeResult();
|
|
1396
|
+
if (lateResourceId && resultSnapshot) {
|
|
1397
|
+
const resourceId = `child:${entry.childScope.identity.scopeId}:${lateResourceId}`;
|
|
1398
|
+
try {
|
|
1399
|
+
if (lateError !== void 0) options2.onLateFailure?.(resourceId, lateError, resultSnapshot);
|
|
1400
|
+
else options2.onLateSuccess?.(resourceId, resultSnapshot);
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
} else if (previous?.cleanupIncomplete && !childResult.cleanupIncomplete && resultSnapshot) {
|
|
1404
|
+
try {
|
|
1405
|
+
options2.onLateSuccess?.(`child:${entry.childScope.identity.scopeId}`, resultSnapshot);
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
notifyChange(publicScope);
|
|
1410
|
+
};
|
|
1411
|
+
refreshPublishedDisposeResult = () => {
|
|
1412
|
+
if (!resultPublished || !resultSnapshot) return;
|
|
1413
|
+
rebuildResultSnapshot();
|
|
1414
|
+
publishDisposeResult();
|
|
1415
|
+
};
|
|
1416
|
+
const onLateFailure = (entry, error2) => {
|
|
1417
|
+
entry.state = "pending";
|
|
1418
|
+
entry.error = errorMessage(error2);
|
|
1419
|
+
if (!errors.some((issue) => issue.resourceId === entry.resourceId && issue.code === "lifecycle.cleanup_failed")) {
|
|
1420
|
+
errors.push({
|
|
1421
|
+
resourceId: entry.resourceId,
|
|
1422
|
+
code: "lifecycle.cleanup_failed",
|
|
1423
|
+
message: errorMessage(error2)
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
pending.add(entry.resourceId);
|
|
1427
|
+
if (resultSnapshot) {
|
|
1428
|
+
rebuildResultSnapshot();
|
|
1429
|
+
}
|
|
1430
|
+
publishDisposeResult();
|
|
1431
|
+
try {
|
|
1432
|
+
options2.onLateFailure?.(entry.resourceId, error2, resultSnapshot);
|
|
1433
|
+
} catch {
|
|
1434
|
+
}
|
|
1435
|
+
notifyChange(publicScope);
|
|
1436
|
+
};
|
|
1437
|
+
const onLateSuccess = (entry) => {
|
|
1438
|
+
if (lateReleased.has(entry.resourceId)) return;
|
|
1439
|
+
lateReleased.add(entry.resourceId);
|
|
1440
|
+
pending.delete(entry.resourceId);
|
|
1441
|
+
released += 1;
|
|
1442
|
+
for (let index = errors.length - 1; index >= 0; index -= 1) {
|
|
1443
|
+
const issue = errors[index];
|
|
1444
|
+
if (issue?.resourceId === entry.resourceId && issue.code === "lifecycle.cleanup_timeout") {
|
|
1445
|
+
errors.splice(index, 1);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (resultSnapshot) {
|
|
1449
|
+
rebuildResultSnapshot();
|
|
1450
|
+
}
|
|
1451
|
+
publishDisposeResult();
|
|
1452
|
+
try {
|
|
1453
|
+
options2.onLateSuccess?.(entry.resourceId, resultSnapshot);
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
notifyChange(publicScope);
|
|
1457
|
+
};
|
|
1458
|
+
const releasePhase = async (phase2) => {
|
|
1459
|
+
for (const entry of entriesToRelease.filter((item) => item.phase === phase2)) {
|
|
1460
|
+
if (entry.childScope) {
|
|
1461
|
+
entry.cleanupStarted = true;
|
|
1462
|
+
try {
|
|
1463
|
+
const childResult = await entry.childScope.dispose({
|
|
1464
|
+
reason,
|
|
1465
|
+
timeoutMs: options2.timeoutMs,
|
|
1466
|
+
onLateSuccess: (resourceId, result2) => {
|
|
1467
|
+
if (result2) projectChildResult(entry, result2, resourceId);
|
|
1468
|
+
},
|
|
1469
|
+
onLateFailure: (resourceId, error2, result2) => {
|
|
1470
|
+
if (result2) projectChildResult(entry, result2, resourceId, error2);
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
entry.cleanupFinished = true;
|
|
1474
|
+
projectChildResult(entry, childResult);
|
|
1475
|
+
} catch (error2) {
|
|
1476
|
+
entry.cleanupFinished = true;
|
|
1477
|
+
entry.state = "pending";
|
|
1478
|
+
entry.error = errorMessage(error2);
|
|
1479
|
+
pending.add(entry.resourceId);
|
|
1480
|
+
errors.push({
|
|
1481
|
+
resourceId: entry.resourceId,
|
|
1482
|
+
code: "lifecycle.cleanup_failed",
|
|
1483
|
+
message: errorMessage(error2)
|
|
1484
|
+
});
|
|
1485
|
+
notifyChange(publicScope);
|
|
1486
|
+
}
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
attempted += 1;
|
|
1490
|
+
const result = await runEntry(entry, reason, options2.timeoutMs, errors, onLateSuccess, onLateFailure);
|
|
1491
|
+
if (result === "released") released += 1;
|
|
1492
|
+
else if (!lateReleased.has(entry.resourceId)) pending.add(entry.resourceId);
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
await releasePhase("before-teardown");
|
|
1496
|
+
if (options2.teardown) {
|
|
1497
|
+
const teardownEntry = {
|
|
1498
|
+
resourceId: "scope:teardown",
|
|
1499
|
+
state: "active",
|
|
1500
|
+
cleanup: options2.teardown,
|
|
1501
|
+
phase: "before-teardown",
|
|
1502
|
+
cleanupStarted: false,
|
|
1503
|
+
cleanupFinished: false,
|
|
1504
|
+
removeOnFailure: false
|
|
1505
|
+
};
|
|
1506
|
+
attempted += 1;
|
|
1507
|
+
const result = await runEntry(teardownEntry, reason, options2.timeoutMs, errors, onLateSuccess, onLateFailure);
|
|
1508
|
+
if (result === "released") released += 1;
|
|
1509
|
+
else if (!lateReleased.has(teardownEntry.resourceId)) pending.add(teardownEntry.resourceId);
|
|
1510
|
+
}
|
|
1511
|
+
await releasePhase("after-teardown");
|
|
1512
|
+
currentState = "stopped";
|
|
1513
|
+
notifyChange(publicScope);
|
|
1514
|
+
resultSnapshot = {
|
|
1515
|
+
scopeId: identity.scopeId,
|
|
1516
|
+
state: "stopped",
|
|
1517
|
+
attempted,
|
|
1518
|
+
released,
|
|
1519
|
+
pending: [...pending],
|
|
1520
|
+
errors,
|
|
1521
|
+
cleanupIncomplete: pending.size > 0 || errors.length > 0
|
|
1522
|
+
};
|
|
1523
|
+
rebuildResultSnapshot();
|
|
1524
|
+
resultPublished = true;
|
|
1525
|
+
publishDisposeResult();
|
|
1526
|
+
return resultSnapshot;
|
|
1527
|
+
})();
|
|
1528
|
+
return disposalPromise;
|
|
1529
|
+
}
|
|
1530
|
+
function resources() {
|
|
1531
|
+
return [...entries.values()].map((entry) => ({
|
|
1532
|
+
resourceId: entry.resourceId,
|
|
1533
|
+
state: entry.state,
|
|
1534
|
+
...entry.error ? { error: entry.error } : {}
|
|
1535
|
+
}));
|
|
1536
|
+
}
|
|
1537
|
+
function child(kind, metadata2 = {}) {
|
|
1538
|
+
assertActive();
|
|
1539
|
+
let childEntry;
|
|
1540
|
+
const childScope = createLifecycleScope({
|
|
1541
|
+
kind,
|
|
1542
|
+
metadata: {
|
|
1543
|
+
...metadata2,
|
|
1544
|
+
parentScopeId: identity.scopeId
|
|
1545
|
+
},
|
|
1546
|
+
// 父作用域的 Host 订阅者也必须看到子作用域资源的状态变化。
|
|
1547
|
+
onChange: () => notifyChange(publicScope),
|
|
1548
|
+
// 只有子作用域已经生成最终结果后,父级才允许更新/删除登记。
|
|
1549
|
+
onDisposeResult: (result) => {
|
|
1550
|
+
const entry2 = childEntry;
|
|
1551
|
+
if (!entry2) return;
|
|
1552
|
+
entry2.childResult = result;
|
|
1553
|
+
entry2.state = result.cleanupIncomplete ? "pending" : "released";
|
|
1554
|
+
entry2.error = result.cleanupIncomplete ? result.errors[0]?.message ?? "Child scope cleanup is still pending" : void 0;
|
|
1555
|
+
refreshPublishedDisposeResult?.();
|
|
1556
|
+
if (!result.cleanupIncomplete && !disposalPromise && !entry2.cleanupStarted) {
|
|
1557
|
+
entries.delete(entry2.resourceId);
|
|
1558
|
+
usedIds.delete(entry2.resourceId);
|
|
1559
|
+
}
|
|
1560
|
+
notifyChange(publicScope);
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
const removeRevoke = onRevoke((reason) => childScope.revoke(reason));
|
|
1564
|
+
const entry = addEntry(`child:${childScope.identity.scopeId}`, async () => void 0, "active");
|
|
1565
|
+
childEntry = entry;
|
|
1566
|
+
entry.childScope = childScope;
|
|
1567
|
+
childScope.onDispose(() => {
|
|
1568
|
+
removeRevoke();
|
|
1569
|
+
}, `parent-link:${identity.scopeId}`);
|
|
1570
|
+
return childScope;
|
|
1571
|
+
}
|
|
1572
|
+
function listen(target, event, listener, options2) {
|
|
1573
|
+
assertActive();
|
|
1574
|
+
if (!target || typeof target.addEventListener !== "function" || typeof target.removeEventListener !== "function") {
|
|
1575
|
+
throw new TypeError("Lifecycle scope listen target must be an EventTarget");
|
|
1576
|
+
}
|
|
1577
|
+
let active = true;
|
|
1578
|
+
target.addEventListener(event, listener, options2);
|
|
1579
|
+
let removeRevoke;
|
|
1580
|
+
const release = () => {
|
|
1581
|
+
if (!active) return;
|
|
1582
|
+
active = false;
|
|
1583
|
+
target.removeEventListener(event, listener, options2);
|
|
1584
|
+
removeRevoke?.();
|
|
1585
|
+
};
|
|
1586
|
+
removeRevoke = onRevoke(release);
|
|
1587
|
+
if (!active) release();
|
|
1588
|
+
return release;
|
|
1589
|
+
}
|
|
1590
|
+
function interval(callback, milliseconds) {
|
|
1591
|
+
assertActive();
|
|
1592
|
+
if (typeof callback !== "function") throw new TypeError("Lifecycle scope interval callback must be a function");
|
|
1593
|
+
if (!Number.isFinite(milliseconds) || milliseconds < 0) throw new TypeError("Lifecycle scope interval milliseconds must be a finite non-negative number");
|
|
1594
|
+
const handle = setInterval(callback, milliseconds);
|
|
1595
|
+
let active = true;
|
|
1596
|
+
let removeRevoke;
|
|
1597
|
+
const release = () => {
|
|
1598
|
+
if (!active) return;
|
|
1599
|
+
active = false;
|
|
1600
|
+
clearInterval(handle);
|
|
1601
|
+
removeRevoke?.();
|
|
1602
|
+
};
|
|
1603
|
+
removeRevoke = onRevoke(release);
|
|
1604
|
+
if (!active) release();
|
|
1605
|
+
return release;
|
|
1606
|
+
}
|
|
1607
|
+
function subscribe(subscribeFn, listener) {
|
|
1608
|
+
assertActive();
|
|
1609
|
+
if (typeof subscribeFn !== "function" || typeof listener !== "function") {
|
|
1610
|
+
throw new TypeError("Lifecycle scope subscribe requires functions");
|
|
1611
|
+
}
|
|
1612
|
+
let active = true;
|
|
1613
|
+
let unsubscribe;
|
|
1614
|
+
let removeRevoke;
|
|
1615
|
+
const release = () => {
|
|
1616
|
+
if (!active) return;
|
|
1617
|
+
active = false;
|
|
1618
|
+
removeRevoke?.();
|
|
1619
|
+
unsubscribe?.();
|
|
1620
|
+
};
|
|
1621
|
+
removeRevoke = onRevoke(release);
|
|
1622
|
+
try {
|
|
1623
|
+
const candidate = subscribeFn(listener);
|
|
1624
|
+
if (typeof candidate !== "function") throw new TypeError("Lifecycle subscribe function must return an unsubscribe function");
|
|
1625
|
+
unsubscribe = candidate;
|
|
1626
|
+
if (!active) unsubscribe();
|
|
1627
|
+
} catch (error2) {
|
|
1628
|
+
release();
|
|
1629
|
+
throw error2;
|
|
1630
|
+
}
|
|
1631
|
+
return release;
|
|
1632
|
+
}
|
|
1633
|
+
Object.assign(publicScope, {
|
|
1634
|
+
identity,
|
|
1635
|
+
signal: controller.signal,
|
|
1636
|
+
onRevoke,
|
|
1637
|
+
onDispose,
|
|
1638
|
+
track,
|
|
1639
|
+
acquire,
|
|
1640
|
+
child,
|
|
1641
|
+
revoke,
|
|
1642
|
+
dispose,
|
|
1643
|
+
assertActive,
|
|
1644
|
+
resources,
|
|
1645
|
+
listen,
|
|
1646
|
+
interval,
|
|
1647
|
+
subscribe
|
|
1648
|
+
});
|
|
1649
|
+
Object.defineProperty(publicScope, "state", {
|
|
1650
|
+
enumerable: true,
|
|
1651
|
+
configurable: false,
|
|
1652
|
+
get: () => currentState
|
|
1653
|
+
});
|
|
1654
|
+
return publicScope;
|
|
1655
|
+
}
|
|
1656
|
+
var createResourceScope = createLifecycleScope;
|
|
1657
|
+
|
|
1658
|
+
// src/lifecycle/permissionLease.ts
|
|
1659
|
+
function uniquePermissions(permissions) {
|
|
1660
|
+
return [...new Set(permissions ?? [])];
|
|
1661
|
+
}
|
|
1662
|
+
function sameBindingValue(actual, expected) {
|
|
1663
|
+
return expected === void 0 || actual === expected;
|
|
1664
|
+
}
|
|
1665
|
+
function sameAttributes(actual, expected) {
|
|
1666
|
+
if (expected === void 0) return true;
|
|
1667
|
+
return Object.keys(expected).every((key) => Object.is(actual[key], expected[key]));
|
|
1668
|
+
}
|
|
1669
|
+
function normalizedRevision(value, name) {
|
|
1670
|
+
if (value === void 0) return void 0;
|
|
1671
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a non-negative safe integer`);
|
|
1672
|
+
return value;
|
|
1673
|
+
}
|
|
1674
|
+
function createPermissionLease(options) {
|
|
1675
|
+
const binding = {
|
|
1676
|
+
...options.identity,
|
|
1677
|
+
requested: Object.freeze(uniquePermissions(options.requested)),
|
|
1678
|
+
approved: Object.freeze(uniquePermissions(options.approved)),
|
|
1679
|
+
...options.sessionConstraints ? { sessionConstraints: Object.freeze(uniquePermissions(options.sessionConstraints)) } : {},
|
|
1680
|
+
...options.policyRevision !== void 0 ? { policyRevision: normalizedRevision(options.policyRevision, "policyRevision") } : {},
|
|
1681
|
+
...options.grantRevision !== void 0 ? { grantRevision: normalizedRevision(options.grantRevision, "grantRevision") } : {},
|
|
1682
|
+
...options.grantId !== void 0 ? { grantId: options.grantId } : {}
|
|
1683
|
+
};
|
|
1684
|
+
Object.freeze(binding);
|
|
1685
|
+
const granted = new Set(
|
|
1686
|
+
binding.requested.filter(
|
|
1687
|
+
(permission) => binding.approved.includes(permission) && (binding.sessionConstraints === void 0 || binding.sessionConstraints.includes(permission))
|
|
1688
|
+
)
|
|
1689
|
+
);
|
|
1690
|
+
let revoked = false;
|
|
1691
|
+
let revokeReason = "permission lease revoked";
|
|
1692
|
+
const lease = {
|
|
1693
|
+
binding,
|
|
1694
|
+
get revoked() {
|
|
1695
|
+
return revoked || options.scope !== void 0 && options.scope.state !== "active";
|
|
1696
|
+
},
|
|
1697
|
+
has(permission) {
|
|
1698
|
+
return !lease.revoked && granted.has(permission);
|
|
1699
|
+
},
|
|
1700
|
+
assert(permission) {
|
|
1701
|
+
if (lease.revoked) {
|
|
1702
|
+
throw new PermissionLeaseRevokedError(revokeReason);
|
|
1703
|
+
}
|
|
1704
|
+
if (!granted.has(permission)) {
|
|
1705
|
+
throw new PermissionDeniedError(permission);
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
assertBinding(expected) {
|
|
1709
|
+
if (!sameBindingValue(binding.pluginId, expected.pluginId) || !sameBindingValue(binding.instanceId, expected.instanceId) || !sameAttributes(binding.attributes, expected.attributes) || !sameBindingValue(binding.policyRevision, expected.policyRevision) || !sameBindingValue(binding.grantRevision, expected.grantRevision) || !sameBindingValue(binding.grantId, expected.grantId)) {
|
|
1710
|
+
throw new PermissionLeaseRevokedError("Permission lease identity does not match");
|
|
1711
|
+
}
|
|
1712
|
+
if (lease.revoked) throw new PermissionLeaseRevokedError(revokeReason);
|
|
1713
|
+
},
|
|
1714
|
+
revoke(reason = "permission lease revoked") {
|
|
1715
|
+
if (revoked) return;
|
|
1716
|
+
revoked = true;
|
|
1717
|
+
revokeReason = reason;
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
if (options.scope) {
|
|
1721
|
+
options.scope.onRevoke((reason) => lease.revoke(reason));
|
|
1722
|
+
options.scope.onDispose((reason) => lease.revoke(reason), "permission-lease");
|
|
1723
|
+
}
|
|
1724
|
+
return lease;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/lifecycle/taskScheduler.ts
|
|
1728
|
+
function errorMessage2(error2) {
|
|
1729
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
1730
|
+
}
|
|
1731
|
+
function createScopedTaskScheduler(scope, options = {}) {
|
|
1732
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
1733
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1734
|
+
let disposed = false;
|
|
1735
|
+
const notify = () => {
|
|
1736
|
+
const value = [...tasks.values()].map((task) => ({
|
|
1737
|
+
id: task.definition.id,
|
|
1738
|
+
pluginId: task.definition.pluginId ?? scope.identity.pluginId ?? "unknown",
|
|
1739
|
+
label: task.definition.label,
|
|
1740
|
+
state: task.state,
|
|
1741
|
+
...task.error ? { error: task.error } : {},
|
|
1742
|
+
...task.lastCompletedAt ? { lastCompletedAt: task.lastCompletedAt } : {},
|
|
1743
|
+
...task.nextRunAt ? { nextRunAt: task.nextRunAt } : {}
|
|
1744
|
+
}));
|
|
1745
|
+
for (const listener of [...listeners]) {
|
|
1746
|
+
try {
|
|
1747
|
+
listener(value);
|
|
1748
|
+
} catch {
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
const clearTimer = (task) => {
|
|
1753
|
+
if (task.timer !== void 0) {
|
|
1754
|
+
clearTimeout(task.timer);
|
|
1755
|
+
task.timer = void 0;
|
|
1756
|
+
}
|
|
1757
|
+
task.nextRunAt = void 0;
|
|
1758
|
+
};
|
|
1759
|
+
const schedule = (task) => {
|
|
1760
|
+
clearTimer(task);
|
|
1761
|
+
const intervalMs = task.definition.intervalMs;
|
|
1762
|
+
if (!task.active || disposed || intervalMs === void 0) {
|
|
1763
|
+
notify();
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 0) {
|
|
1767
|
+
task.error = "\u4EFB\u52A1 intervalMs \u5FC5\u987B\u662F\u975E\u8D1F\u6709\u9650\u6570";
|
|
1768
|
+
task.state = "failed";
|
|
1769
|
+
notify();
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const dueAt = Date.now() + intervalMs;
|
|
1773
|
+
task.nextRunAt = new Date(dueAt).toISOString();
|
|
1774
|
+
task.timer = setTimeout(() => {
|
|
1775
|
+
task.timer = void 0;
|
|
1776
|
+
task.nextRunAt = void 0;
|
|
1777
|
+
void runTask(task, "interval");
|
|
1778
|
+
}, intervalMs);
|
|
1779
|
+
notify();
|
|
1780
|
+
};
|
|
1781
|
+
const runTask = async (task, reason) => {
|
|
1782
|
+
if (!task.active || disposed || scope.state !== "active") return;
|
|
1783
|
+
if (task.runPromise) {
|
|
1784
|
+
task.rerunRequested = true;
|
|
1785
|
+
return task.runPromise;
|
|
1786
|
+
}
|
|
1787
|
+
task.state = "queued";
|
|
1788
|
+
task.error = void 0;
|
|
1789
|
+
notify();
|
|
1790
|
+
const run = (async () => {
|
|
1791
|
+
let requestScope;
|
|
1792
|
+
let controller;
|
|
1793
|
+
let abortFromScope;
|
|
1794
|
+
try {
|
|
1795
|
+
requestScope = scope.child("request");
|
|
1796
|
+
controller = new AbortController();
|
|
1797
|
+
task.controller = controller;
|
|
1798
|
+
abortFromScope = () => controller?.abort(scope.signal.reason);
|
|
1799
|
+
if (scope.signal.aborted) abortFromScope();
|
|
1800
|
+
else scope.signal.addEventListener("abort", abortFromScope, { once: true });
|
|
1801
|
+
task.state = "running";
|
|
1802
|
+
notify();
|
|
1803
|
+
await task.definition.run({ signal: controller.signal, reason });
|
|
1804
|
+
if (!controller.signal.aborted && !requestScope.signal.aborted) {
|
|
1805
|
+
task.state = "idle";
|
|
1806
|
+
task.error = void 0;
|
|
1807
|
+
task.lastCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1808
|
+
} else {
|
|
1809
|
+
task.state = "idle";
|
|
1810
|
+
}
|
|
1811
|
+
} catch (error2) {
|
|
1812
|
+
task.state = "failed";
|
|
1813
|
+
task.error = controller?.signal.aborted ? void 0 : errorMessage2(error2);
|
|
1814
|
+
} finally {
|
|
1815
|
+
if (abortFromScope) scope.signal.removeEventListener("abort", abortFromScope);
|
|
1816
|
+
task.controller = void 0;
|
|
1817
|
+
if (requestScope) {
|
|
1818
|
+
await requestScope.dispose({ reason: `task ${task.definition.id} finished` });
|
|
1819
|
+
}
|
|
1820
|
+
if (task.active && !disposed && scope.state === "active") schedule(task);
|
|
1821
|
+
else clearTimer(task);
|
|
1822
|
+
notify();
|
|
1823
|
+
}
|
|
1824
|
+
})();
|
|
1825
|
+
task.runPromise = run;
|
|
1826
|
+
try {
|
|
1827
|
+
await run;
|
|
1828
|
+
} finally {
|
|
1829
|
+
task.runPromise = void 0;
|
|
1830
|
+
notify();
|
|
1831
|
+
if (task.rerunRequested && task.active && !disposed && scope.state === "active") {
|
|
1832
|
+
task.rerunRequested = false;
|
|
1833
|
+
queueMicrotask(() => {
|
|
1834
|
+
void runTask(task, "coalesced");
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
const releaseTask = async (task, reason) => {
|
|
1840
|
+
if (!task.active) return;
|
|
1841
|
+
task.active = false;
|
|
1842
|
+
task.rerunRequested = false;
|
|
1843
|
+
clearTimer(task);
|
|
1844
|
+
task.controller?.abort(reason);
|
|
1845
|
+
if (task.runPromise) await task.runPromise;
|
|
1846
|
+
tasks.delete(task.definition.id);
|
|
1847
|
+
notify();
|
|
1848
|
+
};
|
|
1849
|
+
const scheduler = {
|
|
1850
|
+
register(definition) {
|
|
1851
|
+
scope.assertActive();
|
|
1852
|
+
if (!definition.id || tasks.has(definition.id)) {
|
|
1853
|
+
throw new Error(`Scoped task id "${definition.id}" is already registered or empty`);
|
|
1854
|
+
}
|
|
1855
|
+
const task = {
|
|
1856
|
+
definition: { ...definition },
|
|
1857
|
+
state: "idle",
|
|
1858
|
+
rerunRequested: false,
|
|
1859
|
+
active: true
|
|
1860
|
+
};
|
|
1861
|
+
tasks.set(definition.id, task);
|
|
1862
|
+
const removeScopeCleanup = scope.onDispose(
|
|
1863
|
+
(reason) => releaseTask(task, reason),
|
|
1864
|
+
`task:${definition.id}`
|
|
1865
|
+
);
|
|
1866
|
+
const unregister = () => {
|
|
1867
|
+
if (!task.active) return;
|
|
1868
|
+
removeScopeCleanup();
|
|
1869
|
+
void releaseTask(task, "task unregistered");
|
|
1870
|
+
};
|
|
1871
|
+
schedule(task);
|
|
1872
|
+
if (options.runOnRegister) void runTask(task, "initial");
|
|
1873
|
+
return unregister;
|
|
1874
|
+
},
|
|
1875
|
+
async runNow(id, reason = "manual") {
|
|
1876
|
+
const task = tasks.get(id);
|
|
1877
|
+
if (!task) throw new Error(`Scoped task "${id}" is not registered`);
|
|
1878
|
+
await runTask(task, reason);
|
|
1879
|
+
},
|
|
1880
|
+
async cancel(id) {
|
|
1881
|
+
const task = tasks.get(id);
|
|
1882
|
+
if (!task) return;
|
|
1883
|
+
task.rerunRequested = false;
|
|
1884
|
+
task.controller?.abort("task canceled");
|
|
1885
|
+
if (task.runPromise) await task.runPromise;
|
|
1886
|
+
},
|
|
1887
|
+
snapshot() {
|
|
1888
|
+
return [...tasks.values()].map((task) => ({
|
|
1889
|
+
id: task.definition.id,
|
|
1890
|
+
pluginId: task.definition.pluginId ?? scope.identity.pluginId ?? "unknown",
|
|
1891
|
+
label: task.definition.label,
|
|
1892
|
+
state: task.state,
|
|
1893
|
+
...task.error ? { error: task.error } : {},
|
|
1894
|
+
...task.lastCompletedAt ? { lastCompletedAt: task.lastCompletedAt } : {},
|
|
1895
|
+
...task.nextRunAt ? { nextRunAt: task.nextRunAt } : {}
|
|
1896
|
+
}));
|
|
1897
|
+
},
|
|
1898
|
+
subscribe(listener) {
|
|
1899
|
+
listeners.add(listener);
|
|
1900
|
+
listener(scheduler.snapshot());
|
|
1901
|
+
return () => listeners.delete(listener);
|
|
1902
|
+
},
|
|
1903
|
+
async dispose() {
|
|
1904
|
+
if (disposed) return;
|
|
1905
|
+
disposed = true;
|
|
1906
|
+
for (const task of [...tasks.values()]) {
|
|
1907
|
+
task.controller?.abort("task scheduler disposed");
|
|
1908
|
+
clearTimer(task);
|
|
1909
|
+
}
|
|
1910
|
+
await Promise.all([...tasks.values()].map((task) => releaseTask(task, "task scheduler disposed")));
|
|
1911
|
+
listeners.clear();
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
return scheduler;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// src/messaging/messageBus.ts
|
|
1918
|
+
function makeMessageId() {
|
|
1919
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
1920
|
+
return crypto.randomUUID();
|
|
1921
|
+
}
|
|
1922
|
+
return `m_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
1923
|
+
}
|
|
1924
|
+
function errorMessage3(err) {
|
|
1925
|
+
if (err instanceof Error) return err.message;
|
|
1926
|
+
if (typeof err === "string") return err;
|
|
1927
|
+
return String(err);
|
|
1928
|
+
}
|
|
1929
|
+
var MAX_HANDLER_CONCURRENCY = 128;
|
|
1930
|
+
function waitForHandler(result, signal) {
|
|
1931
|
+
if (signal.aborted) {
|
|
1932
|
+
return Promise.reject(signal.reason ?? new Error("aborted"));
|
|
1933
|
+
}
|
|
1934
|
+
return new Promise((resolve, reject) => {
|
|
1935
|
+
const onAbort = () => {
|
|
1936
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
1937
|
+
};
|
|
1938
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1939
|
+
result.then(resolve, reject).finally(() => {
|
|
1940
|
+
signal.removeEventListener("abort", onAbort);
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
function createMessageBus() {
|
|
1945
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
1946
|
+
const routedHandlers = /* @__PURE__ */ new Map();
|
|
1947
|
+
const mailboxes = /* @__PURE__ */ new Map();
|
|
1948
|
+
const targetHandlerCount = /* @__PURE__ */ new Map();
|
|
1949
|
+
const targetConcurrency = /* @__PURE__ */ new Map();
|
|
1950
|
+
const snapshotListeners = /* @__PURE__ */ new Set();
|
|
1951
|
+
let total = 0;
|
|
1952
|
+
let completed = 0;
|
|
1953
|
+
let failed = 0;
|
|
1954
|
+
let canceled = 0;
|
|
1955
|
+
let inFlight = 0;
|
|
1956
|
+
let lastError;
|
|
1957
|
+
const pumping = /* @__PURE__ */ new Set();
|
|
1958
|
+
function emitSnapshot() {
|
|
1959
|
+
const snap = snapshot();
|
|
1960
|
+
for (const l of snapshotListeners) l(snap);
|
|
1961
|
+
}
|
|
1962
|
+
function snapshot() {
|
|
1963
|
+
const byTarget = {};
|
|
1964
|
+
let queued = 0;
|
|
1965
|
+
for (const [target, queue] of mailboxes.entries()) {
|
|
1966
|
+
byTarget[target] = queue.length;
|
|
1967
|
+
queued += queue.length;
|
|
1968
|
+
}
|
|
1969
|
+
return {
|
|
1970
|
+
total,
|
|
1971
|
+
queued,
|
|
1972
|
+
inFlight,
|
|
1973
|
+
completed,
|
|
1974
|
+
failed,
|
|
1975
|
+
canceled,
|
|
1976
|
+
lastError,
|
|
1977
|
+
byTarget
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function makeMessage(type, mode, payload, options) {
|
|
1981
|
+
return {
|
|
1982
|
+
id: options.messageId ?? makeMessageId(),
|
|
1983
|
+
type,
|
|
1984
|
+
mode,
|
|
1985
|
+
payload,
|
|
1986
|
+
target: options.target,
|
|
1987
|
+
priority: options.priority,
|
|
1988
|
+
timeoutMs: options.timeoutMs,
|
|
1989
|
+
causationId: options.causationId,
|
|
1990
|
+
createdAt: Date.now()
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function publishInternal(message2) {
|
|
1994
|
+
total += 1;
|
|
1995
|
+
const routed = routedHandlers.get(message2.type);
|
|
1996
|
+
if (routed && !routed.target) {
|
|
1997
|
+
try {
|
|
1998
|
+
const result = routed.handler(message2);
|
|
1999
|
+
if (result && typeof result.then === "function") {
|
|
2000
|
+
result.catch((err) => {
|
|
2001
|
+
failed += 1;
|
|
2002
|
+
lastError = errorMessage3(err);
|
|
2003
|
+
emitSnapshot();
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
failed += 1;
|
|
2008
|
+
lastError = errorMessage3(err);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
const bucket = subscriptions.get(message2.type);
|
|
2012
|
+
if (bucket) {
|
|
2013
|
+
for (const sub of [...bucket]) {
|
|
2014
|
+
try {
|
|
2015
|
+
sub.handler(message2.payload);
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
failed += 1;
|
|
2018
|
+
lastError = errorMessage3(err);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
emitSnapshot();
|
|
2023
|
+
return message2.id;
|
|
2024
|
+
}
|
|
2025
|
+
function settleEntry(entry, state, value) {
|
|
2026
|
+
if (entry.settled) return;
|
|
2027
|
+
entry.settled = true;
|
|
2028
|
+
const wasRunning = entry.state === "running";
|
|
2029
|
+
entry.state = state;
|
|
2030
|
+
entry.cleanup();
|
|
2031
|
+
if (state === "completed") {
|
|
2032
|
+
completed += 1;
|
|
2033
|
+
} else if (state === "failed") {
|
|
2034
|
+
failed += 1;
|
|
2035
|
+
lastError = errorMessage3(value);
|
|
2036
|
+
} else {
|
|
2037
|
+
canceled += 1;
|
|
2038
|
+
lastError = errorMessage3(value ?? entry.signal.reason);
|
|
2039
|
+
}
|
|
2040
|
+
if (wasRunning) inFlight -= 1;
|
|
2041
|
+
emitSnapshot();
|
|
2042
|
+
try {
|
|
2043
|
+
entry.onSettled?.();
|
|
2044
|
+
} catch (err) {
|
|
2045
|
+
lastError = errorMessage3(err);
|
|
2046
|
+
}
|
|
2047
|
+
if (entry.mode === "request") {
|
|
2048
|
+
if (state === "completed") entry.resolve(value);
|
|
2049
|
+
else entry.reject(value);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
function enqueueMessage(message2, mode, signal, onSettled) {
|
|
2053
|
+
total += 1;
|
|
2054
|
+
const target = message2.target;
|
|
2055
|
+
if (!target) {
|
|
2056
|
+
failed += 1;
|
|
2057
|
+
lastError = `MessageBus.${mode === "request" ? "request" : "dispatch"} requires a target`;
|
|
2058
|
+
emitSnapshot();
|
|
2059
|
+
try {
|
|
2060
|
+
onSettled?.();
|
|
2061
|
+
} catch {
|
|
2062
|
+
}
|
|
2063
|
+
return mode === "request" ? Promise.reject(new Error(lastError)) : message2.id;
|
|
2064
|
+
}
|
|
2065
|
+
const handler = routedHandlers.get(message2.type);
|
|
2066
|
+
if (!handler || handler.target !== target) {
|
|
2067
|
+
failed += 1;
|
|
2068
|
+
lastError = `No handler registered for type "${message2.type}" at target "${target}"`;
|
|
2069
|
+
emitSnapshot();
|
|
2070
|
+
try {
|
|
2071
|
+
onSettled?.();
|
|
2072
|
+
} catch {
|
|
2073
|
+
}
|
|
2074
|
+
return mode === "request" ? Promise.reject(new Error(lastError)) : message2.id;
|
|
2075
|
+
}
|
|
2076
|
+
if (signal?.aborted) {
|
|
2077
|
+
canceled += 1;
|
|
2078
|
+
lastError = errorMessage3(signal.reason ?? new Error("aborted"));
|
|
2079
|
+
emitSnapshot();
|
|
2080
|
+
try {
|
|
2081
|
+
onSettled?.();
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
return mode === "request" ? Promise.reject(signal.reason ?? new Error("aborted")) : message2.id;
|
|
2085
|
+
}
|
|
2086
|
+
const ctl = new AbortController();
|
|
2087
|
+
let timeoutHandle;
|
|
2088
|
+
const onUpstreamAbort = () => {
|
|
2089
|
+
ctl.abort(signal?.reason ?? new Error("aborted"));
|
|
2090
|
+
};
|
|
2091
|
+
if (signal) {
|
|
2092
|
+
signal.addEventListener("abort", onUpstreamAbort, { once: true });
|
|
2093
|
+
}
|
|
2094
|
+
if (typeof message2.timeoutMs === "number" && message2.timeoutMs > 0) {
|
|
2095
|
+
timeoutHandle = setTimeout(() => {
|
|
2096
|
+
ctl.abort(new Error("MessageBus.request timeout"));
|
|
2097
|
+
}, message2.timeoutMs);
|
|
2098
|
+
}
|
|
2099
|
+
const stamped = { ...message2, signal: ctl.signal };
|
|
2100
|
+
const mailbox = mailboxes.get(target) ?? [];
|
|
2101
|
+
mailboxes.set(target, mailbox);
|
|
2102
|
+
return new Promise((resolve, reject) => {
|
|
2103
|
+
const entry = {
|
|
2104
|
+
message: stamped,
|
|
2105
|
+
signal: ctl.signal,
|
|
2106
|
+
mode,
|
|
2107
|
+
state: "queued",
|
|
2108
|
+
settled: false,
|
|
2109
|
+
resolve: (v) => resolve(v),
|
|
2110
|
+
reject: (e) => reject(e),
|
|
2111
|
+
onSettled,
|
|
2112
|
+
cleanup: () => {
|
|
2113
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2114
|
+
if (signal) signal.removeEventListener("abort", onUpstreamAbort);
|
|
2115
|
+
ctl.signal.removeEventListener("abort", onAbort);
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
function onAbort() {
|
|
2119
|
+
if (entry.state === "queued") {
|
|
2120
|
+
const idx = mailbox.indexOf(entry);
|
|
2121
|
+
if (idx >= 0) {
|
|
2122
|
+
mailbox.splice(idx, 1);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
settleEntry(entry, "canceled", ctl.signal.reason ?? new Error("aborted"));
|
|
2126
|
+
}
|
|
2127
|
+
ctl.signal.addEventListener("abort", onAbort, { once: true });
|
|
2128
|
+
mailbox.push(entry);
|
|
2129
|
+
emitSnapshot();
|
|
2130
|
+
schedulePump(target);
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
function schedulePump(target) {
|
|
2134
|
+
if (pumping.has(target)) return;
|
|
2135
|
+
const mailbox = mailboxes.get(target);
|
|
2136
|
+
if (!mailbox || mailbox.length === 0) return;
|
|
2137
|
+
pumping.add(target);
|
|
2138
|
+
if (typeof queueMicrotask === "function") {
|
|
2139
|
+
queueMicrotask(() => {
|
|
2140
|
+
void runPump(target);
|
|
2141
|
+
});
|
|
2142
|
+
} else {
|
|
2143
|
+
Promise.resolve().then(() => {
|
|
2144
|
+
void runPump(target);
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
async function runPump(target) {
|
|
2149
|
+
const concurrency = targetConcurrency.get(target) ?? 1;
|
|
2150
|
+
const workers = [];
|
|
2151
|
+
for (let i = 0; i < concurrency; i += 1) {
|
|
2152
|
+
workers.push(workerLoop(target));
|
|
2153
|
+
}
|
|
2154
|
+
try {
|
|
2155
|
+
await Promise.all(workers);
|
|
2156
|
+
} finally {
|
|
2157
|
+
pumping.delete(target);
|
|
2158
|
+
const mailbox = mailboxes.get(target);
|
|
2159
|
+
if (mailbox && mailbox.length > 0) {
|
|
2160
|
+
schedulePump(target);
|
|
2161
|
+
}
|
|
2162
|
+
emitSnapshot();
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
async function workerLoop(target, workerId) {
|
|
2166
|
+
while (true) {
|
|
2167
|
+
const entry = pickBestEntry(target);
|
|
2168
|
+
if (!entry) return;
|
|
2169
|
+
await processEntry(target, entry);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function pickBestEntry(target) {
|
|
2173
|
+
const mailbox = mailboxes.get(target);
|
|
2174
|
+
if (!mailbox || mailbox.length === 0) return void 0;
|
|
2175
|
+
let bestIdx = -1;
|
|
2176
|
+
let bestEntry;
|
|
2177
|
+
for (let i = 0; i < mailbox.length; i += 1) {
|
|
2178
|
+
const cur = mailbox[i];
|
|
2179
|
+
if (cur.settled) continue;
|
|
2180
|
+
if (!bestEntry) {
|
|
2181
|
+
bestEntry = cur;
|
|
2182
|
+
bestIdx = i;
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
const bestPriority = bestEntry.message.priority ?? 0;
|
|
2186
|
+
const curPriority = cur.message.priority ?? 0;
|
|
2187
|
+
if (curPriority > bestPriority || curPriority === bestPriority && cur.message.createdAt < bestEntry.message.createdAt) {
|
|
2188
|
+
bestEntry = cur;
|
|
2189
|
+
bestIdx = i;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
if (bestEntry && bestIdx >= 0) {
|
|
2193
|
+
mailbox.splice(bestIdx, 1);
|
|
2194
|
+
}
|
|
2195
|
+
return bestEntry;
|
|
2196
|
+
}
|
|
2197
|
+
async function processEntry(target, entry) {
|
|
2198
|
+
if (entry.settled) return;
|
|
2199
|
+
entry.state = "running";
|
|
2200
|
+
inFlight += 1;
|
|
2201
|
+
emitSnapshot();
|
|
2202
|
+
const handler = routedHandlers.get(entry.message.type);
|
|
2203
|
+
if (!handler) {
|
|
2204
|
+
settleEntry(entry, "failed", new Error(`No handler for type "${entry.message.type}"`));
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
const result = handler.handler(entry.message);
|
|
2209
|
+
if (result && typeof result.then === "function") {
|
|
2210
|
+
const v = await waitForHandler(
|
|
2211
|
+
result,
|
|
2212
|
+
entry.signal
|
|
2213
|
+
);
|
|
2214
|
+
if (entry.signal.aborted) {
|
|
2215
|
+
settleEntry(entry, "canceled", entry.signal.reason);
|
|
2216
|
+
} else {
|
|
2217
|
+
settleEntry(entry, "completed", v);
|
|
2218
|
+
}
|
|
2219
|
+
} else {
|
|
2220
|
+
if (entry.signal.aborted) {
|
|
2221
|
+
settleEntry(entry, "canceled", entry.signal.reason);
|
|
2222
|
+
} else {
|
|
2223
|
+
settleEntry(entry, "completed", result);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
} catch (err) {
|
|
2227
|
+
if (entry.signal.aborted) {
|
|
2228
|
+
settleEntry(entry, "canceled", entry.signal.reason ?? err);
|
|
2229
|
+
} else {
|
|
2230
|
+
settleEntry(entry, "failed", err);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const bus = {
|
|
2235
|
+
publish(type, payload, options) {
|
|
2236
|
+
const message2 = makeMessage(type, "event", payload, {
|
|
2237
|
+
causationId: options?.causationId,
|
|
2238
|
+
messageId: options?.messageId
|
|
2239
|
+
});
|
|
2240
|
+
return publishInternal(message2);
|
|
2241
|
+
},
|
|
2242
|
+
subscribe(type, handler) {
|
|
2243
|
+
const record2 = { type, handler };
|
|
2244
|
+
let bucket = subscriptions.get(type);
|
|
2245
|
+
if (!bucket) {
|
|
2246
|
+
bucket = /* @__PURE__ */ new Set();
|
|
2247
|
+
subscriptions.set(type, bucket);
|
|
2248
|
+
}
|
|
2249
|
+
bucket.add(record2);
|
|
2250
|
+
return () => {
|
|
2251
|
+
bucket?.delete(record2);
|
|
2252
|
+
};
|
|
2253
|
+
},
|
|
2254
|
+
dispatch(type, payload, options) {
|
|
2255
|
+
const message2 = makeMessage(type, "command", payload, {
|
|
2256
|
+
target: options.target,
|
|
2257
|
+
priority: options.priority,
|
|
2258
|
+
timeoutMs: options.timeoutMs,
|
|
2259
|
+
causationId: options.causationId,
|
|
2260
|
+
messageId: options.messageId
|
|
2261
|
+
});
|
|
2262
|
+
const result = enqueueMessage(message2, "command", options.signal, options.onSettled);
|
|
2263
|
+
if (typeof result === "string") return result;
|
|
2264
|
+
return message2.id;
|
|
2265
|
+
},
|
|
2266
|
+
request(type, payload, options) {
|
|
2267
|
+
const message2 = makeMessage(type, "request", payload, {
|
|
2268
|
+
target: options.target,
|
|
2269
|
+
priority: options.priority,
|
|
2270
|
+
timeoutMs: options.timeoutMs,
|
|
2271
|
+
causationId: options.causationId,
|
|
2272
|
+
messageId: options.messageId
|
|
2273
|
+
});
|
|
2274
|
+
const result = enqueueMessage(message2, "request", options.signal, options.onSettled);
|
|
2275
|
+
if (typeof result === "string") {
|
|
2276
|
+
return Promise.reject(new Error("MessageBus.request returned a string id unexpectedly"));
|
|
2277
|
+
}
|
|
2278
|
+
return result;
|
|
2279
|
+
},
|
|
2280
|
+
handle(type, handler, options) {
|
|
2281
|
+
const concurrency = options?.concurrency ?? 1;
|
|
2282
|
+
if (!Number.isFinite(concurrency) || !Number.isInteger(concurrency) || concurrency <= 0) {
|
|
2283
|
+
throw new Error("Handler concurrency must be a positive integer");
|
|
2284
|
+
}
|
|
2285
|
+
if (concurrency > MAX_HANDLER_CONCURRENCY) {
|
|
2286
|
+
throw new Error(
|
|
2287
|
+
`Handler concurrency must not exceed ${MAX_HANDLER_CONCURRENCY}`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
const record2 = {
|
|
2291
|
+
type,
|
|
2292
|
+
target: options?.target ?? "",
|
|
2293
|
+
priority: options?.priority ?? 0,
|
|
2294
|
+
concurrency,
|
|
2295
|
+
handler
|
|
2296
|
+
};
|
|
2297
|
+
if (routedHandlers.has(type)) {
|
|
2298
|
+
throw new Error(`Handler for "${type}" is already registered`);
|
|
2299
|
+
}
|
|
2300
|
+
if (record2.target) {
|
|
2301
|
+
const existing = targetConcurrency.get(record2.target);
|
|
2302
|
+
if (existing !== void 0 && existing !== concurrency) {
|
|
2303
|
+
throw new Error(`Conflicting concurrency for target "${record2.target}"`);
|
|
2304
|
+
}
|
|
2305
|
+
targetConcurrency.set(record2.target, concurrency);
|
|
2306
|
+
targetHandlerCount.set(record2.target, (targetHandlerCount.get(record2.target) ?? 0) + 1);
|
|
2307
|
+
if (!mailboxes.has(record2.target)) {
|
|
2308
|
+
mailboxes.set(record2.target, []);
|
|
2309
|
+
}
|
|
2310
|
+
schedulePump(record2.target);
|
|
2311
|
+
}
|
|
2312
|
+
routedHandlers.set(type, record2);
|
|
2313
|
+
return () => {
|
|
2314
|
+
if (routedHandlers.get(type) === record2) {
|
|
2315
|
+
routedHandlers.delete(type);
|
|
2316
|
+
if (record2.target) {
|
|
2317
|
+
const count = (targetHandlerCount.get(record2.target) ?? 1) - 1;
|
|
2318
|
+
if (count <= 0) {
|
|
2319
|
+
targetHandlerCount.delete(record2.target);
|
|
2320
|
+
targetConcurrency.delete(record2.target);
|
|
2321
|
+
} else {
|
|
2322
|
+
targetHandlerCount.set(record2.target, count);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
},
|
|
2328
|
+
snapshot,
|
|
2329
|
+
onSnapshot(handler) {
|
|
2330
|
+
snapshotListeners.add(handler);
|
|
2331
|
+
handler(snapshot());
|
|
2332
|
+
return () => snapshotListeners.delete(handler);
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
return bus;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// src/resources/resourceRegistry.ts
|
|
2339
|
+
function createResourceRegistry() {
|
|
2340
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
2341
|
+
function registerOwned(ownerId, definition) {
|
|
2342
|
+
if (definitions.has(definition.id)) {
|
|
2343
|
+
throw new Error(
|
|
2344
|
+
`Resource definition "${definition.id}" is already registered`
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
const owned = Object.assign({}, definition);
|
|
2348
|
+
Object.defineProperty(owned, RESOURCE_OWNER, {
|
|
2349
|
+
value: ownerId,
|
|
2350
|
+
enumerable: false,
|
|
2351
|
+
writable: false,
|
|
2352
|
+
configurable: false
|
|
2353
|
+
});
|
|
2354
|
+
definitions.set(definition.id, owned);
|
|
2355
|
+
}
|
|
2356
|
+
return {
|
|
2357
|
+
_registerOwned: registerOwned,
|
|
2358
|
+
register(definition) {
|
|
2359
|
+
registerOwned("", definition);
|
|
2360
|
+
},
|
|
2361
|
+
unregister(id) {
|
|
2362
|
+
definitions.delete(id);
|
|
2363
|
+
},
|
|
2364
|
+
get(id) {
|
|
2365
|
+
return definitions.get(id);
|
|
2366
|
+
},
|
|
2367
|
+
/** 获取所有已注册的资源定义 id(用于 ownership 快照) */
|
|
2368
|
+
_ids() {
|
|
2369
|
+
return Array.from(definitions.keys());
|
|
2370
|
+
}
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
function registerOwnedResource(registry, ownerId, definition) {
|
|
2374
|
+
const internal = registry;
|
|
2375
|
+
if (!internal._registerOwned) {
|
|
2376
|
+
registry.register(definition);
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
internal._registerOwned(ownerId, definition);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// src/resources/resourceStore.ts
|
|
2383
|
+
function createContext(ownerId, getCapability, getAttributes) {
|
|
2384
|
+
return {
|
|
2385
|
+
getCapability,
|
|
2386
|
+
attributes: getAttributes(ownerId),
|
|
2387
|
+
ownerId
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
function recordKey(definitionId, key) {
|
|
2391
|
+
return `${definitionId}::${key.join("::")}`;
|
|
2392
|
+
}
|
|
2393
|
+
function defaultEquals(a, b) {
|
|
2394
|
+
return Object.is(a, b);
|
|
2395
|
+
}
|
|
2396
|
+
function createResourceStore(registry, getCapability, getAttributes = () => ({})) {
|
|
2397
|
+
const records = /* @__PURE__ */ new Map();
|
|
2398
|
+
const microtaskQueue = /* @__PURE__ */ new Map();
|
|
2399
|
+
const contextSubscribers = /* @__PURE__ */ new Set();
|
|
2400
|
+
let microtaskScheduled = false;
|
|
2401
|
+
const notify = (record2) => {
|
|
2402
|
+
for (const subscriber of [...record2.subscribers]) {
|
|
2403
|
+
try {
|
|
2404
|
+
subscriber();
|
|
2405
|
+
} catch {
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
const notifyContext = () => {
|
|
2410
|
+
for (const subscriber of [...contextSubscribers]) {
|
|
2411
|
+
try {
|
|
2412
|
+
subscriber();
|
|
2413
|
+
} catch {
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
const cleanupRecord = (record2) => {
|
|
2418
|
+
record2.abortController?.abort();
|
|
2419
|
+
record2.providerUnsubscribe?.();
|
|
2420
|
+
record2.providerUnsubscribe = null;
|
|
2421
|
+
};
|
|
2422
|
+
const contextFor = (definition) => createContext(
|
|
2423
|
+
definition[RESOURCE_OWNER] ?? "__unowned__",
|
|
2424
|
+
getCapability,
|
|
2425
|
+
getAttributes
|
|
2426
|
+
);
|
|
2427
|
+
const getOrCreateRecord = (definition, args) => {
|
|
2428
|
+
const context = contextFor(definition);
|
|
2429
|
+
const key = definition.key(args, context);
|
|
2430
|
+
const keyString = recordKey(definition.id, key);
|
|
2431
|
+
const current = records.get(keyString);
|
|
2432
|
+
if (current) {
|
|
2433
|
+
if (definition.subscribe && !current.providerUnsubscribe) {
|
|
2434
|
+
current.providerUnsubscribe = definition.subscribe(args, context, () => {
|
|
2435
|
+
scheduleInvalidation(definition.id, args);
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
return current;
|
|
2439
|
+
}
|
|
2440
|
+
const record2 = {
|
|
2441
|
+
snapshot: { key, status: "pending", data: void 0, revision: 0 },
|
|
2442
|
+
inFlight: null,
|
|
2443
|
+
abortController: null,
|
|
2444
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
2445
|
+
invalidationScheduled: false,
|
|
2446
|
+
loadRevision: 0,
|
|
2447
|
+
owner: definition[RESOURCE_OWNER] ?? "__unowned__",
|
|
2448
|
+
providerUnsubscribe: null
|
|
2449
|
+
};
|
|
2450
|
+
records.set(keyString, record2);
|
|
2451
|
+
if (definition.subscribe) {
|
|
2452
|
+
record2.providerUnsubscribe = definition.subscribe(args, context, () => {
|
|
2453
|
+
scheduleInvalidation(definition.id, args);
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
return record2;
|
|
2457
|
+
};
|
|
2458
|
+
const loadResource = (definition, args, record2) => {
|
|
2459
|
+
record2.abortController?.abort();
|
|
2460
|
+
const context = contextFor(definition);
|
|
2461
|
+
const key = definition.key(args, context);
|
|
2462
|
+
if (record2.snapshot.key.join("::") !== key.join("::")) return;
|
|
2463
|
+
const abortController = new AbortController();
|
|
2464
|
+
const loadRevision = ++record2.loadRevision;
|
|
2465
|
+
record2.abortController = abortController;
|
|
2466
|
+
try {
|
|
2467
|
+
record2.inFlight = Promise.resolve(definition.load(args, context, abortController.signal));
|
|
2468
|
+
} catch (error2) {
|
|
2469
|
+
record2.inFlight = Promise.reject(error2);
|
|
2470
|
+
}
|
|
2471
|
+
record2.snapshot = { ...record2.snapshot, status: "pending", revision: record2.snapshot.revision + 1 };
|
|
2472
|
+
notify(record2);
|
|
2473
|
+
record2.inFlight.then((data) => {
|
|
2474
|
+
if (abortController.signal.aborted || record2.loadRevision !== loadRevision) return;
|
|
2475
|
+
if (record2.snapshot.key.join("::") !== key.join("::")) return;
|
|
2476
|
+
const equals = definition.equals ?? defaultEquals;
|
|
2477
|
+
const changed = !equals(record2.snapshot.data, data);
|
|
2478
|
+
record2.snapshot = {
|
|
2479
|
+
key,
|
|
2480
|
+
status: "ready",
|
|
2481
|
+
data,
|
|
2482
|
+
revision: changed ? record2.snapshot.revision + 1 : record2.snapshot.revision
|
|
2483
|
+
};
|
|
2484
|
+
record2.inFlight = null;
|
|
2485
|
+
record2.abortController = null;
|
|
2486
|
+
if (changed) notify(record2);
|
|
2487
|
+
}).catch((error2) => {
|
|
2488
|
+
if (abortController.signal.aborted || record2.loadRevision !== loadRevision) return;
|
|
2489
|
+
const blocked = error2 instanceof Error && error2.message === "blocked";
|
|
2490
|
+
const errorValue = error2 instanceof Error ? error2 : new Error(String(error2));
|
|
2491
|
+
record2.snapshot = blocked ? { ...record2.snapshot, status: "blocked", revision: record2.snapshot.revision + 1 } : {
|
|
2492
|
+
...record2.snapshot,
|
|
2493
|
+
status: record2.snapshot.data === void 0 ? "error" : "stale",
|
|
2494
|
+
error: {
|
|
2495
|
+
code: typeof errorValue.code === "string" ? String(errorValue.code) : "resource.load_failed",
|
|
2496
|
+
message: errorValue.message
|
|
2497
|
+
},
|
|
2498
|
+
revision: record2.snapshot.revision + 1
|
|
2499
|
+
};
|
|
2500
|
+
record2.inFlight = null;
|
|
2501
|
+
record2.abortController = null;
|
|
2502
|
+
notify(record2);
|
|
2503
|
+
});
|
|
2504
|
+
};
|
|
2505
|
+
const invalidateNow = (definitionId, args) => {
|
|
2506
|
+
const definition = registry.get(definitionId);
|
|
2507
|
+
if (!definition) return;
|
|
2508
|
+
const context = contextFor(definition);
|
|
2509
|
+
const key = definition.key(args, context);
|
|
2510
|
+
const record2 = records.get(recordKey(definitionId, key));
|
|
2511
|
+
if (!record2) return;
|
|
2512
|
+
record2.snapshot = { ...record2.snapshot, status: "stale", revision: record2.snapshot.revision + 1 };
|
|
2513
|
+
notify(record2);
|
|
2514
|
+
loadResource(definition, args, record2);
|
|
2515
|
+
};
|
|
2516
|
+
const flushInvalidations = () => {
|
|
2517
|
+
const queue = [...microtaskQueue.values()];
|
|
2518
|
+
microtaskQueue.clear();
|
|
2519
|
+
microtaskScheduled = false;
|
|
2520
|
+
for (const item of queue) invalidateNow(item.definitionId, item.args);
|
|
2521
|
+
};
|
|
2522
|
+
const scheduleInvalidation = (definitionId, args) => {
|
|
2523
|
+
const definition = registry.get(definitionId);
|
|
2524
|
+
if (!definition) return;
|
|
2525
|
+
if (definition.invalidation === "immediate") {
|
|
2526
|
+
invalidateNow(definitionId, args);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
const key = recordKey(definitionId, definition.key(args, contextFor(definition)));
|
|
2530
|
+
if (microtaskQueue.has(key)) return;
|
|
2531
|
+
microtaskQueue.set(key, { definitionId, args });
|
|
2532
|
+
if (!microtaskScheduled) {
|
|
2533
|
+
microtaskScheduled = true;
|
|
2534
|
+
queueMicrotask(flushInvalidations);
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
const refreshRuntimeBindings = () => {
|
|
2538
|
+
for (const record2 of records.values()) cleanupRecord(record2);
|
|
2539
|
+
records.clear();
|
|
2540
|
+
notifyContext();
|
|
2541
|
+
};
|
|
2542
|
+
return {
|
|
2543
|
+
ensure(definitionId, args) {
|
|
2544
|
+
const definition = registry.get(definitionId);
|
|
2545
|
+
if (!definition) throw new Error(`Resource definition "${definitionId}" not found`);
|
|
2546
|
+
const record2 = getOrCreateRecord(definition, args);
|
|
2547
|
+
if (!record2.inFlight && record2.snapshot.status === "pending") loadResource(definition, args, record2);
|
|
2548
|
+
return record2.snapshot;
|
|
2549
|
+
},
|
|
2550
|
+
subscribe(definitionId, args, callback) {
|
|
2551
|
+
const definition = registry.get(definitionId);
|
|
2552
|
+
if (!definition) return () => void 0;
|
|
2553
|
+
let record2 = getOrCreateRecord(definition, args);
|
|
2554
|
+
record2.subscribers.add(callback);
|
|
2555
|
+
const removeContext = definition.scope === "context" ? (() => {
|
|
2556
|
+
const listener = () => {
|
|
2557
|
+
record2.subscribers.delete(callback);
|
|
2558
|
+
record2 = getOrCreateRecord(definition, args);
|
|
2559
|
+
record2.subscribers.add(callback);
|
|
2560
|
+
callback();
|
|
2561
|
+
};
|
|
2562
|
+
contextSubscribers.add(listener);
|
|
2563
|
+
return () => contextSubscribers.delete(listener);
|
|
2564
|
+
})() : void 0;
|
|
2565
|
+
return () => {
|
|
2566
|
+
removeContext?.();
|
|
2567
|
+
record2.subscribers.delete(callback);
|
|
2568
|
+
if (record2.subscribers.size === 0 && record2.providerUnsubscribe) {
|
|
2569
|
+
record2.providerUnsubscribe();
|
|
2570
|
+
record2.providerUnsubscribe = null;
|
|
2571
|
+
}
|
|
2572
|
+
if (record2.subscribers.size === 0 && record2.abortController) {
|
|
2573
|
+
const current = record2;
|
|
2574
|
+
setTimeout(() => {
|
|
2575
|
+
if (current.subscribers.size === 0 && current.abortController) {
|
|
2576
|
+
current.abortController.abort();
|
|
2577
|
+
current.abortController = null;
|
|
2578
|
+
current.inFlight = null;
|
|
2579
|
+
}
|
|
2580
|
+
}, 100);
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
},
|
|
2584
|
+
read(definitionId, args) {
|
|
2585
|
+
const definition = registry.get(definitionId);
|
|
2586
|
+
if (!definition) return void 0;
|
|
2587
|
+
const key = definition.key(args, contextFor(definition));
|
|
2588
|
+
return records.get(recordKey(definitionId, key))?.snapshot;
|
|
2589
|
+
},
|
|
2590
|
+
invalidate: scheduleInvalidation,
|
|
2591
|
+
disposeOwner(ownerId) {
|
|
2592
|
+
for (const [key, record2] of records) {
|
|
2593
|
+
if (record2.owner !== ownerId) continue;
|
|
2594
|
+
cleanupRecord(record2);
|
|
2595
|
+
records.delete(key);
|
|
2596
|
+
}
|
|
2597
|
+
},
|
|
2598
|
+
refreshRuntimeBindings,
|
|
2599
|
+
subscribeContext(callback) {
|
|
2600
|
+
contextSubscribers.add(callback);
|
|
2601
|
+
return () => contextSubscribers.delete(callback);
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/lifecycle/scopedMessageBus.ts
|
|
2607
|
+
function mergeSignals(scopeSignal, requestSignal) {
|
|
2608
|
+
if (!requestSignal) return { signal: scopeSignal, dispose: () => void 0 };
|
|
2609
|
+
if (scopeSignal.aborted) {
|
|
2610
|
+
const controller2 = new AbortController();
|
|
2611
|
+
controller2.abort(scopeSignal.reason);
|
|
2612
|
+
return { signal: controller2.signal, dispose: () => void 0 };
|
|
2613
|
+
}
|
|
2614
|
+
if (requestSignal.aborted) {
|
|
2615
|
+
const controller2 = new AbortController();
|
|
2616
|
+
controller2.abort(requestSignal.reason);
|
|
2617
|
+
return { signal: controller2.signal, dispose: () => void 0 };
|
|
2618
|
+
}
|
|
2619
|
+
const controller = new AbortController();
|
|
2620
|
+
const abortFrom = (source) => {
|
|
2621
|
+
try {
|
|
2622
|
+
controller.abort(source.reason);
|
|
2623
|
+
} catch {
|
|
2624
|
+
controller.abort();
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
const onScopeAbort = () => abortFrom(scopeSignal);
|
|
2628
|
+
const onRequestAbort = () => abortFrom(requestSignal);
|
|
2629
|
+
scopeSignal.addEventListener("abort", onScopeAbort, { once: true });
|
|
2630
|
+
requestSignal.addEventListener("abort", onRequestAbort, { once: true });
|
|
2631
|
+
return {
|
|
2632
|
+
signal: controller.signal,
|
|
2633
|
+
dispose: () => {
|
|
2634
|
+
scopeSignal.removeEventListener("abort", onScopeAbort);
|
|
2635
|
+
requestSignal.removeEventListener("abort", onRequestAbort);
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
function withScopeCleanup(scope, cleanup) {
|
|
2640
|
+
let active = true;
|
|
2641
|
+
let removeRevoke = () => void 0;
|
|
2642
|
+
let removeDispose = () => void 0;
|
|
2643
|
+
const runCleanup = () => {
|
|
2644
|
+
if (!active) return;
|
|
2645
|
+
active = false;
|
|
2646
|
+
removeRevoke();
|
|
2647
|
+
removeDispose();
|
|
2648
|
+
cleanup();
|
|
2649
|
+
};
|
|
2650
|
+
removeRevoke = scope.onRevoke(runCleanup);
|
|
2651
|
+
removeDispose = scope.onDispose(runCleanup, "message-bus-registration");
|
|
2652
|
+
return () => {
|
|
2653
|
+
if (!active) return;
|
|
2654
|
+
active = false;
|
|
2655
|
+
removeRevoke();
|
|
2656
|
+
removeDispose();
|
|
2657
|
+
cleanup();
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
function createScopedMessageBus(base, scope) {
|
|
2661
|
+
return {
|
|
2662
|
+
publish(type, payload, options) {
|
|
2663
|
+
scope.assertActive();
|
|
2664
|
+
return base.publish(type, payload, options);
|
|
2665
|
+
},
|
|
2666
|
+
subscribe(type, handler) {
|
|
2667
|
+
scope.assertActive();
|
|
2668
|
+
const unsubscribe = base.subscribe(type, (payload) => {
|
|
2669
|
+
if (scope.state !== "active") return;
|
|
2670
|
+
handler(payload);
|
|
2671
|
+
});
|
|
2672
|
+
return withScopeCleanup(scope, unsubscribe);
|
|
2673
|
+
},
|
|
2674
|
+
dispatch(type, payload, options) {
|
|
2675
|
+
scope.assertActive();
|
|
2676
|
+
const merged = mergeSignals(scope.signal, options.signal);
|
|
2677
|
+
const cleanup = withScopeCleanup(scope, merged.dispose);
|
|
2678
|
+
try {
|
|
2679
|
+
return base.dispatch(type, payload, {
|
|
2680
|
+
...options,
|
|
2681
|
+
signal: merged.signal,
|
|
2682
|
+
onSettled: () => {
|
|
2683
|
+
cleanup();
|
|
2684
|
+
options.onSettled?.();
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
} catch (error2) {
|
|
2688
|
+
cleanup();
|
|
2689
|
+
throw error2;
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2692
|
+
request(type, payload, options) {
|
|
2693
|
+
scope.assertActive();
|
|
2694
|
+
const merged = mergeSignals(scope.signal, options.signal);
|
|
2695
|
+
const cleanup = withScopeCleanup(scope, merged.dispose);
|
|
2696
|
+
try {
|
|
2697
|
+
const request = base.request(type, payload, {
|
|
2698
|
+
...options,
|
|
2699
|
+
signal: merged.signal,
|
|
2700
|
+
onSettled: () => {
|
|
2701
|
+
cleanup();
|
|
2702
|
+
options.onSettled?.();
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
return request.finally(cleanup);
|
|
2706
|
+
} catch (error2) {
|
|
2707
|
+
cleanup();
|
|
2708
|
+
return Promise.reject(error2);
|
|
2709
|
+
}
|
|
2710
|
+
},
|
|
2711
|
+
handle(type, handler, options) {
|
|
2712
|
+
scope.assertActive();
|
|
2713
|
+
const scopedHandler = (message2) => {
|
|
2714
|
+
if (scope.state !== "active") {
|
|
2715
|
+
throw new LifecycleScopeRevokedError(
|
|
2716
|
+
`Lifecycle scope "${scope.identity.scopeId}" is ${scope.state}`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
const merged = mergeSignals(scope.signal, message2.signal);
|
|
2720
|
+
try {
|
|
2721
|
+
const result = handler({ ...message2, signal: merged.signal });
|
|
2722
|
+
if (result && typeof result.then === "function") {
|
|
2723
|
+
return Promise.resolve(result).finally(merged.dispose);
|
|
2724
|
+
}
|
|
2725
|
+
merged.dispose();
|
|
2726
|
+
return result;
|
|
2727
|
+
} catch (error2) {
|
|
2728
|
+
merged.dispose();
|
|
2729
|
+
throw error2;
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
const unregister = base.handle(type, scopedHandler, options);
|
|
2733
|
+
return withScopeCleanup(scope, unregister);
|
|
2734
|
+
},
|
|
2735
|
+
snapshot() {
|
|
2736
|
+
return base.snapshot();
|
|
2737
|
+
},
|
|
2738
|
+
onSnapshot(handler) {
|
|
2739
|
+
scope.assertActive();
|
|
2740
|
+
const unsubscribe = base.onSnapshot(handler);
|
|
2741
|
+
return withScopeCleanup(scope, unsubscribe);
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// src/host/createPluginHost.ts
|
|
2747
|
+
function createInMemoryPluginConfigStore(initial = {}, readOnly = false) {
|
|
2748
|
+
const values = new Map(Object.entries(initial).filter((entry) => typeof entry[1] === "boolean"));
|
|
2749
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2750
|
+
const snapshot = () => Object.freeze(Object.fromEntries(values));
|
|
2751
|
+
return {
|
|
2752
|
+
read: snapshot,
|
|
2753
|
+
setEnabled(pluginId, enabled) {
|
|
2754
|
+
if (readOnly || values.get(pluginId) === enabled) return;
|
|
2755
|
+
values.set(pluginId, enabled);
|
|
2756
|
+
const next = snapshot();
|
|
2757
|
+
for (const listener of [...listeners]) listener(next);
|
|
2758
|
+
},
|
|
2759
|
+
subscribe(listener) {
|
|
2760
|
+
listeners.add(listener);
|
|
2761
|
+
return () => listeners.delete(listener);
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
var StartupCapabilityError = class extends Error {
|
|
2766
|
+
details;
|
|
2767
|
+
constructor(details, phase2 = "startup") {
|
|
2768
|
+
const labels = details.map((item) => typeof item.capability === "string" ? item.capability : item.capability.id);
|
|
2769
|
+
super(`Startup prerequisite unavailable during ${phase2}${labels.length > 0 ? `: ${labels.join(", ")}` : ""}`);
|
|
2770
|
+
this.name = "StartupCapabilityError";
|
|
2771
|
+
this.details = Object.freeze([...details]);
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
var StartupPluginError = class extends Error {
|
|
2775
|
+
details;
|
|
2776
|
+
constructor(details) {
|
|
2777
|
+
super(`Startup plugin failed: ${details.pluginId}`);
|
|
2778
|
+
this.name = "StartupPluginError";
|
|
2779
|
+
this.details = Object.freeze({ ...details });
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
function errorMessage4(error2) {
|
|
2783
|
+
return error2 instanceof Error ? error2.message : typeof error2 === "string" ? error2 : String(error2);
|
|
2784
|
+
}
|
|
2785
|
+
function makeId2(prefix) {
|
|
2786
|
+
try {
|
|
2787
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `${prefix}:${crypto.randomUUID()}`;
|
|
2788
|
+
} catch {
|
|
2789
|
+
}
|
|
2790
|
+
return `${prefix}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
2791
|
+
}
|
|
2792
|
+
function lifecycleStateFor(state, desired) {
|
|
2793
|
+
switch (state) {
|
|
2794
|
+
case "starting":
|
|
2795
|
+
return "starting";
|
|
2796
|
+
case "stopping":
|
|
2797
|
+
return "stopping";
|
|
2798
|
+
case "enabled":
|
|
2799
|
+
return "running";
|
|
2800
|
+
case "blocked":
|
|
2801
|
+
return "waiting";
|
|
2802
|
+
case "error-disabled":
|
|
2803
|
+
case "cleanup-pending":
|
|
2804
|
+
return "failed";
|
|
2805
|
+
case "disabled":
|
|
2806
|
+
return "disabled";
|
|
2807
|
+
case "registered":
|
|
2808
|
+
case "unknown":
|
|
2809
|
+
return desired ? "waiting" : "disabled";
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
function implementationKey(pluginId, unitId) {
|
|
2813
|
+
return `${pluginId}\0${unitId}`;
|
|
2814
|
+
}
|
|
2815
|
+
function isRemote(capability) {
|
|
2816
|
+
return capability.kind === "rpc" || capability.kind === "stream";
|
|
2817
|
+
}
|
|
2818
|
+
function mergeSignals2(...signals) {
|
|
2819
|
+
const active = signals.filter((signal) => signal !== void 0);
|
|
2820
|
+
const controller = new AbortController();
|
|
2821
|
+
const abort = (signal) => {
|
|
2822
|
+
try {
|
|
2823
|
+
controller.abort(signal.reason);
|
|
2824
|
+
} catch {
|
|
2825
|
+
controller.abort();
|
|
2826
|
+
}
|
|
2827
|
+
};
|
|
2828
|
+
const listeners = active.map((signal) => {
|
|
2829
|
+
const listener = () => abort(signal);
|
|
2830
|
+
if (signal.aborted) abort(signal);
|
|
2831
|
+
else signal.addEventListener("abort", listener, { once: true });
|
|
2832
|
+
return { signal, listener };
|
|
2833
|
+
});
|
|
2834
|
+
return {
|
|
2835
|
+
signal: controller.signal,
|
|
2836
|
+
dispose() {
|
|
2837
|
+
for (const item of listeners) item.signal.removeEventListener("abort", item.listener);
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
function assertTimeout(value) {
|
|
2842
|
+
const result = value ?? 3e4;
|
|
2843
|
+
if (!Number.isFinite(result) || result < 1 || result > 3e5) throw new TypeError("timeoutMs must be a finite number from 1 to 300000");
|
|
2844
|
+
return result;
|
|
2845
|
+
}
|
|
2846
|
+
function rejectedStreamSubscription(error2) {
|
|
2847
|
+
let rejectReady;
|
|
2848
|
+
let rejectClosed;
|
|
2849
|
+
const ready = new Promise((_, reject) => {
|
|
2850
|
+
rejectReady = reject;
|
|
2851
|
+
});
|
|
2852
|
+
const closed = new Promise((_, reject) => {
|
|
2853
|
+
rejectClosed = reject;
|
|
2854
|
+
});
|
|
2855
|
+
void ready.catch(() => void 0);
|
|
2856
|
+
void closed.catch(() => void 0);
|
|
2857
|
+
rejectReady(error2);
|
|
2858
|
+
rejectClosed(error2);
|
|
2859
|
+
return { ready, closed, cancel() {
|
|
2860
|
+
} };
|
|
2861
|
+
}
|
|
2862
|
+
function createPluginHost(options = {}) {
|
|
2863
|
+
const configuredRuntime = options.runtime;
|
|
2864
|
+
const runtimeKind = configuredRuntime ?? "window-main";
|
|
2865
|
+
const runtimeId = options.runtimeId ?? `${runtimeKind}:runtime`;
|
|
2866
|
+
const runtimeInstanceId = options.runtimeInstanceId ?? makeId2(runtimeId);
|
|
2867
|
+
let capabilityBridge = options.capabilityBridge;
|
|
2868
|
+
let versionCounter = 0;
|
|
2869
|
+
let disposed = false;
|
|
2870
|
+
let disposePromise;
|
|
2871
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2872
|
+
const known = /* @__PURE__ */ new Map();
|
|
2873
|
+
const records = /* @__PURE__ */ new Map();
|
|
2874
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
2875
|
+
const stateCache = /* @__PURE__ */ new Map();
|
|
2876
|
+
const publicClients = /* @__PURE__ */ new Map();
|
|
2877
|
+
const configStore = options.configStore ?? createInMemoryPluginConfigStore(options.initialPluginConfig);
|
|
2878
|
+
const internalConfigWrites = /* @__PURE__ */ new Set();
|
|
2879
|
+
const starting = /* @__PURE__ */ new Map();
|
|
2880
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
2881
|
+
let start;
|
|
2882
|
+
let intentSnapshot = options.pluginIntentCoordinator?.snapshot();
|
|
2883
|
+
const bump = () => {
|
|
2884
|
+
versionCounter += 1;
|
|
2885
|
+
for (const listener of [...listeners]) {
|
|
2886
|
+
try {
|
|
2887
|
+
listener({ version: versionCounter });
|
|
2888
|
+
} catch {
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
};
|
|
2892
|
+
const rootScope = createLifecycleScope({ kind: "root", metadata: { attributes: Object.freeze({ ...options.rootAttributes ?? {}, runtimeId, runtimeInstanceId }) }, onChange: bump });
|
|
2893
|
+
const messageBus = options.messageBus ?? createMessageBus();
|
|
2894
|
+
const resourceRegistry = options.resourceRegistry ?? createResourceRegistry();
|
|
2895
|
+
const capabilities = createCapabilityRegistry();
|
|
2896
|
+
capabilities.provide(RESOURCE_REGISTRY, resourceRegistry, "host", rootScope);
|
|
2897
|
+
const taskScheduler = createScopedTaskScheduler(rootScope);
|
|
2898
|
+
const resourceStore = createResourceStore(resourceRegistry, options.resourceCapabilityResolver ?? (() => void 0), (ownerId) => {
|
|
2899
|
+
const instance = ownerId ? records.get(ownerId)?.scope : void 0;
|
|
2900
|
+
return instance?.identity.attributes ?? rootScope.identity.attributes;
|
|
2901
|
+
});
|
|
2902
|
+
const addHostCapabilities = (source) => {
|
|
2903
|
+
if (!source) return;
|
|
2904
|
+
let entries;
|
|
2905
|
+
if (Array.isArray(source)) entries = source;
|
|
2906
|
+
else {
|
|
2907
|
+
const map = source;
|
|
2908
|
+
entries = [...map.entries()].map(([capability, value]) => ({ capability, value }));
|
|
2909
|
+
}
|
|
2910
|
+
for (const item of entries) capabilities.provide(item.capability, item.value, "host", rootScope);
|
|
2911
|
+
};
|
|
2912
|
+
addHostCapabilities(options.capabilities);
|
|
2913
|
+
addHostCapabilities(options.builtinCapabilities);
|
|
2914
|
+
const manifestUnit = (manifest) => selectRuntimeUnit(manifest, configuredRuntime);
|
|
2915
|
+
const desired = (manifest) => {
|
|
2916
|
+
if (manifest.startup === "required" || manifest.canDisable === false) return true;
|
|
2917
|
+
if (intentSnapshot && Object.prototype.hasOwnProperty.call(intentSnapshot.desiredEnabled, manifest.id)) return intentSnapshot.desiredEnabled[manifest.id] === true;
|
|
2918
|
+
return configStore.read()[manifest.id] ?? manifest.defaultEnabled;
|
|
2919
|
+
};
|
|
2920
|
+
const desiredRevision = (pluginId) => intentSnapshot?.desiredRevision[pluginId];
|
|
2921
|
+
const writeConfigIntent = (pluginId, value) => {
|
|
2922
|
+
internalConfigWrites.add(pluginId);
|
|
2923
|
+
try {
|
|
2924
|
+
configStore.setEnabled(pluginId, value);
|
|
2925
|
+
} finally {
|
|
2926
|
+
internalConfigWrites.delete(pluginId);
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
const initializeConfigIntent = (manifest) => {
|
|
2930
|
+
if (options.pluginIntentCoordinator) return;
|
|
2931
|
+
const current = configStore.read();
|
|
2932
|
+
if (required(manifest)) {
|
|
2933
|
+
if (current[manifest.id] !== true) writeConfigIntent(manifest.id, true);
|
|
2934
|
+
} else if (!Object.prototype.hasOwnProperty.call(current, manifest.id)) {
|
|
2935
|
+
writeConfigIntent(manifest.id, manifest.defaultEnabled);
|
|
2936
|
+
}
|
|
2937
|
+
};
|
|
2938
|
+
const definitionList = (pluginId, unitId) => {
|
|
2939
|
+
const fromRegistry = options.runtimeUnitImplementationRegistry?.getCapabilities?.(pluginId, unitId);
|
|
2940
|
+
if (fromRegistry) return fromRegistry;
|
|
2941
|
+
return options.capabilityDefinitions?.get(implementationKey(pluginId, unitId)) ?? [];
|
|
2942
|
+
};
|
|
2943
|
+
const graph = () => buildPluginGraph([...known.values()], { runtime: configuredRuntime, enabledPluginIds: enabled, externalRuntimeDependencies: options.externalRuntimeDependencies, builtinCapabilities: new Set(capabilities.descriptors()) });
|
|
2944
|
+
const unavailableReason = (manifest) => {
|
|
2945
|
+
const unit = manifestUnit(manifest);
|
|
2946
|
+
if (!unit) return manifest.units && manifest.units.length > 0 ? "runtime_unit_ambiguous" : void 0;
|
|
2947
|
+
return options.runtimeUnitAvailability?.({ pluginId: manifest.id, unitId: unit.id, runtime: unit.runtime });
|
|
2948
|
+
};
|
|
2949
|
+
const validateManifest = (manifest) => {
|
|
2950
|
+
if (!manifest || typeof manifest.id !== "string" || manifest.id.trim() === "") throw new TypeError("Plugin id must be a non-empty string");
|
|
2951
|
+
if ((manifest.units?.length ?? 0) > 1 && configuredRuntime === void 0) {
|
|
2952
|
+
throw new Error(`Plugin "${manifest.id}" execution must be explicit for multi-unit manifests`);
|
|
2953
|
+
}
|
|
2954
|
+
options.manifestValidator?.(manifest);
|
|
2955
|
+
validatePluginGraph([manifest], { runtime: configuredRuntime, allowMissingDependencies: true });
|
|
2956
|
+
};
|
|
2957
|
+
const missingDependencies = (manifest) => {
|
|
2958
|
+
const unit = manifestUnit(manifest);
|
|
2959
|
+
if (!unit) return [];
|
|
2960
|
+
const localRuntime = unit.runtime;
|
|
2961
|
+
const current = graph();
|
|
2962
|
+
return (unit.dependencies ?? []).filter((dependency) => dependency.source !== "peer").filter((dependency) => {
|
|
2963
|
+
if (dependency.optional) return false;
|
|
2964
|
+
if (capabilities.has(dependency.capability)) return false;
|
|
2965
|
+
if ((current.providers[capabilityKey(dependency.capability)]?.length ?? 0) > 0) {
|
|
2966
|
+
const provider = current.providers[capabilityKey(dependency.capability)]?.[0];
|
|
2967
|
+
return provider === void 0 || !enabled.has(provider);
|
|
2968
|
+
}
|
|
2969
|
+
if (dependency.sourceRuntime !== localRuntime && options.externalRuntimeDependencies) return false;
|
|
2970
|
+
return true;
|
|
2971
|
+
}).map((dependency) => dependency.capability);
|
|
2972
|
+
};
|
|
2973
|
+
const required = (manifest) => manifest.startup === "required" || manifest.canDisable === false;
|
|
2974
|
+
const createUnavailableRemoteClient = (capability, scope) => {
|
|
2975
|
+
if (capability.kind === "rpc") return {
|
|
2976
|
+
call(request, _options) {
|
|
2977
|
+
scope.assertActive();
|
|
2978
|
+
return Promise.reject(new WebLoomError("capability_unavailable", `Capability "${capability.id}" is unavailable in the remote Runtime`, "wait", { capabilityId: capability.id }));
|
|
2979
|
+
}
|
|
2980
|
+
};
|
|
2981
|
+
return {
|
|
2982
|
+
subscribe(request, _options) {
|
|
2983
|
+
scope.assertActive();
|
|
2984
|
+
return rejectedStreamSubscription(new WebLoomError("capability_unavailable", `Capability "${capability.id}" is unavailable in the remote Runtime`, "wait", { capabilityId: capability.id }));
|
|
2985
|
+
}
|
|
2986
|
+
};
|
|
2987
|
+
};
|
|
2988
|
+
const getRemoteClient = (capability, scope, forceRemote = false) => {
|
|
2989
|
+
if (!forceRemote && capabilities.has(capability)) {
|
|
2990
|
+
if (capability.kind === "rpc") return createLocalRpcClient(capability, scope);
|
|
2991
|
+
return createLocalStreamClient(capability, scope);
|
|
2992
|
+
}
|
|
2993
|
+
if (capabilityBridge) return capabilityBridge.getClient(capability, scope);
|
|
2994
|
+
if (forceRemote) return createUnavailableRemoteClient(capability, scope);
|
|
2995
|
+
if (capability.kind === "rpc") return createLocalRpcClient(capability, scope);
|
|
2996
|
+
return createLocalStreamClient(capability, scope);
|
|
2997
|
+
};
|
|
2998
|
+
const createLocalRpcClient = (capability, scope) => {
|
|
2999
|
+
const client = {
|
|
3000
|
+
call(request, callOptions = {}) {
|
|
3001
|
+
scope.assertActive();
|
|
3002
|
+
const timeoutMs = assertTimeout(callOptions.timeoutMs);
|
|
3003
|
+
const timeoutController = new AbortController();
|
|
3004
|
+
const merged = mergeSignals2(timeoutController.signal, scope.signal, callOptions.signal);
|
|
3005
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
3006
|
+
return new Promise((resolve, reject) => {
|
|
3007
|
+
let settled = false;
|
|
3008
|
+
let timedOut = false;
|
|
3009
|
+
let timer;
|
|
3010
|
+
const finish = (error2, value) => {
|
|
3011
|
+
if (settled) return;
|
|
3012
|
+
settled = true;
|
|
3013
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3014
|
+
merged.dispose();
|
|
3015
|
+
if (error2 !== void 0) reject(error2);
|
|
3016
|
+
else resolve(value);
|
|
3017
|
+
};
|
|
3018
|
+
const entry = capabilities.registration(capability);
|
|
3019
|
+
if (!entry || entry.capability.kind !== "rpc") {
|
|
3020
|
+
finish(new WebLoomError("capability_unavailable", `Capability "${capability.id}" is not available`, "wait", { capabilityId: capability.id }));
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
const rpc = capability;
|
|
3024
|
+
const context = {
|
|
3025
|
+
signal: merged.signal,
|
|
3026
|
+
deadlineAt,
|
|
3027
|
+
...callOptions.operationId !== void 0 ? { operationId: callOptions.operationId } : {},
|
|
3028
|
+
reference: entry.reference,
|
|
3029
|
+
origin: "local"
|
|
3030
|
+
};
|
|
3031
|
+
timer = setTimeout(() => {
|
|
3032
|
+
timedOut = true;
|
|
3033
|
+
try {
|
|
3034
|
+
timeoutController.abort(new WebLoomError("call_timeout", "Capability call timed out", "execute"));
|
|
3035
|
+
} catch {
|
|
3036
|
+
timeoutController.abort();
|
|
3037
|
+
}
|
|
3038
|
+
finish(new WebLoomError("call_timeout", "Capability call timed out", "execute", { capabilityId: capability.id, serviceInstanceId: entry.reference.serviceInstanceId }));
|
|
3039
|
+
}, timeoutMs);
|
|
3040
|
+
const onAbort = () => {
|
|
3041
|
+
if (!timedOut) finish(new WebLoomError("request_cancelled", "Capability call was cancelled", "dispose", { capabilityId: capability.id }));
|
|
3042
|
+
};
|
|
3043
|
+
merged.signal.addEventListener("abort", onAbort, { once: true });
|
|
3044
|
+
if (merged.signal.aborted) {
|
|
3045
|
+
onAbort();
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
let invoke;
|
|
3049
|
+
try {
|
|
3050
|
+
invoke = invokeCapabilityHandler(entry, request, context);
|
|
3051
|
+
} catch (error2) {
|
|
3052
|
+
finish(error2 instanceof WebLoomError ? error2 : new WebLoomError("handler_failed", errorMessage4(error2), "execute", { capabilityId: capability.id }));
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
Promise.resolve(invoke).then((value) => {
|
|
3056
|
+
if (settled) return;
|
|
3057
|
+
try {
|
|
3058
|
+
finish(void 0, rpc.response.parse(value));
|
|
3059
|
+
} catch (error2) {
|
|
3060
|
+
finish(new WebLoomError("response_validation_failed", errorMessage4(error2), "receive", { capabilityId: capability.id }));
|
|
3061
|
+
}
|
|
3062
|
+
}, (error2) => finish(error2 instanceof WebLoomError ? error2 : new WebLoomError("handler_failed", errorMessage4(error2), "execute", { capabilityId: capability.id })));
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
return client;
|
|
3067
|
+
};
|
|
3068
|
+
const createLocalStreamClient = (capability, scope) => {
|
|
3069
|
+
const client = {
|
|
3070
|
+
subscribe(request, subscribeOptions) {
|
|
3071
|
+
scope.assertActive();
|
|
3072
|
+
const timeoutMs = assertTimeout(subscribeOptions.timeoutMs);
|
|
3073
|
+
const timeoutController = new AbortController();
|
|
3074
|
+
const merged = mergeSignals2(timeoutController.signal, scope.signal, subscribeOptions.signal);
|
|
3075
|
+
let cancelled = false;
|
|
3076
|
+
let timedOut = false;
|
|
3077
|
+
let iterator;
|
|
3078
|
+
let readyTimer;
|
|
3079
|
+
let iteratorReturnStarted = false;
|
|
3080
|
+
let readySettled = false;
|
|
3081
|
+
let closedSettled = false;
|
|
3082
|
+
let resolveReady;
|
|
3083
|
+
let rejectReady;
|
|
3084
|
+
let resolveClosed;
|
|
3085
|
+
let rejectClosed;
|
|
3086
|
+
const ready = new Promise((resolve, reject) => {
|
|
3087
|
+
resolveReady = resolve;
|
|
3088
|
+
rejectReady = reject;
|
|
3089
|
+
});
|
|
3090
|
+
const closed = new Promise((resolve, reject) => {
|
|
3091
|
+
resolveClosed = resolve;
|
|
3092
|
+
rejectClosed = reject;
|
|
3093
|
+
});
|
|
3094
|
+
void ready.catch(() => void 0);
|
|
3095
|
+
void closed.catch(() => void 0);
|
|
3096
|
+
const settleReadyResolve = () => {
|
|
3097
|
+
if (!readySettled) {
|
|
3098
|
+
readySettled = true;
|
|
3099
|
+
resolveReady();
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
const settleReadyReject = (error2) => {
|
|
3103
|
+
if (!readySettled) {
|
|
3104
|
+
readySettled = true;
|
|
3105
|
+
rejectReady(error2);
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
3108
|
+
const settleClosedResolve = () => {
|
|
3109
|
+
if (!closedSettled) {
|
|
3110
|
+
closedSettled = true;
|
|
3111
|
+
resolveClosed();
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
const settleClosedReject = (error2) => {
|
|
3115
|
+
if (!closedSettled) {
|
|
3116
|
+
closedSettled = true;
|
|
3117
|
+
rejectClosed(error2);
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
const closeIterator = (late) => {
|
|
3121
|
+
const current = iterator ?? late;
|
|
3122
|
+
if (!current || iteratorReturnStarted) return;
|
|
3123
|
+
const close = current.return;
|
|
3124
|
+
if (!close) {
|
|
3125
|
+
iteratorReturnStarted = true;
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
iteratorReturnStarted = true;
|
|
3129
|
+
try {
|
|
3130
|
+
void Promise.resolve(close.call(current)).catch(() => void 0);
|
|
3131
|
+
} catch {
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
const terminate = (error2) => {
|
|
3135
|
+
if (cancelled) return;
|
|
3136
|
+
cancelled = true;
|
|
3137
|
+
if (readyTimer !== void 0) clearTimeout(readyTimer);
|
|
3138
|
+
closeIterator();
|
|
3139
|
+
merged.dispose();
|
|
3140
|
+
settleReadyReject(error2);
|
|
3141
|
+
settleClosedReject(error2);
|
|
3142
|
+
};
|
|
3143
|
+
const cancel = (reason = "stream cancelled") => {
|
|
3144
|
+
const error2 = timedOut ? new WebLoomError("call_timeout", "Stream subscription timed out while opening", "wait", { capabilityId: capability.id }) : new WebLoomError("request_cancelled", "Stream subscription was cancelled", "dispose", { capabilityId: capability.id });
|
|
3145
|
+
terminate(error2);
|
|
3146
|
+
};
|
|
3147
|
+
merged.signal.addEventListener("abort", () => cancel("stream cancelled"), { once: true });
|
|
3148
|
+
const start2 = async () => {
|
|
3149
|
+
try {
|
|
3150
|
+
const entry = capabilities.registration(capability);
|
|
3151
|
+
if (!entry || entry.capability.kind !== "stream") throw new WebLoomError("capability_unavailable", `Capability "${capability.id}" is not available`, "wait", { capabilityId: capability.id });
|
|
3152
|
+
const stream = capability;
|
|
3153
|
+
const context = { signal: merged.signal, deadlineAt: Date.now() + timeoutMs, reference: entry.reference, origin: "local" };
|
|
3154
|
+
readyTimer = setTimeout(() => {
|
|
3155
|
+
if (cancelled || readySettled) return;
|
|
3156
|
+
timedOut = true;
|
|
3157
|
+
try {
|
|
3158
|
+
timeoutController.abort(new WebLoomError("call_timeout", "Stream subscription timed out while opening", "wait"));
|
|
3159
|
+
} catch {
|
|
3160
|
+
timeoutController.abort();
|
|
3161
|
+
}
|
|
3162
|
+
terminate(new WebLoomError("call_timeout", "Stream subscription timed out while opening", "wait", { capabilityId: capability.id }));
|
|
3163
|
+
}, timeoutMs);
|
|
3164
|
+
const iterable = await capabilities.openStream(capability, request, context);
|
|
3165
|
+
if (cancelled) {
|
|
3166
|
+
try {
|
|
3167
|
+
closeIterator(iterable[Symbol.asyncIterator]());
|
|
3168
|
+
} catch {
|
|
3169
|
+
}
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
iterator = iterable[Symbol.asyncIterator]();
|
|
3173
|
+
if (readyTimer !== void 0) clearTimeout(readyTimer);
|
|
3174
|
+
settleReadyResolve();
|
|
3175
|
+
while (!cancelled) {
|
|
3176
|
+
const next = await iterator.next();
|
|
3177
|
+
if (next.done) {
|
|
3178
|
+
cancelled = true;
|
|
3179
|
+
settleClosedResolve();
|
|
3180
|
+
break;
|
|
3181
|
+
}
|
|
3182
|
+
if (cancelled) break;
|
|
3183
|
+
try {
|
|
3184
|
+
const item = stream.item.parse(next.value);
|
|
3185
|
+
await subscribeOptions.onNext(item);
|
|
3186
|
+
} catch {
|
|
3187
|
+
terminate(new WebLoomError("handler_failed", "Local stream consumer failed", "execute", { capabilityId: capability.id }));
|
|
3188
|
+
break;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
} catch (error2) {
|
|
3192
|
+
if (!cancelled) {
|
|
3193
|
+
const wrapped = error2 instanceof WebLoomError ? error2 : new WebLoomError("handler_failed", "Local stream operation failed", "execute", { capabilityId: capability.id });
|
|
3194
|
+
terminate(wrapped);
|
|
3195
|
+
}
|
|
3196
|
+
} finally {
|
|
3197
|
+
merged.dispose();
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
3200
|
+
if (merged.signal.aborted) cancel("stream cancelled");
|
|
3201
|
+
else void start2();
|
|
3202
|
+
return { ready, closed, cancel };
|
|
3203
|
+
}
|
|
3204
|
+
};
|
|
3205
|
+
return client;
|
|
3206
|
+
};
|
|
3207
|
+
const beginStop = (record2, reason) => {
|
|
3208
|
+
if (record2.state !== "enabled" && record2.state !== "starting" && record2.state !== "blocked" && record2.state !== "error-disabled") return false;
|
|
3209
|
+
record2.stopRequested = reason;
|
|
3210
|
+
record2.state = "stopping";
|
|
3211
|
+
record2.scope?.revoke(reason);
|
|
3212
|
+
for (const contribution of record2.contributions) {
|
|
3213
|
+
try {
|
|
3214
|
+
contribution.revoke();
|
|
3215
|
+
} catch {
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
for (const capability of record2.provided) capabilities.revoke(capability, record2.instanceId);
|
|
3219
|
+
for (const capability of record2.provided) publicClients.delete(capabilityKey(capability));
|
|
3220
|
+
enabled.delete(record2.manifest.id);
|
|
3221
|
+
bump();
|
|
3222
|
+
return true;
|
|
3223
|
+
};
|
|
3224
|
+
const finishStop = async (record2, reason, preserveIntent) => {
|
|
3225
|
+
const scope = record2.scope;
|
|
3226
|
+
const projectLateCleanup = (result, error2) => {
|
|
3227
|
+
if (result) record2.cleanup = result;
|
|
3228
|
+
if (error2 !== void 0) {
|
|
3229
|
+
record2.state = "cleanup-pending";
|
|
3230
|
+
record2.error = errorMessage4(error2);
|
|
3231
|
+
bump();
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
if (!result || result.cleanupIncomplete || record2.state !== "cleanup-pending") return;
|
|
3235
|
+
const currentDesired2 = desired(record2.manifest);
|
|
3236
|
+
const missing2 = missingDependencies(record2.manifest);
|
|
3237
|
+
const unavailable2 = unavailableReason(record2.manifest);
|
|
3238
|
+
record2.state = currentDesired2 && (missing2.length > 0 || unavailable2) ? "blocked" : "disabled";
|
|
3239
|
+
record2.blockedBy = record2.state === "blocked" ? [.../* @__PURE__ */ new Set([...missing2.map((item) => `missing:${item.kind}:${item.id}@${item.version}`), ...unavailable2 ? [unavailable2] : []])] : void 0;
|
|
3240
|
+
record2.error = void 0;
|
|
3241
|
+
bump();
|
|
3242
|
+
if (currentDesired2 && record2.state === "disabled" && !disposed) {
|
|
3243
|
+
queueMicrotask(() => {
|
|
3244
|
+
void start(record2.manifest.id).catch(() => void 0);
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
if (!scope && record2.state === "cleanup-pending") return;
|
|
3249
|
+
const cleanup = scope ? await scope.dispose({
|
|
3250
|
+
reason,
|
|
3251
|
+
timeoutMs: options.lifecycleCleanupTimeoutMs,
|
|
3252
|
+
teardown: record2.teardown,
|
|
3253
|
+
onLateSuccess: (_resourceId, result) => projectLateCleanup(result),
|
|
3254
|
+
onLateFailure: (_resourceId, error2, result) => projectLateCleanup(result, error2)
|
|
3255
|
+
}) : void 0;
|
|
3256
|
+
if (cleanup) record2.cleanup = cleanup;
|
|
3257
|
+
record2.scope = void 0;
|
|
3258
|
+
record2.provided = [];
|
|
3259
|
+
record2.teardown = void 0;
|
|
3260
|
+
record2.instanceId = void 0;
|
|
3261
|
+
record2.unitId = void 0;
|
|
3262
|
+
record2.stopRequested = void 0;
|
|
3263
|
+
record2.contributions = [];
|
|
3264
|
+
const currentDesired = desired(record2.manifest);
|
|
3265
|
+
const missing = missingDependencies(record2.manifest);
|
|
3266
|
+
const unavailable = unavailableReason(record2.manifest);
|
|
3267
|
+
const hasTimeout = Boolean(cleanup?.pending.length && cleanup.errors.some((item) => item.code === "lifecycle.cleanup_timeout"));
|
|
3268
|
+
const cleanupError = cleanup?.errors.find((item) => item.code !== "lifecycle.cleanup_timeout");
|
|
3269
|
+
if (hasTimeout) {
|
|
3270
|
+
record2.state = "cleanup-pending";
|
|
3271
|
+
record2.error = cleanupError?.message ?? "Plugin cleanup is still pending";
|
|
3272
|
+
} else if (cleanupError || cleanup?.cleanupIncomplete) {
|
|
3273
|
+
record2.state = "error-disabled";
|
|
3274
|
+
record2.error = cleanupError?.message ?? "Plugin cleanup did not complete";
|
|
3275
|
+
} else if (currentDesired && (missing.length > 0 || unavailable)) {
|
|
3276
|
+
record2.state = "blocked";
|
|
3277
|
+
record2.blockedBy = [
|
|
3278
|
+
.../* @__PURE__ */ new Set([
|
|
3279
|
+
...missing.map((item) => `missing:${item.kind}:${item.id}@${item.version}`),
|
|
3280
|
+
...unavailable ? [unavailable] : []
|
|
3281
|
+
])
|
|
3282
|
+
];
|
|
3283
|
+
record2.error = void 0;
|
|
3284
|
+
} else {
|
|
3285
|
+
record2.state = "disabled";
|
|
3286
|
+
record2.blockedBy = void 0;
|
|
3287
|
+
record2.error = void 0;
|
|
3288
|
+
}
|
|
3289
|
+
bump();
|
|
3290
|
+
if (currentDesired && record2.state === "disabled" && !disposed) {
|
|
3291
|
+
queueMicrotask(() => {
|
|
3292
|
+
void start(record2.manifest.id).catch(() => void 0);
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
};
|
|
3296
|
+
const stopAfterBegin = (record2, reason, preserveIntent) => {
|
|
3297
|
+
const existing = stopping.get(record2.manifest.id);
|
|
3298
|
+
if (existing) return existing;
|
|
3299
|
+
if (record2.state !== "stopping") return Promise.resolve();
|
|
3300
|
+
const startingTask = starting.get(record2.manifest.id);
|
|
3301
|
+
const task = (startingTask ? startingTask.catch(() => void 0) : Promise.resolve()).then(() => finishStop(record2, reason)).finally(() => {
|
|
3302
|
+
if (stopping.get(record2.manifest.id) === task) stopping.delete(record2.manifest.id);
|
|
3303
|
+
});
|
|
3304
|
+
stopping.set(record2.manifest.id, task);
|
|
3305
|
+
return task;
|
|
3306
|
+
};
|
|
3307
|
+
const stop = async (record2, reason, preserveIntent) => {
|
|
3308
|
+
const existing = stopping.get(record2.manifest.id);
|
|
3309
|
+
if (existing) return existing;
|
|
3310
|
+
if (!beginStop(record2, reason)) return;
|
|
3311
|
+
return stopAfterBegin(record2, reason);
|
|
3312
|
+
};
|
|
3313
|
+
const registerContribution = async (record2, unit, instanceId, scope) => {
|
|
3314
|
+
if (unit.contribution === void 0) return;
|
|
3315
|
+
for (const adapter of options.contributionAdapters ?? []) {
|
|
3316
|
+
const result = await adapter.register({ pluginId: record2.manifest.id, unitId: unit.id, instanceId, scope, contribution: unit.contribution, manifest: record2.manifest });
|
|
3317
|
+
if (!result) continue;
|
|
3318
|
+
const handle = typeof result === "function" ? { revoke: result } : result;
|
|
3319
|
+
let active = true;
|
|
3320
|
+
let disposePromise2;
|
|
3321
|
+
const runtime = {
|
|
3322
|
+
get active() {
|
|
3323
|
+
return active;
|
|
3324
|
+
},
|
|
3325
|
+
revoke() {
|
|
3326
|
+
if (!active) return;
|
|
3327
|
+
active = false;
|
|
3328
|
+
handle.revoke?.();
|
|
3329
|
+
},
|
|
3330
|
+
dispose() {
|
|
3331
|
+
if (disposePromise2) return disposePromise2;
|
|
3332
|
+
disposePromise2 = Promise.resolve(handle.dispose?.());
|
|
3333
|
+
disposePromise2.catch(() => void 0);
|
|
3334
|
+
return disposePromise2;
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
record2.contributions.push(runtime);
|
|
3338
|
+
scope.onRevoke(() => runtime.revoke());
|
|
3339
|
+
scope.onDispose(() => runtime.dispose(), `contribution:${adapter.name ?? "anonymous"}`);
|
|
3340
|
+
}
|
|
3341
|
+
};
|
|
3342
|
+
const startImplementation = async (pluginId) => {
|
|
3343
|
+
const record2 = records.get(pluginId);
|
|
3344
|
+
if (!record2) throw new Error(`Plugin "${pluginId}" is not registered`);
|
|
3345
|
+
if (record2.state === "enabled") return;
|
|
3346
|
+
if (record2.state === "starting") return;
|
|
3347
|
+
if (disposed) throw new LifecycleScopeRevokedError("Plugin host is disposed");
|
|
3348
|
+
const unit = manifestUnit(record2.manifest);
|
|
3349
|
+
if (!unit) {
|
|
3350
|
+
record2.state = "blocked";
|
|
3351
|
+
record2.blockedBy = ["runtime_unit_ambiguous"];
|
|
3352
|
+
bump();
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
const unavailable = options.runtimeUnitAvailability?.({ pluginId, unitId: unit.id, runtime: unit.runtime });
|
|
3356
|
+
const missing = missingDependencies(record2.manifest);
|
|
3357
|
+
if (unavailable || missing.length > 0) {
|
|
3358
|
+
record2.state = "blocked";
|
|
3359
|
+
record2.blockedBy = [...unavailable ? [unavailable] : [], ...missing.map((item) => `missing:${item.kind}:${item.id}@${item.version}`)];
|
|
3360
|
+
bump();
|
|
3361
|
+
if (required(record2.manifest)) throw new StartupCapabilityError(missing.map((capability) => ({ capability })), "startup");
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
const parent = options.runtimeUnitParentScope?.({ pluginId, unitId: unit.id, runtime: unit.runtime, manifest: record2.manifest }) ?? rootScope;
|
|
3365
|
+
const attributes = cloneFrozenAttributes(options.runtimeUnitAttributes?.({ pluginId, unitId: unit.id, runtime: unit.runtime, manifest: record2.manifest }));
|
|
3366
|
+
const scope = parent.child("runtime-unit", { pluginId, attributes });
|
|
3367
|
+
const instanceId = scope.identity.instanceId;
|
|
3368
|
+
const permissions = [...unit.permissions ?? []];
|
|
3369
|
+
const policy = options.permissionPolicy?.({ pluginId, unitId: unit.id, identity: scope.identity, requested: permissions });
|
|
3370
|
+
const lease = createPermissionLease({ identity: scope.identity, requested: permissions, approved: policy?.approved ?? permissions, sessionConstraints: policy?.sessionConstraints, scope, ...policy?.binding });
|
|
3371
|
+
const grantedPermissions = permissions.filter(
|
|
3372
|
+
(permission) => (policy?.approved ?? permissions).includes(permission) && (policy?.sessionConstraints === void 0 || policy.sessionConstraints.includes(permission))
|
|
3373
|
+
);
|
|
3374
|
+
const definitions = definitionList(pluginId, unit.id);
|
|
3375
|
+
record2.state = "starting";
|
|
3376
|
+
record2.scope = scope;
|
|
3377
|
+
record2.instanceId = instanceId;
|
|
3378
|
+
record2.unitId = unit.id;
|
|
3379
|
+
record2.error = void 0;
|
|
3380
|
+
record2.blockedBy = void 0;
|
|
3381
|
+
record2.provided = [];
|
|
3382
|
+
bump();
|
|
3383
|
+
definitions.filter((capability) => capability.kind === "local");
|
|
3384
|
+
const declaredProvides = unit.provides ?? [];
|
|
3385
|
+
const declaredDependencies = unit.dependencies ?? [];
|
|
3386
|
+
const resolveCapability = (capability, optional) => {
|
|
3387
|
+
const descriptor = capabilityDescriptor(capability);
|
|
3388
|
+
const declared = [...declaredProvides, ...declaredDependencies.map((dependency2) => dependency2.capability)].some((item) => capabilityKey(item) === capabilityKey(descriptor));
|
|
3389
|
+
if (!declared) throw new WebLoomError("capability_unavailable", `Capability "${capability.id}" is not declared by plugin`, "validate", { capabilityId: capability.id });
|
|
3390
|
+
if (capability.kind === "local") {
|
|
3391
|
+
if (!capabilities.has(capability)) {
|
|
3392
|
+
if (optional) return void 0;
|
|
3393
|
+
throw new WebLoomError("capability_unavailable", `Capability "${capability.id}" is not available`, "wait", { capabilityId: capability.id });
|
|
3394
|
+
}
|
|
3395
|
+
return capabilities.get(capability);
|
|
3396
|
+
}
|
|
3397
|
+
const dependency = declaredDependencies.find((item) => capabilityKey(item.capability) === capabilityKey(descriptor));
|
|
3398
|
+
if (dependency?.source === "peer") {
|
|
3399
|
+
throw new WebLoomError("capability_unavailable", `Capability "${capability.id}" is only available through call.peer`, "dispatch", { capabilityId: capability.id });
|
|
3400
|
+
}
|
|
3401
|
+
const remote = dependency?.sourceRuntime !== void 0 && dependency.sourceRuntime !== unit.runtime;
|
|
3402
|
+
if (optional && remote && (!capabilityBridge || !capabilityBridge.services().some((service) => service.kind === capability.kind && service.capabilityId === capability.id && service.contractVersion === capability.version))) return void 0;
|
|
3403
|
+
if (optional && !remote && !capabilities.has(capability)) return void 0;
|
|
3404
|
+
return getRemoteClient(capability, scope, remote);
|
|
3405
|
+
};
|
|
3406
|
+
const handle = (capability, handler) => {
|
|
3407
|
+
if (scope.state !== "active") return;
|
|
3408
|
+
const descriptor = capabilityDescriptor(capability);
|
|
3409
|
+
if (!declaredProvides.some((item) => capabilityKey(item) === capabilityKey(descriptor))) throw new WebLoomError("capability_unavailable", `Plugin "${pluginId}" cannot handle undeclared capability`, "validate", { capabilityId: capability.id });
|
|
3410
|
+
const reference = Object.freeze({ kind: capability.kind, capabilityId: capability.id, contractVersion: capability.version, runtime: unit.runtime, runtimeInstanceId, serviceInstanceId: makeId2(`service:${instanceId}:${capability.id}`), attributes });
|
|
3411
|
+
const peerDependencies = declaredDependencies.filter((dependency) => dependency.source === "peer").map((dependency) => dependency.capability);
|
|
3412
|
+
if (capability.kind === "rpc") capabilities.handle(capability, handler, instanceId, scope, reference, peerDependencies);
|
|
3413
|
+
else capabilities.stream(capability, handler, instanceId, scope, reference, peerDependencies);
|
|
3414
|
+
record2.provided.push(capability);
|
|
3415
|
+
bump();
|
|
3416
|
+
};
|
|
3417
|
+
const provide = (capability, value) => {
|
|
3418
|
+
if (scope.state !== "active") return;
|
|
3419
|
+
const descriptor = capabilityDescriptor(capability);
|
|
3420
|
+
if (!declaredProvides.some((item) => capabilityKey(item) === capabilityKey(descriptor))) throw new WebLoomError("capability_unavailable", `Plugin "${pluginId}" cannot provide undeclared capability`, "validate", { capabilityId: capability.id });
|
|
3421
|
+
capabilities.provide(capability, value, instanceId, scope);
|
|
3422
|
+
record2.provided.push(capability);
|
|
3423
|
+
bump();
|
|
3424
|
+
};
|
|
3425
|
+
const context = {
|
|
3426
|
+
pluginId,
|
|
3427
|
+
instanceId,
|
|
3428
|
+
unitId: unit.id,
|
|
3429
|
+
scope,
|
|
3430
|
+
signal: scope.signal,
|
|
3431
|
+
permissions: grantedPermissions,
|
|
3432
|
+
permissionLease: lease,
|
|
3433
|
+
taskScheduler,
|
|
3434
|
+
// Context extensions are realm-local host injection points. They may
|
|
3435
|
+
// intentionally contain live services (for example a logger or a
|
|
3436
|
+
// coordinator facade), so they are not wire attributes and must not be
|
|
3437
|
+
// forced through the DTO/structured-clone validator. The extension
|
|
3438
|
+
// callback remains the trust boundary; transport-visible attributes
|
|
3439
|
+
// continue to use cloneFrozenAttributes above and at exposure time.
|
|
3440
|
+
extension: options.contextExtension?.({ pluginId, unitId: unit.id, instanceId, scope, manifest: record2.manifest }) ?? {},
|
|
3441
|
+
config: unit.config,
|
|
3442
|
+
onDispose(cleanup) {
|
|
3443
|
+
scope.onDispose(cleanup);
|
|
3444
|
+
},
|
|
3445
|
+
provide,
|
|
3446
|
+
handle,
|
|
3447
|
+
capability(capability) {
|
|
3448
|
+
return resolveCapability(capability, false);
|
|
3449
|
+
},
|
|
3450
|
+
optionalCapability(capability) {
|
|
3451
|
+
return resolveCapability(capability, true);
|
|
3452
|
+
},
|
|
3453
|
+
messageBus: createScopedMessageBus(messageBus, scope)
|
|
3454
|
+
};
|
|
3455
|
+
try {
|
|
3456
|
+
const setup = options.runtimeUnitImplementationRegistry?.get(pluginId, unit.id);
|
|
3457
|
+
if (!setup) throw new Error(`Runtime implementation is unavailable for ${pluginId}/${unit.id}`);
|
|
3458
|
+
const result = await setup(context);
|
|
3459
|
+
record2.teardown = typeof result === "function" ? result : void 0;
|
|
3460
|
+
await registerContribution(record2, unit, instanceId, scope);
|
|
3461
|
+
if (required(record2.manifest)) {
|
|
3462
|
+
for (const descriptor of declaredProvides) {
|
|
3463
|
+
if (!record2.provided.some((capability) => capabilityKey(capability) === capabilityKey(descriptor))) throw new Error(`Plugin "${pluginId}" did not register declared capability "${descriptor.id}@${descriptor.version}"`);
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
if (record2.stopRequested || scope.state !== "active") {
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
if (!desired(record2.manifest)) {
|
|
3470
|
+
queueMicrotask(() => {
|
|
3471
|
+
void stop(record2, "startup superseded", false).catch(() => void 0);
|
|
3472
|
+
});
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
record2.state = "enabled";
|
|
3476
|
+
enabled.add(pluginId);
|
|
3477
|
+
bump();
|
|
3478
|
+
} catch (error2) {
|
|
3479
|
+
if (record2.stopRequested || scope.state !== "active") {
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
record2.state = "error-disabled";
|
|
3483
|
+
record2.error = errorMessage4(error2);
|
|
3484
|
+
scope.revoke("plugin setup failed");
|
|
3485
|
+
for (const capability of record2.provided) capabilities.revoke(capability, instanceId);
|
|
3486
|
+
record2.provided = [];
|
|
3487
|
+
record2.cleanup = await scope.dispose({ reason: "plugin setup failed", timeoutMs: options.lifecycleCleanupTimeoutMs });
|
|
3488
|
+
record2.scope = void 0;
|
|
3489
|
+
record2.instanceId = void 0;
|
|
3490
|
+
record2.unitId = void 0;
|
|
3491
|
+
bump();
|
|
3492
|
+
if (required(record2.manifest)) throw new StartupPluginError({ pluginId, unitId: unit.id, capabilities: declaredProvides, state: record2.state, error: record2.error });
|
|
3493
|
+
}
|
|
3494
|
+
};
|
|
3495
|
+
start = async (pluginId) => {
|
|
3496
|
+
const existingStop = stopping.get(pluginId);
|
|
3497
|
+
if (existingStop) await existingStop;
|
|
3498
|
+
const existing = starting.get(pluginId);
|
|
3499
|
+
if (existing) return existing;
|
|
3500
|
+
const task = startImplementation(pluginId);
|
|
3501
|
+
starting.set(pluginId, task);
|
|
3502
|
+
try {
|
|
3503
|
+
await task;
|
|
3504
|
+
} finally {
|
|
3505
|
+
if (starting.get(pluginId) === task) starting.delete(pluginId);
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
let reconcilePromise;
|
|
3509
|
+
let reconcileRequested = false;
|
|
3510
|
+
const runReconcile = async () => {
|
|
3511
|
+
let progress = true;
|
|
3512
|
+
while (progress) {
|
|
3513
|
+
progress = false;
|
|
3514
|
+
const stops = [];
|
|
3515
|
+
for (const record2 of records.values()) {
|
|
3516
|
+
const wants = desired(record2.manifest);
|
|
3517
|
+
const invalid = record2.state === "enabled" || record2.state === "starting" ? missingDependencies(record2.manifest).length > 0 || unavailableReason(record2.manifest) !== void 0 : false;
|
|
3518
|
+
if (!wants && (record2.state === "enabled" || record2.state === "starting" || record2.state === "blocked") || wants && invalid) {
|
|
3519
|
+
const before = record2.state;
|
|
3520
|
+
stops.push(stop(record2, wants ? "runtime dependency unavailable" : "desired intent disabled"));
|
|
3521
|
+
progress = progress || before !== record2.state;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
if (stops.length > 0) await Promise.all(stops);
|
|
3525
|
+
for (const manifest of known.values()) {
|
|
3526
|
+
const record2 = records.get(manifest.id);
|
|
3527
|
+
if (!record2 || !desired(manifest)) continue;
|
|
3528
|
+
if (record2.state !== "registered" && record2.state !== "disabled" && record2.state !== "blocked") continue;
|
|
3529
|
+
const before = record2.state;
|
|
3530
|
+
await start(manifest.id);
|
|
3531
|
+
progress = progress || before !== record2.state;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
const reconcile = async () => {
|
|
3536
|
+
if (disposed) return;
|
|
3537
|
+
if (reconcilePromise) {
|
|
3538
|
+
reconcileRequested = true;
|
|
3539
|
+
return reconcilePromise;
|
|
3540
|
+
}
|
|
3541
|
+
const task = (async () => {
|
|
3542
|
+
do {
|
|
3543
|
+
reconcileRequested = false;
|
|
3544
|
+
await runReconcile();
|
|
3545
|
+
} while (reconcileRequested && !disposed);
|
|
3546
|
+
})().finally(() => {
|
|
3547
|
+
reconcilePromise = void 0;
|
|
3548
|
+
});
|
|
3549
|
+
reconcilePromise = task;
|
|
3550
|
+
return task;
|
|
3551
|
+
};
|
|
3552
|
+
const submitIntent = async (pluginId, desiredEnabled) => {
|
|
3553
|
+
if (!known.has(pluginId)) throw new Error(`Plugin "${pluginId}" is not registered`);
|
|
3554
|
+
if (options.pluginIntentCoordinator) {
|
|
3555
|
+
const current = options.pluginIntentCoordinator.snapshot();
|
|
3556
|
+
return options.pluginIntentCoordinator.submit({
|
|
3557
|
+
commandId: makeId2(`plugin-intent:${pluginId}`),
|
|
3558
|
+
authorityInstanceId: options.pluginIntentCoordinator.authorityInstanceId,
|
|
3559
|
+
expectedRevision: current.revision,
|
|
3560
|
+
pluginId,
|
|
3561
|
+
desiredEnabled
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
internalConfigWrites.add(pluginId);
|
|
3565
|
+
try {
|
|
3566
|
+
configStore.setEnabled(pluginId, desiredEnabled);
|
|
3567
|
+
} finally {
|
|
3568
|
+
internalConfigWrites.delete(pluginId);
|
|
3569
|
+
}
|
|
3570
|
+
return {
|
|
3571
|
+
status: "accepted",
|
|
3572
|
+
commandId: makeId2(`plugin-intent:${pluginId}`),
|
|
3573
|
+
snapshot: { revision: 0, desiredEnabled: configStore.read(), desiredRevision: {} },
|
|
3574
|
+
persisted: true
|
|
3575
|
+
};
|
|
3576
|
+
};
|
|
3577
|
+
const enable = async (pluginId) => {
|
|
3578
|
+
const result = await submitIntent(pluginId, true);
|
|
3579
|
+
if (result.status !== "accepted" && result.status !== "duplicate") {
|
|
3580
|
+
throw new Error(`Could not enable plugin "${pluginId}": ${result.status}`);
|
|
3581
|
+
}
|
|
3582
|
+
await reconcile();
|
|
3583
|
+
const record2 = records.get(pluginId);
|
|
3584
|
+
if (record2?.state === "cleanup-pending") throw new Error(`Plugin "${pluginId}" cleanup is pending`);
|
|
3585
|
+
};
|
|
3586
|
+
const collectStopPlan = (pluginId) => {
|
|
3587
|
+
const current = graph();
|
|
3588
|
+
const result = [];
|
|
3589
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3590
|
+
const visitDependents = (providerId) => {
|
|
3591
|
+
for (const dependent of reverseDependentsOf(current, providerId)) {
|
|
3592
|
+
if (visited.has(dependent.pluginId)) continue;
|
|
3593
|
+
const dependentRecord = records.get(dependent.pluginId);
|
|
3594
|
+
if (!dependentRecord) continue;
|
|
3595
|
+
visited.add(dependent.pluginId);
|
|
3596
|
+
visitDependents(dependent.pluginId);
|
|
3597
|
+
if (dependentRecord.state === "enabled" || dependentRecord.state === "starting" || dependentRecord.state === "stopping" || dependentRecord.state === "blocked" || dependentRecord.state === "error-disabled") result.push(dependentRecord);
|
|
3598
|
+
}
|
|
3599
|
+
};
|
|
3600
|
+
visitDependents(pluginId);
|
|
3601
|
+
const target = records.get(pluginId);
|
|
3602
|
+
if (target && (target.state === "enabled" || target.state === "starting" || target.state === "stopping" || target.state === "blocked" || target.state === "error-disabled")) result.push(target);
|
|
3603
|
+
return result;
|
|
3604
|
+
};
|
|
3605
|
+
const host = {
|
|
3606
|
+
capabilities,
|
|
3607
|
+
messageBus,
|
|
3608
|
+
resourceRegistry,
|
|
3609
|
+
resourceStore,
|
|
3610
|
+
rootScope,
|
|
3611
|
+
taskScheduler,
|
|
3612
|
+
runtimeKind,
|
|
3613
|
+
runtimeId,
|
|
3614
|
+
runtimeInstanceId,
|
|
3615
|
+
installed: () => [...known.keys()],
|
|
3616
|
+
manifests: () => [...known.keys()],
|
|
3617
|
+
state(pluginId) {
|
|
3618
|
+
const record2 = records.get(pluginId);
|
|
3619
|
+
const manifest = record2?.manifest;
|
|
3620
|
+
const unit = manifest ? manifestUnit(manifest) : void 0;
|
|
3621
|
+
if (!record2) {
|
|
3622
|
+
const cached2 = stateCache.get(pluginId);
|
|
3623
|
+
if (cached2?.signature === "unknown") return cached2.state;
|
|
3624
|
+
const unknownState = { id: pluginId, kind: "disabled", lifecycleState: "disabled", desiredEnabled: false, units: [] };
|
|
3625
|
+
stateCache.set(pluginId, { signature: "unknown", state: unknownState });
|
|
3626
|
+
return unknownState;
|
|
3627
|
+
}
|
|
3628
|
+
const desiredEnabled = desired(record2.manifest);
|
|
3629
|
+
const signature = JSON.stringify([record2.state, record2.error, record2.blockedBy, record2.instanceId, record2.unitId, desiredEnabled, desiredRevision(record2.manifest.id), record2.cleanup]);
|
|
3630
|
+
const cached = stateCache.get(pluginId);
|
|
3631
|
+
if (cached?.signature === signature) return cached.state;
|
|
3632
|
+
const unitState = unit ? [{ pluginId, unitId: unit.id, runtime: unit.runtime, kind: record2.state, ...record2.instanceId ? { instanceId: record2.instanceId } : {}, ...record2.error ? { error: record2.error } : {} }] : [];
|
|
3633
|
+
const next = { id: pluginId, kind: record2.state, lifecycleState: lifecycleStateFor(record2.state, desiredEnabled), ...record2.error ? { error: record2.error } : {}, desiredEnabled, ...desiredRevision(record2.manifest.id) !== void 0 ? { desiredRevision: desiredRevision(record2.manifest.id) } : {}, ...record2.instanceId ? { instanceId: record2.instanceId } : {}, ...record2.unitId ? { unitId: record2.unitId } : {}, ...record2.blockedBy ? { blockedBy: [...record2.blockedBy] } : {}, ...record2.cleanup ? { cleanup: record2.cleanup } : {}, units: unitState };
|
|
3634
|
+
stateCache.set(pluginId, { signature, state: next });
|
|
3635
|
+
return next;
|
|
3636
|
+
},
|
|
3637
|
+
scope: (pluginId) => records.get(pluginId)?.scope,
|
|
3638
|
+
refreshRuntimeUnitSnapshots: bump,
|
|
3639
|
+
reconcile,
|
|
3640
|
+
graph,
|
|
3641
|
+
version: () => versionCounter,
|
|
3642
|
+
subscribe(listener) {
|
|
3643
|
+
listeners.add(listener);
|
|
3644
|
+
return () => listeners.delete(listener);
|
|
3645
|
+
},
|
|
3646
|
+
getManifest: (pluginId) => known.get(pluginId),
|
|
3647
|
+
reverseDeps(pluginId) {
|
|
3648
|
+
return [...reverseDependentsOf(graph(), pluginId)];
|
|
3649
|
+
},
|
|
3650
|
+
validateManifestSet(manifests) {
|
|
3651
|
+
for (const manifest of manifests) {
|
|
3652
|
+
options.manifestValidator?.(manifest);
|
|
3653
|
+
}
|
|
3654
|
+
validatePluginGraph(manifests, { runtime: configuredRuntime, builtinCapabilities: new Set(capabilities.descriptors()), externalRuntimeDependencies: options.externalRuntimeDependencies });
|
|
3655
|
+
},
|
|
3656
|
+
provide(capability, value) {
|
|
3657
|
+
if (disposed) throw new LifecycleScopeRevokedError("Plugin host is disposed");
|
|
3658
|
+
capabilities.provide(capability, value, "host", rootScope);
|
|
3659
|
+
bump();
|
|
3660
|
+
},
|
|
3661
|
+
register: async (manifest) => {
|
|
3662
|
+
if (disposed) throw new LifecycleScopeRevokedError("Plugin host is disposed");
|
|
3663
|
+
validateManifest(manifest);
|
|
3664
|
+
if (known.has(manifest.id)) throw new Error(`Plugin "${manifest.id}" is already registered`);
|
|
3665
|
+
known.set(manifest.id, manifest);
|
|
3666
|
+
records.set(manifest.id, { manifest, state: "registered", provided: [], contributions: [] });
|
|
3667
|
+
initializeConfigIntent(manifest);
|
|
3668
|
+
bump();
|
|
3669
|
+
await reconcile();
|
|
3670
|
+
},
|
|
3671
|
+
registerAll: async (manifests) => {
|
|
3672
|
+
if (disposed) throw new LifecycleScopeRevokedError("Plugin host is disposed");
|
|
3673
|
+
validatePluginGraph(manifests, { runtime: configuredRuntime, builtinCapabilities: new Set(capabilities.descriptors()), externalRuntimeDependencies: options.externalRuntimeDependencies, allowMissingDependencies: true });
|
|
3674
|
+
for (const manifest of manifests) {
|
|
3675
|
+
validateManifest(manifest);
|
|
3676
|
+
if (known.has(manifest.id)) throw new Error(`Plugin "${manifest.id}" is already registered`);
|
|
3677
|
+
}
|
|
3678
|
+
for (const manifest of manifests) {
|
|
3679
|
+
known.set(manifest.id, manifest);
|
|
3680
|
+
records.set(manifest.id, { manifest, state: "registered", provided: [], contributions: [] });
|
|
3681
|
+
initializeConfigIntent(manifest);
|
|
3682
|
+
}
|
|
3683
|
+
bump();
|
|
3684
|
+
await reconcile();
|
|
3685
|
+
},
|
|
3686
|
+
enable,
|
|
3687
|
+
retry: async (pluginId) => {
|
|
3688
|
+
const record2 = records.get(pluginId);
|
|
3689
|
+
if (!record2) throw new Error(`Plugin "${pluginId}" is not registered`);
|
|
3690
|
+
record2.state = "registered";
|
|
3691
|
+
record2.error = void 0;
|
|
3692
|
+
await start(pluginId);
|
|
3693
|
+
},
|
|
3694
|
+
submitIntent: async (pluginId, desiredEnabled) => {
|
|
3695
|
+
const result = await submitIntent(pluginId, desiredEnabled);
|
|
3696
|
+
if (result.status === "accepted" || result.status === "duplicate") await reconcile();
|
|
3697
|
+
return result;
|
|
3698
|
+
},
|
|
3699
|
+
disable: async (pluginId) => {
|
|
3700
|
+
const record2 = records.get(pluginId);
|
|
3701
|
+
if (!record2) throw new Error(`Plugin "${pluginId}" is not registered`);
|
|
3702
|
+
if (required(record2.manifest)) return { ok: false, reason: `Plugin "${pluginId}" cannot be disabled` };
|
|
3703
|
+
if (options.pluginIntentCoordinator) {
|
|
3704
|
+
const result = await submitIntent(pluginId, false);
|
|
3705
|
+
if (result.status !== "accepted" && result.status !== "duplicate") return { ok: false, reason: `Could not disable plugin "${pluginId}": ${result.status}` };
|
|
3706
|
+
} else {
|
|
3707
|
+
writeConfigIntent(pluginId, false);
|
|
3708
|
+
}
|
|
3709
|
+
const plan = collectStopPlan(pluginId);
|
|
3710
|
+
for (const item of plan) {
|
|
3711
|
+
const reason = item.manifest.id === pluginId ? "plugin disabled" : `dependency ${pluginId} disabled`;
|
|
3712
|
+
beginStop(item, reason);
|
|
3713
|
+
}
|
|
3714
|
+
await Promise.all(plan.map((item) => stopAfterBegin(item, item.manifest.id === pluginId ? "plugin disabled" : `dependency ${pluginId} disabled`, item.manifest.id !== pluginId)));
|
|
3715
|
+
return { ok: true };
|
|
3716
|
+
},
|
|
3717
|
+
suspend: async (pluginId, reason = "runtime identity changed") => {
|
|
3718
|
+
const record2 = records.get(pluginId);
|
|
3719
|
+
if (record2) await stop(record2, reason);
|
|
3720
|
+
},
|
|
3721
|
+
unregister: async (pluginId) => {
|
|
3722
|
+
const record2 = records.get(pluginId);
|
|
3723
|
+
if (!record2) return;
|
|
3724
|
+
if (required(record2.manifest)) throw new Error(`Plugin "${pluginId}" cannot be unregistered`);
|
|
3725
|
+
await host.disable(pluginId);
|
|
3726
|
+
records.delete(pluginId);
|
|
3727
|
+
known.delete(pluginId);
|
|
3728
|
+
bump();
|
|
3729
|
+
},
|
|
3730
|
+
dispose: (reason = "plugin host disposed") => {
|
|
3731
|
+
if (disposePromise) return disposePromise;
|
|
3732
|
+
disposed = true;
|
|
3733
|
+
disposePromise = (async () => {
|
|
3734
|
+
for (const record2 of [...records.values()].reverse()) await stop(record2, reason);
|
|
3735
|
+
return rootScope.dispose({ reason, timeoutMs: options.lifecycleCleanupTimeoutMs });
|
|
3736
|
+
})();
|
|
3737
|
+
return disposePromise;
|
|
3738
|
+
},
|
|
3739
|
+
assertCapabilities(requiredCapabilities, extra = {}) {
|
|
3740
|
+
const details = [];
|
|
3741
|
+
const current = graph();
|
|
3742
|
+
for (const capability of requiredCapabilities) {
|
|
3743
|
+
if (capabilities.has(capability)) continue;
|
|
3744
|
+
const descriptor = capabilityDescriptor(capability);
|
|
3745
|
+
const providerPluginId = current.providers[capabilityKey(descriptor)]?.[0];
|
|
3746
|
+
const provider = providerPluginId ? records.get(providerPluginId) : void 0;
|
|
3747
|
+
details.push({
|
|
3748
|
+
capability: descriptor,
|
|
3749
|
+
providerPluginId,
|
|
3750
|
+
providerState: provider?.state,
|
|
3751
|
+
providerError: provider?.error,
|
|
3752
|
+
configuredEnabled: provider ? desired(provider.manifest) : void 0
|
|
3753
|
+
});
|
|
3754
|
+
}
|
|
3755
|
+
if (details.length > 0) throw new StartupCapabilityError(details, extra.phase);
|
|
3756
|
+
},
|
|
3757
|
+
serviceReferences() {
|
|
3758
|
+
return capabilities.registrations().filter((entry) => isRemote(entry.capability)).map((entry) => entry.reference);
|
|
3759
|
+
},
|
|
3760
|
+
capability(capability) {
|
|
3761
|
+
if (capability.kind === "local") return capabilities.get(capability);
|
|
3762
|
+
const key = capabilityKey(capability);
|
|
3763
|
+
const cached = publicClients.get(key);
|
|
3764
|
+
if (cached) return cached;
|
|
3765
|
+
const client = getRemoteClient(capability, rootScope);
|
|
3766
|
+
publicClients.set(key, client);
|
|
3767
|
+
return client;
|
|
3768
|
+
},
|
|
3769
|
+
optionalCapability(capability) {
|
|
3770
|
+
if (capability.kind === "local") return capabilities.has(capability) ? capabilities.get(capability) : void 0;
|
|
3771
|
+
if (capabilityBridge && !capabilityBridge.services().some((service) => service.kind === capability.kind && service.capabilityId === capability.id && service.contractVersion === capability.version) && !capabilities.has(capability)) return void 0;
|
|
3772
|
+
if (!capabilityBridge && !capabilities.has(capability)) return void 0;
|
|
3773
|
+
return host.capability(capability);
|
|
3774
|
+
},
|
|
3775
|
+
attachRemote(bridge) {
|
|
3776
|
+
capabilityBridge?.invalidate("Remote bridge replaced");
|
|
3777
|
+
capabilityBridge = bridge;
|
|
3778
|
+
bump();
|
|
3779
|
+
},
|
|
3780
|
+
detachRemote(reason = "Remote bridge detached") {
|
|
3781
|
+
capabilityBridge?.invalidate(reason);
|
|
3782
|
+
capabilityBridge = void 0;
|
|
3783
|
+
bump();
|
|
3784
|
+
},
|
|
3785
|
+
registerImplementation(implementation) {
|
|
3786
|
+
const registry = options.runtimeUnitImplementationRegistry;
|
|
3787
|
+
if (!registry?.register) throw new Error("This Host was not created with an advanced implementation registry");
|
|
3788
|
+
registry.register(implementation);
|
|
3789
|
+
},
|
|
3790
|
+
inspect() {
|
|
3791
|
+
return { runtimeId, runtimeKind, runtimeInstanceId, version: versionCounter, pluginCount: known.size, peerCount: 0, pendingCallCount: 0, activeStreamCount: 0, plugins: [...known.keys()].sort().map((id) => host.state(id)) };
|
|
3792
|
+
},
|
|
3793
|
+
explain(target) {
|
|
3794
|
+
const id = typeof target === "string" ? target : target.id;
|
|
3795
|
+
const record2 = records.get(id);
|
|
3796
|
+
if (!record2) return { target: id, reasons: ["unknown"] };
|
|
3797
|
+
const blocked = record2.blockedBy ?? [];
|
|
3798
|
+
return { target: id, state: record2.state, reasons: blocked.length > 0 ? blocked : record2.error ? [record2.error] : [] };
|
|
3799
|
+
}
|
|
3800
|
+
};
|
|
3801
|
+
if (options.pluginIntentCoordinator) options.pluginIntentCoordinator.subscribe((snapshot) => {
|
|
3802
|
+
intentSnapshot = snapshot;
|
|
3803
|
+
void reconcile();
|
|
3804
|
+
bump();
|
|
3805
|
+
});
|
|
3806
|
+
configStore.subscribe((snapshot) => {
|
|
3807
|
+
if (internalConfigWrites.size === 0 && !options.pluginIntentCoordinator) {
|
|
3808
|
+
for (const record2 of records.values()) {
|
|
3809
|
+
if (required(record2.manifest) && snapshot[record2.manifest.id] !== true) writeConfigIntent(record2.manifest.id, true);
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
if (internalConfigWrites.size === 0) void reconcile();
|
|
3813
|
+
});
|
|
3814
|
+
return host;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
// src/host/runtimeUnitImplementationRegistry.ts
|
|
3818
|
+
function implementationKey2(pluginId, unitId) {
|
|
3819
|
+
return `${pluginId}\0${unitId}`;
|
|
3820
|
+
}
|
|
3821
|
+
function createRuntimeUnitImplementationRegistry(implementations = []) {
|
|
3822
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3823
|
+
const register = (implementation) => {
|
|
3824
|
+
const key = implementationKey2(implementation.pluginId, implementation.unitId);
|
|
3825
|
+
if (entries.has(key)) throw new Error(`\u8FD0\u884C\u5355\u5143\u5B9E\u73B0\u91CD\u590D\u6CE8\u518C: ${implementation.pluginId}/${implementation.unitId}`);
|
|
3826
|
+
entries.set(key, { ...implementation, capabilities: implementation.capabilities ? Object.freeze([...implementation.capabilities]) : void 0 });
|
|
3827
|
+
};
|
|
3828
|
+
for (const implementation of implementations) register(implementation);
|
|
3829
|
+
return {
|
|
3830
|
+
get(pluginId, unitId) {
|
|
3831
|
+
return entries.get(implementationKey2(pluginId, unitId))?.setup;
|
|
3832
|
+
},
|
|
3833
|
+
getCapabilities(pluginId, unitId) {
|
|
3834
|
+
return entries.get(implementationKey2(pluginId, unitId))?.capabilities;
|
|
3835
|
+
},
|
|
3836
|
+
register,
|
|
3837
|
+
unregister(pluginId, unitId) {
|
|
3838
|
+
entries.delete(implementationKey2(pluginId, unitId));
|
|
3839
|
+
}
|
|
3840
|
+
};
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
// src/runtime/pluginDefinitions.ts
|
|
3844
|
+
function cloneDependencies(dependencies, runtime) {
|
|
3845
|
+
if (!dependencies) return void 0;
|
|
3846
|
+
return Object.freeze(dependencies.map((dependency) => {
|
|
3847
|
+
const base = {
|
|
3848
|
+
capability: Object.freeze({ ...dependency.capability }),
|
|
3849
|
+
...dependency.optional !== void 0 ? { optional: dependency.optional } : {},
|
|
3850
|
+
...dependency.reason !== void 0 ? { reason: dependency.reason } : {}
|
|
3851
|
+
};
|
|
3852
|
+
return Object.freeze(dependency.source === "peer" ? { ...base, source: "peer" } : { ...base, sourceRuntime: dependency.sourceRuntime ?? runtime });
|
|
3853
|
+
}));
|
|
3854
|
+
}
|
|
3855
|
+
function selectUnit(input, runtime) {
|
|
3856
|
+
const candidates = [...input.manifest.units ?? []];
|
|
3857
|
+
if (candidates.length === 0) return { id: input.manifest.id, runtime };
|
|
3858
|
+
const requested = input.unitId;
|
|
3859
|
+
if (requested !== void 0) {
|
|
3860
|
+
const unit = candidates.find((candidate) => candidate.id === requested);
|
|
3861
|
+
if (!unit) throw new Error(`Plugin "${input.manifest.id}" does not declare unit "${requested}"`);
|
|
3862
|
+
if (unit.runtime !== void 0 && unit.runtime !== runtime) throw new Error(`Plugin "${input.manifest.id}" unit "${requested}" targets ${unit.runtime}, not ${runtime}`);
|
|
3863
|
+
return { ...unit, runtime };
|
|
3864
|
+
}
|
|
3865
|
+
const matches = candidates.filter((unit) => unit.runtime === void 0 || unit.runtime === runtime);
|
|
3866
|
+
if (matches.length !== 1 || !matches[0]) throw new Error(`Plugin "${input.manifest.id}" has ${matches.length} implementation units for ${runtime}; specify unitId explicitly`);
|
|
3867
|
+
return { ...matches[0], runtime };
|
|
3868
|
+
}
|
|
3869
|
+
function materializePluginDefinition(input, runtime) {
|
|
3870
|
+
if (!input || !input.manifest || typeof input.manifest.id !== "string" || input.manifest.id.trim() === "") throw new TypeError("Plugin manifest id must be a non-empty string");
|
|
3871
|
+
if (typeof input.setup !== "function") throw new TypeError(`Plugin "${input.manifest.id}" setup must be a function`);
|
|
3872
|
+
const unit = selectUnit(input, runtime);
|
|
3873
|
+
const materializedUnit = Object.freeze({
|
|
3874
|
+
...unit,
|
|
3875
|
+
runtime,
|
|
3876
|
+
...cloneDependencies(unit.dependencies, runtime) ? { dependencies: cloneDependencies(unit.dependencies, runtime) } : {},
|
|
3877
|
+
...unit.provides ? { provides: Object.freeze(unit.provides.map((item) => Object.freeze({ ...item }))) } : {},
|
|
3878
|
+
...unit.permissions ? { permissions: Object.freeze([...unit.permissions]) } : {}
|
|
3879
|
+
});
|
|
3880
|
+
const manifest = Object.freeze({
|
|
3881
|
+
id: input.manifest.id,
|
|
3882
|
+
name: input.manifest.name,
|
|
3883
|
+
...input.manifest.description !== void 0 ? { description: input.manifest.description } : {},
|
|
3884
|
+
startup: input.manifest.startup,
|
|
3885
|
+
defaultEnabled: input.manifest.defaultEnabled,
|
|
3886
|
+
canDisable: input.manifest.canDisable,
|
|
3887
|
+
units: Object.freeze([materializedUnit])
|
|
3888
|
+
});
|
|
3889
|
+
const capabilities = Object.freeze([...input.capabilities ?? []]);
|
|
3890
|
+
return { manifest, unitId: unit.id, setup: input.setup, capabilities };
|
|
3891
|
+
}
|
|
3892
|
+
function materializePluginDefinitions(inputs, runtime) {
|
|
3893
|
+
return inputs.map((input) => materializePluginDefinition(input, runtime));
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
// src/runtime/runtimeTypes.ts
|
|
3897
|
+
var RuntimeInitializationError = class extends Error {
|
|
3898
|
+
code = "runtime_initialization_failed";
|
|
3899
|
+
details;
|
|
3900
|
+
constructor(details) {
|
|
3901
|
+
super(`Runtime initialization failed${details.pluginId ? ` for ${details.pluginId}` : ""} during ${details.phase}: ${details.error}`);
|
|
3902
|
+
this.name = "RuntimeInitializationError";
|
|
3903
|
+
this.details = Object.freeze({ ...details });
|
|
3904
|
+
}
|
|
3905
|
+
};
|
|
3906
|
+
var RuntimeUnavailableError = class extends Error {
|
|
3907
|
+
code = "transport_unavailable";
|
|
3908
|
+
constructor(message2 = "Runtime is unavailable") {
|
|
3909
|
+
super(message2);
|
|
3910
|
+
this.name = "RuntimeUnavailableError";
|
|
3911
|
+
}
|
|
3912
|
+
};
|
|
3913
|
+
|
|
3914
|
+
// src/runtime/windowRuntime.ts
|
|
3915
|
+
var appHosts = /* @__PURE__ */ new WeakMap();
|
|
3916
|
+
var ownedHosts = /* @__PURE__ */ new WeakSet();
|
|
3917
|
+
function makeRuntimeInstanceId(runtimeId) {
|
|
3918
|
+
try {
|
|
3919
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `${runtimeId}:${crypto.randomUUID()}`;
|
|
3920
|
+
} catch {
|
|
3921
|
+
}
|
|
3922
|
+
return `${runtimeId}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
3923
|
+
}
|
|
3924
|
+
function message(error2) {
|
|
3925
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3926
|
+
}
|
|
3927
|
+
function snapshotFromHost(host, id, instanceId, state = "ready") {
|
|
3928
|
+
const units = host.installed().flatMap((pluginId) => host.state(pluginId).units.map((unit) => ({ pluginId: unit.pluginId, unitId: unit.unitId, runtime: unit.runtime, ...unit.instanceId !== void 0 ? { instanceId: unit.instanceId } : {}, state: unit.kind })));
|
|
3929
|
+
const services = state === "ready" ? host.serviceReferences().map((service) => ({ kind: service.kind, capabilityId: service.capabilityId, contractVersion: service.contractVersion, serviceInstanceId: service.serviceInstanceId, attributes: cloneFrozenAttributes(service.attributes), ...service.grantId !== void 0 ? { grantId: service.grantId } : {}, ...service.authorizationRevision !== void 0 ? { authorizationRevision: service.authorizationRevision } : {} })) : [];
|
|
3930
|
+
return Object.freeze({ protocolVersion: RUNTIME_PROTOCOL_VERSION, runtimeId: id, runtimeKind: "window-main", runtimeInstanceId: instanceId, revision: Math.max(1, host.version()), state, units: Object.freeze(units), services: Object.freeze(services) });
|
|
3931
|
+
}
|
|
3932
|
+
async function createAppFromHost(id, instanceId, host) {
|
|
3933
|
+
if (ownedHosts.has(host)) throw new Error("This WebLoom Host is already owned by a WindowApp");
|
|
3934
|
+
let current = snapshotFromHost(host, id, instanceId, "starting");
|
|
3935
|
+
let disposed = false;
|
|
3936
|
+
let disposePromise;
|
|
3937
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3938
|
+
const emit = (state) => {
|
|
3939
|
+
current = snapshotFromHost(host, id, instanceId, state ?? (disposed ? "disposed" : "ready"));
|
|
3940
|
+
for (const listener of [...listeners]) {
|
|
3941
|
+
try {
|
|
3942
|
+
listener(current);
|
|
3943
|
+
} catch {
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
};
|
|
3947
|
+
const removeHost = host.subscribe(() => emit());
|
|
3948
|
+
const app = {
|
|
3949
|
+
runtimeKind: "window-main",
|
|
3950
|
+
runtimeId: id,
|
|
3951
|
+
runtimeInstanceId: instanceId,
|
|
3952
|
+
state: () => current,
|
|
3953
|
+
pluginState: (pluginId) => host.state(pluginId),
|
|
3954
|
+
capability(capability) {
|
|
3955
|
+
if (disposed) throw new RuntimeUnavailableError("Window Runtime has been disposed");
|
|
3956
|
+
return host.capability(capability);
|
|
3957
|
+
},
|
|
3958
|
+
optionalCapability(capability) {
|
|
3959
|
+
if (disposed) return void 0;
|
|
3960
|
+
return host.optionalCapability(capability);
|
|
3961
|
+
},
|
|
3962
|
+
inspect() {
|
|
3963
|
+
return host.inspect();
|
|
3964
|
+
},
|
|
3965
|
+
subscribe(listener) {
|
|
3966
|
+
listeners.add(listener);
|
|
3967
|
+
listener(current);
|
|
3968
|
+
return () => listeners.delete(listener);
|
|
3969
|
+
},
|
|
3970
|
+
dispose(reason = "window runtime disposed") {
|
|
3971
|
+
if (disposePromise) return disposePromise;
|
|
3972
|
+
disposed = true;
|
|
3973
|
+
emit("stopping");
|
|
3974
|
+
removeHost();
|
|
3975
|
+
disposePromise = host.dispose(reason).then((result) => {
|
|
3976
|
+
emit("disposed");
|
|
3977
|
+
return result;
|
|
3978
|
+
}, (error2) => {
|
|
3979
|
+
emit("failed");
|
|
3980
|
+
throw error2;
|
|
3981
|
+
});
|
|
3982
|
+
return disposePromise;
|
|
3983
|
+
}
|
|
3984
|
+
};
|
|
3985
|
+
appHosts.set(app, host);
|
|
3986
|
+
ownedHosts.add(host);
|
|
3987
|
+
emit("ready");
|
|
3988
|
+
return app;
|
|
3989
|
+
}
|
|
3990
|
+
async function createWindowApp(options) {
|
|
3991
|
+
const id = options.id ?? "window-main";
|
|
3992
|
+
const instanceId = makeRuntimeInstanceId(id);
|
|
3993
|
+
let materialized;
|
|
3994
|
+
try {
|
|
3995
|
+
materialized = materializePluginDefinitions(options.plugins, "window-main");
|
|
3996
|
+
} catch (error2) {
|
|
3997
|
+
throw new RuntimeInitializationError({ phase: "validate", error: message(error2) });
|
|
3998
|
+
}
|
|
3999
|
+
const implementations = createRuntimeUnitImplementationRegistry(materialized.map((item) => ({ pluginId: item.manifest.id, unitId: item.unitId, setup: item.setup, capabilities: item.capabilities })));
|
|
4000
|
+
const { id: _id, plugins: _plugins, ...hostOptions } = options;
|
|
4001
|
+
const host = createPluginHost({ ...hostOptions, runtime: "window-main", runtimeId: id, runtimeInstanceId: instanceId, runtimeUnitImplementationRegistry: implementations });
|
|
4002
|
+
try {
|
|
4003
|
+
await host.registerAll(materialized.map((item) => item.manifest));
|
|
4004
|
+
} catch (error2) {
|
|
4005
|
+
await host.dispose("window Runtime initialization failed").catch(() => void 0);
|
|
4006
|
+
if (error2 instanceof StartupPluginError) throw new RuntimeInitializationError({ pluginId: error2.details.pluginId, unitId: error2.details.unitId, phase: "startup", error: error2.details.error ?? error2.message });
|
|
4007
|
+
throw new RuntimeInitializationError({ phase: "startup", error: message(error2) });
|
|
4008
|
+
}
|
|
4009
|
+
return createAppFromHost(id, instanceId, host);
|
|
4010
|
+
}
|
|
4011
|
+
async function createWindowAppFromHost(options) {
|
|
4012
|
+
const id = options.id ?? options.host.runtimeId;
|
|
4013
|
+
if (options.host.runtimeKind !== "window-main") throw new TypeError("createWindowAppFromHost requires a window-main Host");
|
|
4014
|
+
return createAppFromHost(id, options.host.runtimeInstanceId, options.host);
|
|
4015
|
+
}
|
|
4016
|
+
function hostForWindowApp(app) {
|
|
4017
|
+
const host = appHosts.get(app);
|
|
4018
|
+
if (!host) throw new Error("WindowApp is not owned by a WebLoom Host");
|
|
4019
|
+
return host;
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
export { DEFAULT_RUNTIME_LIMITS, LIFECYCLE_ERROR_TEXT, LifecycleScopeRevokedError, PermissionDeniedError, PermissionLeaseRevokedError, RESOURCE_OWNER, RESOURCE_REGISTRY, RUNTIME_CALL_TYPE, RUNTIME_CANCEL_TYPE, RUNTIME_CREDIT_TYPE, RUNTIME_ERROR_MESSAGE_TYPE, RUNTIME_ERROR_TYPE, RUNTIME_NEXT_TYPE, RUNTIME_PROTOCOL_VERSION, RUNTIME_RESULT_TYPE, RUNTIME_SNAPSHOT_TYPE, RuntimeInitializationError, RuntimeUnavailableError, SCOPED_TASK_SCHEDULER_CAPABILITY, StartupCapabilityError, StartupPluginError, UpgradeGateRejectedError, WebLoomError, assertCapability, assertReceivedPortSet, buildPluginGraph, capabilityDescriptor, capabilityKey, cloneFrozenAttributes, createCapabilityRegistry, createInMemoryPluginConfigStore, createLifecycleScope, createMessageBus, createPermissionLease, createPluginHost, createReceivePortLedger, createResourceRegistry, createResourceScope, createResourceStore, createRuntimeBudget, createRuntimeMessageCodec, createRuntimeUnitImplementationRegistry, createScopedTaskScheduler, createWindowApp, createWindowAppFromHost, defineCapability, dependenciesOfManifest, hostForWindowApp, invokeCapabilityHandler, isCapability, isCapabilityDescriptor, isRuntimeSnapshot, lifecycleErrorText, materializePluginDefinitions, normalizeRuntimeLimits, providesOfManifest, registerOwnedResource, reverseDependentsOf, selectRuntimeUnit, validateDto, validatePluginGraph, validateRawDto, validateTransferList, validateTransferListWithStats };
|
|
4023
|
+
//# sourceMappingURL=chunk-HJKPKWI7.js.map
|
|
4024
|
+
//# sourceMappingURL=chunk-HJKPKWI7.js.map
|