@skenora/sdk 0.1.2
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 +167 -0
- package/dist/editor.d.ts +14 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +14 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/lightbox.d.ts +3 -0
- package/dist/lightbox.d.ts.map +1 -0
- package/dist/lightbox.js +2 -0
- package/dist/lightbox.js.map +1 -0
- package/dist/playback-config.d.ts +27 -0
- package/dist/playback-config.d.ts.map +1 -0
- package/dist/playback-config.js +87 -0
- package/dist/playback-config.js.map +1 -0
- package/dist/renderer-diagnostics.d.ts +4 -0
- package/dist/renderer-diagnostics.d.ts.map +1 -0
- package/dist/renderer-diagnostics.js +85 -0
- package/dist/renderer-diagnostics.js.map +1 -0
- package/dist/renderer.d.ts +10 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +8 -0
- package/dist/renderer.js.map +1 -0
- package/dist/scene-config.d.ts +58 -0
- package/dist/scene-config.d.ts.map +1 -0
- package/dist/scene-config.js +223 -0
- package/dist/scene-config.js.map +1 -0
- package/dist/scene-plan/apply-patch.d.ts +5 -0
- package/dist/scene-plan/apply-patch.d.ts.map +1 -0
- package/dist/scene-plan/apply-patch.js +174 -0
- package/dist/scene-plan/apply-patch.js.map +1 -0
- package/dist/scene-plan/executor.d.ts +26 -0
- package/dist/scene-plan/executor.d.ts.map +1 -0
- package/dist/scene-plan/executor.js +928 -0
- package/dist/scene-plan/executor.js.map +1 -0
- package/dist/scene-plan/gateway.d.ts +15 -0
- package/dist/scene-plan/gateway.d.ts.map +1 -0
- package/dist/scene-plan/gateway.js +104 -0
- package/dist/scene-plan/gateway.js.map +1 -0
- package/dist/scene-plan/index.d.ts +4 -0
- package/dist/scene-plan/index.d.ts.map +1 -0
- package/dist/scene-plan/index.js +4 -0
- package/dist/scene-plan/index.js.map +1 -0
- package/dist/scene-plan/types.d.ts +181 -0
- package/dist/scene-plan/types.d.ts.map +1 -0
- package/dist/scene-plan/types.js +2 -0
- package/dist/scene-plan/types.js.map +1 -0
- package/dist/skenora-renderer.d.ts +117 -0
- package/dist/skenora-renderer.d.ts.map +1 -0
- package/dist/skenora-renderer.js +464 -0
- package/dist/skenora-renderer.js.map +1 -0
- package/dist/skenora-scene.d.ts +263 -0
- package/dist/skenora-scene.d.ts.map +1 -0
- package/dist/skenora-scene.js +832 -0
- package/dist/skenora-scene.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import { EditorRuntimeProjectionError, } from "@skenora/babylon-editor";
|
|
2
|
+
import { createSceneDocumentRevisionHash, EditorRevisionConflictError, } from "@skenora/editor";
|
|
3
|
+
import { compileSceneBlueprint, compileScenePatch, createDiagnostic, } from "@skenora/scene-plan";
|
|
4
|
+
import { createResourceBindingScope, createResourceBindingTable, ResourceBindingValidationError, } from "@skenora/resources";
|
|
5
|
+
import { findUnavailableSceneCapabilities, SceneCapabilityUnavailableError, } from "@skenora/contracts";
|
|
6
|
+
import { materializeScenePatch } from "./apply-patch.js";
|
|
7
|
+
export class ScenePlanExecutor {
|
|
8
|
+
editor;
|
|
9
|
+
bridge;
|
|
10
|
+
resources;
|
|
11
|
+
sessionEpoch;
|
|
12
|
+
capabilities;
|
|
13
|
+
context;
|
|
14
|
+
#initialCapabilityAvailability;
|
|
15
|
+
#getCapabilityAvailability;
|
|
16
|
+
#controller = new AbortController();
|
|
17
|
+
#operations = new Map();
|
|
18
|
+
#idempotency = new Map();
|
|
19
|
+
#abortListener;
|
|
20
|
+
#queue = Promise.resolve();
|
|
21
|
+
#disposed = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
if (options.bridge.editor !== options.editor) {
|
|
24
|
+
throw new Error("ScenePlanExecutor bridge and editor must be the same session");
|
|
25
|
+
}
|
|
26
|
+
if (options.bridge.resources !== options.resources) {
|
|
27
|
+
throw new Error("ScenePlanExecutor bridge and resources must use the same resolver");
|
|
28
|
+
}
|
|
29
|
+
this.editor = options.editor;
|
|
30
|
+
this.bridge = options.bridge;
|
|
31
|
+
this.resources = options.resources;
|
|
32
|
+
this.sessionEpoch = options.sessionEpoch ?? createSessionEpoch();
|
|
33
|
+
this.capabilities = cloneFrozenCapabilities(options.capabilities ?? []);
|
|
34
|
+
const observedAvailability = options.context?.capabilityAvailability;
|
|
35
|
+
this.#initialCapabilityAvailability = observedAvailability
|
|
36
|
+
? cloneFrozenAvailability(observedAvailability)
|
|
37
|
+
: undefined;
|
|
38
|
+
this.#getCapabilityAvailability = options.getCapabilityAvailability;
|
|
39
|
+
this.context = {
|
|
40
|
+
...options.context,
|
|
41
|
+
...(observedAvailability === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: {
|
|
44
|
+
capabilityAvailability: this.#initialCapabilityAvailability,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
if (options.signal) {
|
|
48
|
+
const listener = () => this.#controller.abort();
|
|
49
|
+
this.#abortListener = listener;
|
|
50
|
+
if (options.signal.aborted)
|
|
51
|
+
this.#controller.abort();
|
|
52
|
+
else
|
|
53
|
+
options.signal.addEventListener("abort", listener, { once: true });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.#abortListener = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
get revisionToken() {
|
|
60
|
+
this.#assertActive();
|
|
61
|
+
return this.#createRevisionToken();
|
|
62
|
+
}
|
|
63
|
+
inspectScene(options = {}) {
|
|
64
|
+
this.#assertActive();
|
|
65
|
+
const document = this.editor.snapshot;
|
|
66
|
+
const capabilityAvailability = this.#readCapabilityAvailability();
|
|
67
|
+
const revisionToken = this.#createRevisionToken();
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
sceneId: document.id,
|
|
70
|
+
sceneName: document.name,
|
|
71
|
+
revisionToken,
|
|
72
|
+
counts: Object.freeze({
|
|
73
|
+
assets: Object.keys(document.assets).length,
|
|
74
|
+
entities: Object.keys(document.entities).length,
|
|
75
|
+
materials: Object.keys(document.materials).length,
|
|
76
|
+
materialBindings: Object.keys(document.materialBindings).length,
|
|
77
|
+
textureAnimations: Object.keys(document.textureAnimations).length,
|
|
78
|
+
cameraPaths: Object.keys(document.cameraPaths).length,
|
|
79
|
+
lights: document.lights.length,
|
|
80
|
+
flows: Object.keys(document.flows).length,
|
|
81
|
+
}),
|
|
82
|
+
capabilities: cloneFrozenCapabilities(this.capabilities),
|
|
83
|
+
outline: createSceneOutlinePage(document, revisionToken, options),
|
|
84
|
+
...(capabilityAvailability === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { capabilityAvailability }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
validateBlueprint(input, options = {}) {
|
|
90
|
+
this.#assertActive();
|
|
91
|
+
try {
|
|
92
|
+
const prepared = this.#prepareBindings(options);
|
|
93
|
+
const result = compileSceneBlueprint(input, prepared.context);
|
|
94
|
+
return {
|
|
95
|
+
valid: result.ok,
|
|
96
|
+
planKind: "blueprint",
|
|
97
|
+
...(result.planHash === undefined ? {} : { planHash: result.planHash }),
|
|
98
|
+
sourceMappings: result.sourceMappings,
|
|
99
|
+
diagnostics: result.diagnostics,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
valid: false,
|
|
105
|
+
planKind: "blueprint",
|
|
106
|
+
diagnostics: [validationSetupDiagnostic(error)],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
validatePatch(input, options = {}) {
|
|
111
|
+
this.#assertActive();
|
|
112
|
+
try {
|
|
113
|
+
const prepared = this.#prepareBindings(options);
|
|
114
|
+
const document = this.editor.snapshot;
|
|
115
|
+
const context = {
|
|
116
|
+
...prepared.context,
|
|
117
|
+
document,
|
|
118
|
+
documentRevision: this.editor.revision,
|
|
119
|
+
documentHash: createSceneDocumentRevisionHash(document),
|
|
120
|
+
};
|
|
121
|
+
const result = compileScenePatch(input, context);
|
|
122
|
+
return {
|
|
123
|
+
valid: result.ok,
|
|
124
|
+
planKind: "patch",
|
|
125
|
+
...(result.planHash === undefined ? {} : { planHash: result.planHash }),
|
|
126
|
+
diagnostics: result.diagnostics,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
planKind: "patch",
|
|
133
|
+
diagnostics: [validationSetupDiagnostic(error)],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
validatePlan(input, options = {}) {
|
|
138
|
+
return detectPlanKind(input) === "patch"
|
|
139
|
+
? this.validatePatch(input, options)
|
|
140
|
+
: this.validateBlueprint(input, options);
|
|
141
|
+
}
|
|
142
|
+
applyPlan(input, options = {}) {
|
|
143
|
+
this.#assertActive();
|
|
144
|
+
const kind = detectPlanKind(input);
|
|
145
|
+
if (kind === "patch")
|
|
146
|
+
return this.applyPatch(input, options);
|
|
147
|
+
if (kind === "blueprint")
|
|
148
|
+
return this.applyBlueprint(input, options);
|
|
149
|
+
const operationId = resolveOperationId(options.operationId);
|
|
150
|
+
return Promise.resolve(this.#result(operationId, options, "rejected", undefined, [
|
|
151
|
+
diagnostic("invalid-input", "Plan must be a SceneBlueprint or ScenePatch"),
|
|
152
|
+
]));
|
|
153
|
+
}
|
|
154
|
+
applyBlueprint(input, options = {}) {
|
|
155
|
+
this.#assertActive();
|
|
156
|
+
const operationId = resolveOperationId(options.operationId);
|
|
157
|
+
const fingerprint = createOperationFingerprint("blueprint", input, options);
|
|
158
|
+
const existing = this.#findExisting(operationId, options.idempotencyKey, options.operationNamespace, fingerprint);
|
|
159
|
+
if (existing)
|
|
160
|
+
return existing;
|
|
161
|
+
return this.#schedule(operationId, options, fingerprint, () => this.#runBlueprint(operationId, input, options));
|
|
162
|
+
}
|
|
163
|
+
applyPatch(input, options = {}) {
|
|
164
|
+
this.#assertActive();
|
|
165
|
+
const operationId = resolveOperationId(options.operationId);
|
|
166
|
+
const fingerprint = createOperationFingerprint("patch", input, options);
|
|
167
|
+
const existing = this.#findExisting(operationId, options.idempotencyKey, options.operationNamespace, fingerprint);
|
|
168
|
+
if (existing)
|
|
169
|
+
return existing;
|
|
170
|
+
return this.#schedule(operationId, options, fingerprint, () => this.#runPatch(operationId, input, options));
|
|
171
|
+
}
|
|
172
|
+
waitOperation(operationId, options = {}) {
|
|
173
|
+
return (this.#operations.get(operationMapKey(options.operationNamespace, operationId))?.result ?? null);
|
|
174
|
+
}
|
|
175
|
+
dispose() {
|
|
176
|
+
if (this.#disposed)
|
|
177
|
+
return;
|
|
178
|
+
this.#disposed = true;
|
|
179
|
+
this.#controller.abort();
|
|
180
|
+
this.#operations.clear();
|
|
181
|
+
this.#idempotency.clear();
|
|
182
|
+
}
|
|
183
|
+
#schedule(operationId, options, fingerprint, work) {
|
|
184
|
+
const run = this.#queue.then(() => work(), () => work());
|
|
185
|
+
const result = run.catch((error) => this.#result(operationId, options, "failed", undefined, [
|
|
186
|
+
executionDiagnostic(error),
|
|
187
|
+
]));
|
|
188
|
+
this.#queue = result.then(() => undefined, () => undefined);
|
|
189
|
+
const record = Object.freeze({ fingerprint, result });
|
|
190
|
+
this.#operations.set(operationMapKey(options.operationNamespace, operationId), record);
|
|
191
|
+
if (options.idempotencyKey) {
|
|
192
|
+
this.#idempotency.set(operationMapKey(options.operationNamespace, options.idempotencyKey), record);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
async #runBlueprint(operationId, input, options) {
|
|
197
|
+
const before = structuredClone(this.editor.snapshot);
|
|
198
|
+
const operationSignal = linkSignals(this.#controller.signal, options.signal);
|
|
199
|
+
const acquisition = {};
|
|
200
|
+
try {
|
|
201
|
+
this.#assertNotAborted(operationSignal.signal);
|
|
202
|
+
const expected = this.#resolveExpected(options);
|
|
203
|
+
this.#assertCurrentExpected(expected);
|
|
204
|
+
const prepared = this.#prepareBindings(options);
|
|
205
|
+
const compiled = compileSceneBlueprint(input, prepared.context);
|
|
206
|
+
if (!compiled.ok || !compiled.document) {
|
|
207
|
+
return this.#result(operationId, options, "rejected", "blueprint", compiled.diagnostics, compiled.planHash);
|
|
208
|
+
}
|
|
209
|
+
this.#assertCurrentExpected(expected);
|
|
210
|
+
const requiredResources = compiled.sourceMappings
|
|
211
|
+
.filter((mapping) => mapping.kind === "resource")
|
|
212
|
+
.map((mapping) => mapping.id);
|
|
213
|
+
const resourceSummary = await this.#acquireResources(prepared.table, requiredResources, operationSignal.signal, (scope) => {
|
|
214
|
+
acquisition.scope = scope;
|
|
215
|
+
});
|
|
216
|
+
this.#assertNotAborted(operationSignal.signal);
|
|
217
|
+
this.#assertCurrentCapabilities(compiled.document);
|
|
218
|
+
this.#assertCurrentExpected(expected);
|
|
219
|
+
const commit = this.editor.commitCandidateAtRevision(compiled.document, {
|
|
220
|
+
expectedRevision: expected.revision,
|
|
221
|
+
...(expected.documentHash === undefined
|
|
222
|
+
? {}
|
|
223
|
+
: { expectedDocumentHash: expected.documentHash }),
|
|
224
|
+
label: options.label ?? "Apply SceneBlueprint",
|
|
225
|
+
});
|
|
226
|
+
const projection = await this.#project(operationId, commit.revision, commit.changeId, options.modelLoadPolicy);
|
|
227
|
+
const diagnostics = projection.diagnostics;
|
|
228
|
+
return this.#result(operationId, options, "committed", "blueprint", diagnostics, compiled.planHash, commit, diffDocuments(before, this.editor.snapshot), resourceSummary, projection.summary);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
return this.#result(operationId, options, isAbortError(error) ? "cancelled" : "rejected", "blueprint", [executionDiagnostic(error)]);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
acquisition.scope?.dispose();
|
|
235
|
+
operationSignal.dispose();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async #runPatch(operationId, input, options) {
|
|
239
|
+
const before = structuredClone(this.editor.snapshot);
|
|
240
|
+
const operationSignal = linkSignals(this.#controller.signal, options.signal);
|
|
241
|
+
const acquisition = {};
|
|
242
|
+
try {
|
|
243
|
+
this.#assertNotAborted(operationSignal.signal);
|
|
244
|
+
const expected = this.#resolveExpected(options);
|
|
245
|
+
this.#assertCurrentExpected(expected);
|
|
246
|
+
const prepared = this.#prepareBindings(options);
|
|
247
|
+
const currentDocument = this.editor.snapshot;
|
|
248
|
+
const currentHash = createSceneDocumentRevisionHash(currentDocument);
|
|
249
|
+
const context = {
|
|
250
|
+
...prepared.context,
|
|
251
|
+
document: currentDocument,
|
|
252
|
+
documentRevision: this.editor.revision,
|
|
253
|
+
documentHash: currentHash,
|
|
254
|
+
};
|
|
255
|
+
const compiled = compileScenePatch(input, context);
|
|
256
|
+
if (!compiled.ok || !compiled.candidate || !compiled.patch) {
|
|
257
|
+
return this.#result(operationId, options, "rejected", "patch", compiled.diagnostics, compiled.planHash);
|
|
258
|
+
}
|
|
259
|
+
if (compiled.patch.target.expectedRevision !== expected.revision) {
|
|
260
|
+
return this.#result(operationId, options, "rejected", "patch", [
|
|
261
|
+
diagnostic("revision-conflict", "Patch target revision does not match the executor precondition"),
|
|
262
|
+
], compiled.planHash);
|
|
263
|
+
}
|
|
264
|
+
const candidate = materializeScenePatch(currentDocument, compiled.candidate, prepared.table);
|
|
265
|
+
this.#assertCurrentExpected(expected);
|
|
266
|
+
const requiredResources = unique(compiled.candidate.operations
|
|
267
|
+
.filter((operation) => operation.target.kind === "resource" && operation.op !== "remove")
|
|
268
|
+
.map((operation) => operation.target.id));
|
|
269
|
+
const resourceSummary = await this.#acquireResources(prepared.table, requiredResources, operationSignal.signal, (scope) => {
|
|
270
|
+
acquisition.scope = scope;
|
|
271
|
+
});
|
|
272
|
+
this.#assertNotAborted(operationSignal.signal);
|
|
273
|
+
this.#assertCurrentCapabilities(candidate);
|
|
274
|
+
this.#assertCurrentExpected(expected);
|
|
275
|
+
const expectedDocumentHash = compiled.patch.target.expectedDocumentHash ?? expected.documentHash;
|
|
276
|
+
const commit = this.editor.commitCandidateAtRevision(candidate, {
|
|
277
|
+
expectedRevision: compiled.patch.target.expectedRevision,
|
|
278
|
+
...(expectedDocumentHash === undefined ? {} : { expectedDocumentHash }),
|
|
279
|
+
label: options.label ?? "Apply ScenePatch",
|
|
280
|
+
});
|
|
281
|
+
const projection = await this.#project(operationId, commit.revision, commit.changeId, options.modelLoadPolicy);
|
|
282
|
+
return this.#result(operationId, options, "committed", "patch", projection.diagnostics, compiled.planHash, commit, diffDocuments(before, this.editor.snapshot), resourceSummary, projection.summary);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
return this.#result(operationId, options, isAbortError(error) ? "cancelled" : "rejected", "patch", [executionDiagnostic(error)]);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
acquisition.scope?.dispose();
|
|
289
|
+
operationSignal.dispose();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async #project(operationId, revision, changeId, modelLoadPolicy) {
|
|
293
|
+
let result;
|
|
294
|
+
try {
|
|
295
|
+
result = await this.bridge.flushProjection({
|
|
296
|
+
operationId,
|
|
297
|
+
sourceRevision: revision,
|
|
298
|
+
changeId,
|
|
299
|
+
...(modelLoadPolicy === undefined ? {} : { modelLoadPolicy }),
|
|
300
|
+
});
|
|
301
|
+
if (!result) {
|
|
302
|
+
result = await this.bridge.projectCurrentDocument({
|
|
303
|
+
operationId,
|
|
304
|
+
sourceRevision: revision,
|
|
305
|
+
changeId,
|
|
306
|
+
...(modelLoadPolicy === undefined ? {} : { modelLoadPolicy }),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
summary: {
|
|
313
|
+
status: "failed",
|
|
314
|
+
operationId,
|
|
315
|
+
modelFailureEntityIds: [],
|
|
316
|
+
},
|
|
317
|
+
diagnostics: [projectionDiagnostic(error)],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (!result) {
|
|
321
|
+
return {
|
|
322
|
+
summary: {
|
|
323
|
+
status: "skipped",
|
|
324
|
+
operationId,
|
|
325
|
+
modelFailureEntityIds: [],
|
|
326
|
+
},
|
|
327
|
+
diagnostics: [
|
|
328
|
+
diagnostic("projection-unavailable", "EditorRuntimeBridge did not create a projection operation", "warning"),
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const summary = projectionSummary(result);
|
|
333
|
+
if (result.status === "partial") {
|
|
334
|
+
return {
|
|
335
|
+
summary,
|
|
336
|
+
diagnostics: [
|
|
337
|
+
diagnostic("runtime-projection-partial", "Scene committed, but one or more models failed during projection", "warning"),
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (result.status === "failed" || result.status === "cancelled") {
|
|
342
|
+
return {
|
|
343
|
+
summary,
|
|
344
|
+
diagnostics: [projectionDiagnostic(result.error)],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return { summary, diagnostics: [] };
|
|
348
|
+
}
|
|
349
|
+
async #acquireResources(table, assetRefs, signal, onScope) {
|
|
350
|
+
if (assetRefs.length === 0) {
|
|
351
|
+
return {
|
|
352
|
+
required: Object.freeze([...assetRefs]),
|
|
353
|
+
acquired: Object.freeze([]),
|
|
354
|
+
failed: Object.freeze([]),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (!table) {
|
|
358
|
+
throw new Error("ScenePlan resource operations require host-created resource bindings");
|
|
359
|
+
}
|
|
360
|
+
const scope = createResourceBindingScope(this.resources, table, { signal });
|
|
361
|
+
onScope(scope);
|
|
362
|
+
const leases = scope.acquireAll(assetRefs);
|
|
363
|
+
const acquired = [];
|
|
364
|
+
const failed = [];
|
|
365
|
+
for (const lease of leases) {
|
|
366
|
+
try {
|
|
367
|
+
await lease.resource;
|
|
368
|
+
acquired.push(lease.assetRef);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
failed.push(lease.assetRef);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
this.#assertNotAborted(signal);
|
|
375
|
+
if (failed.length > 0) {
|
|
376
|
+
throw new Error("Resource acquisition failed: " + failed.join(", "));
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
required: Object.freeze([...assetRefs]),
|
|
380
|
+
acquired: Object.freeze(acquired),
|
|
381
|
+
failed: Object.freeze(failed),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
#prepareBindings(options) {
|
|
385
|
+
const allowVariables = options.allowVariables ?? this.context.allowVariables;
|
|
386
|
+
const capabilityAvailability = this.#readCapabilityAvailability();
|
|
387
|
+
const context = {
|
|
388
|
+
...this.context,
|
|
389
|
+
...(capabilityAvailability === undefined
|
|
390
|
+
? {}
|
|
391
|
+
: { capabilityAvailability }),
|
|
392
|
+
...(this.capabilities.length === 0
|
|
393
|
+
? {}
|
|
394
|
+
: { capabilities: this.capabilities }),
|
|
395
|
+
...(allowVariables === undefined ? {} : { allowVariables }),
|
|
396
|
+
};
|
|
397
|
+
if (options.resourceBindings === undefined &&
|
|
398
|
+
options.resourceRequirements === undefined) {
|
|
399
|
+
return { context };
|
|
400
|
+
}
|
|
401
|
+
const table = createResourceBindingTable(options.resourceBindings ?? [], options.resourceRequirements ?? [], { allowAdditionalBindings: true });
|
|
402
|
+
const resourceBindings = {};
|
|
403
|
+
for (const binding of table.bindings) {
|
|
404
|
+
resourceBindings[binding.assetRef] = structuredClone(binding.asset);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
table,
|
|
408
|
+
context: {
|
|
409
|
+
...context,
|
|
410
|
+
resourceBindings,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
#readCapabilityAvailability() {
|
|
415
|
+
try {
|
|
416
|
+
const observed = this.#getCapabilityAvailability
|
|
417
|
+
? this.#getCapabilityAvailability()
|
|
418
|
+
: this.#initialCapabilityAvailability;
|
|
419
|
+
return observed === undefined
|
|
420
|
+
? undefined
|
|
421
|
+
: cloneFrozenAvailability(observed);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
throw new CapabilityAvailabilityReadError();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
#assertCurrentCapabilities(document) {
|
|
428
|
+
const availability = this.#readCapabilityAvailability();
|
|
429
|
+
if (availability === undefined)
|
|
430
|
+
return;
|
|
431
|
+
const unavailable = findUnavailableSceneCapabilities(document, availability)[0];
|
|
432
|
+
if (unavailable)
|
|
433
|
+
throw new SceneCapabilityUnavailableError(unavailable);
|
|
434
|
+
}
|
|
435
|
+
#resolveExpected(options) {
|
|
436
|
+
const token = options.expectedRevisionToken;
|
|
437
|
+
if (token) {
|
|
438
|
+
if (token.sceneId !== this.editor.snapshot.id) {
|
|
439
|
+
throw new Error("Revision token sceneId does not match the active scene");
|
|
440
|
+
}
|
|
441
|
+
if (token.sessionEpoch !== this.sessionEpoch) {
|
|
442
|
+
throw new Error("Revision token belongs to another executor session");
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
revision: token.revision,
|
|
446
|
+
documentHash: token.documentHash,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
revision: options.expectedRevision ?? this.editor.revision,
|
|
451
|
+
...(options.expectedDocumentHash === undefined
|
|
452
|
+
? {}
|
|
453
|
+
: { documentHash: options.expectedDocumentHash }),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
#assertCurrentExpected(expected) {
|
|
457
|
+
const actualRevision = this.editor.revision;
|
|
458
|
+
const actualHash = createSceneDocumentRevisionHash(this.editor.snapshot);
|
|
459
|
+
if (actualRevision !== expected.revision) {
|
|
460
|
+
throw new EditorRevisionConflictError({
|
|
461
|
+
reason: "revision",
|
|
462
|
+
expectedRevision: expected.revision,
|
|
463
|
+
actualRevision,
|
|
464
|
+
expectedDocumentHash: expected.documentHash ?? null,
|
|
465
|
+
actualDocumentHash: actualHash,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (expected.documentHash !== undefined &&
|
|
469
|
+
actualHash !== expected.documentHash) {
|
|
470
|
+
throw new EditorRevisionConflictError({
|
|
471
|
+
reason: "document-hash",
|
|
472
|
+
expectedRevision: expected.revision,
|
|
473
|
+
actualRevision,
|
|
474
|
+
expectedDocumentHash: expected.documentHash,
|
|
475
|
+
actualDocumentHash: actualHash,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
#createRevisionToken() {
|
|
480
|
+
const document = this.editor.snapshot;
|
|
481
|
+
return Object.freeze({
|
|
482
|
+
sceneId: document.id,
|
|
483
|
+
sessionEpoch: this.sessionEpoch,
|
|
484
|
+
revision: this.editor.revision,
|
|
485
|
+
documentHash: createSceneDocumentRevisionHash(document),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
#findExisting(operationId, idempotencyKey, operationNamespace, fingerprint) {
|
|
489
|
+
const existing = this.#operations.get(operationMapKey(operationNamespace, operationId)) ??
|
|
490
|
+
(idempotencyKey
|
|
491
|
+
? this.#idempotency.get(operationMapKey(operationNamespace, idempotencyKey))
|
|
492
|
+
: undefined);
|
|
493
|
+
if (!existing)
|
|
494
|
+
return null;
|
|
495
|
+
if (existing.fingerprint !== fingerprint) {
|
|
496
|
+
throw new Error("ScenePlan operation or idempotency key was already used for a different request");
|
|
497
|
+
}
|
|
498
|
+
return existing.result;
|
|
499
|
+
}
|
|
500
|
+
#result(operationId, options, status, planKind, diagnostics, planHash, commit, changes, resources, projection) {
|
|
501
|
+
const result = {
|
|
502
|
+
operationId,
|
|
503
|
+
...(options.idempotencyKey === undefined
|
|
504
|
+
? {}
|
|
505
|
+
: { idempotencyKey: options.idempotencyKey }),
|
|
506
|
+
sceneId: this.editor.snapshot.id,
|
|
507
|
+
status,
|
|
508
|
+
...(planKind === undefined ? {} : { planKind }),
|
|
509
|
+
...(planHash === undefined ? {} : { planHash }),
|
|
510
|
+
...(commit === undefined
|
|
511
|
+
? {}
|
|
512
|
+
: {
|
|
513
|
+
baseRevision: commit.baseRevision,
|
|
514
|
+
revision: commit.revision,
|
|
515
|
+
changeId: commit.changeId,
|
|
516
|
+
revisionToken: this.#createRevisionToken(),
|
|
517
|
+
}),
|
|
518
|
+
...(changes === undefined ? {} : { changes }),
|
|
519
|
+
...(resources === undefined ? {} : { resources }),
|
|
520
|
+
...(projection === undefined ? {} : { projection }),
|
|
521
|
+
diagnostics: Object.freeze([...diagnostics]),
|
|
522
|
+
};
|
|
523
|
+
return Object.freeze(result);
|
|
524
|
+
}
|
|
525
|
+
#assertActive() {
|
|
526
|
+
if (this.#disposed)
|
|
527
|
+
throw new Error("ScenePlanExecutor has been disposed");
|
|
528
|
+
this.#assertNotAborted(this.#controller.signal);
|
|
529
|
+
}
|
|
530
|
+
#assertNotAborted(signal) {
|
|
531
|
+
if (!signal.aborted)
|
|
532
|
+
return;
|
|
533
|
+
throw createAbortError();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function linkSignals(first, second) {
|
|
537
|
+
const controller = new AbortController();
|
|
538
|
+
const listeners = [];
|
|
539
|
+
const forward = () => controller.abort();
|
|
540
|
+
for (const signal of [first, second]) {
|
|
541
|
+
if (!signal)
|
|
542
|
+
continue;
|
|
543
|
+
if (signal.aborted)
|
|
544
|
+
controller.abort();
|
|
545
|
+
else {
|
|
546
|
+
signal.addEventListener("abort", forward, { once: true });
|
|
547
|
+
listeners.push(() => signal.removeEventListener("abort", forward));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
signal: controller.signal,
|
|
552
|
+
dispose() {
|
|
553
|
+
for (const remove of listeners)
|
|
554
|
+
remove();
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function projectionSummary(result) {
|
|
559
|
+
return {
|
|
560
|
+
status: result.status,
|
|
561
|
+
operationId: result.operationId,
|
|
562
|
+
projectionId: result.projectionId,
|
|
563
|
+
sourceRevision: result.sourceRevision,
|
|
564
|
+
changeId: result.changeId,
|
|
565
|
+
modelFailureEntityIds: Object.freeze(result.modelFailures.map((failure) => failure.entityId)),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function diffDocuments(before, after) {
|
|
569
|
+
const beforeIds = {
|
|
570
|
+
...recordIds(before.assets),
|
|
571
|
+
...recordIds(before.entities),
|
|
572
|
+
...recordIds(before.materials),
|
|
573
|
+
...recordIds(before.materialBindings),
|
|
574
|
+
...recordIds(before.textureAnimations),
|
|
575
|
+
...recordIds(before.cameraPaths),
|
|
576
|
+
...recordIds(before.flows),
|
|
577
|
+
};
|
|
578
|
+
const afterIds = {
|
|
579
|
+
...recordIds(after.assets),
|
|
580
|
+
...recordIds(after.entities),
|
|
581
|
+
...recordIds(after.materials),
|
|
582
|
+
...recordIds(after.materialBindings),
|
|
583
|
+
...recordIds(after.textureAnimations),
|
|
584
|
+
...recordIds(after.cameraPaths),
|
|
585
|
+
...recordIds(after.flows),
|
|
586
|
+
};
|
|
587
|
+
const created = [];
|
|
588
|
+
const updated = [];
|
|
589
|
+
const removed = [];
|
|
590
|
+
for (const id of Object.keys(afterIds)) {
|
|
591
|
+
if (!beforeIds[id])
|
|
592
|
+
created.push(id);
|
|
593
|
+
else if (JSON.stringify(beforeIds[id]) !== JSON.stringify(afterIds[id])) {
|
|
594
|
+
updated.push(id);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const id of Object.keys(beforeIds)) {
|
|
598
|
+
if (!afterIds[id])
|
|
599
|
+
removed.push(id);
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
created: Object.freeze(created.sort()),
|
|
603
|
+
updated: Object.freeze(updated.sort()),
|
|
604
|
+
removed: Object.freeze(removed.sort()),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function recordIds(record) {
|
|
608
|
+
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, value]));
|
|
609
|
+
}
|
|
610
|
+
function detectPlanKind(input) {
|
|
611
|
+
let value = input;
|
|
612
|
+
if (typeof input === "string") {
|
|
613
|
+
try {
|
|
614
|
+
value = JSON.parse(input);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!value || typeof value !== "object")
|
|
621
|
+
return undefined;
|
|
622
|
+
const schema = value.schema;
|
|
623
|
+
if (schema === "skenora.scene.blueprint")
|
|
624
|
+
return "blueprint";
|
|
625
|
+
if (schema === "skenora.scene.patch")
|
|
626
|
+
return "patch";
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
function createSceneOutlinePage(document, revisionToken, options) {
|
|
630
|
+
const limit = options.limit ?? 100;
|
|
631
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
|
|
632
|
+
throw new Error("ScenePlan outline limit must be an integer in 1..500");
|
|
633
|
+
}
|
|
634
|
+
const cursorPrefix = `scene-plan-outline:${encodeURIComponent(revisionToken.sessionEpoch)}:${revisionToken.revision}:${revisionToken.documentHash}:`;
|
|
635
|
+
const offset = parseSceneOutlineOffset(options.cursor, cursorPrefix);
|
|
636
|
+
const roots = new Set(document.rootEntityIds);
|
|
637
|
+
const byId = (left, right) => left.id.localeCompare(right.id);
|
|
638
|
+
const items = [
|
|
639
|
+
...Object.values(document.assets)
|
|
640
|
+
.sort(byId)
|
|
641
|
+
.map((asset) => Object.freeze({
|
|
642
|
+
kind: "asset",
|
|
643
|
+
id: asset.id,
|
|
644
|
+
resourceKind: asset.kind,
|
|
645
|
+
...(asset.label === undefined ? {} : { label: asset.label }),
|
|
646
|
+
})),
|
|
647
|
+
...Object.values(document.entities)
|
|
648
|
+
.sort(byId)
|
|
649
|
+
.map((entity) => Object.freeze({
|
|
650
|
+
kind: "entity",
|
|
651
|
+
id: entity.id,
|
|
652
|
+
name: entity.name,
|
|
653
|
+
entityType: entity.type,
|
|
654
|
+
parentId: entity.parentId,
|
|
655
|
+
root: roots.has(entity.id),
|
|
656
|
+
enabled: entity.enabled,
|
|
657
|
+
visible: entity.visible,
|
|
658
|
+
transform: structuredClone(entity.transform),
|
|
659
|
+
...(entity.assetId === undefined ? {} : { assetId: entity.assetId }),
|
|
660
|
+
})),
|
|
661
|
+
...Object.entries(document.materials)
|
|
662
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
663
|
+
.map(([id, material]) => Object.freeze({
|
|
664
|
+
kind: "material",
|
|
665
|
+
id,
|
|
666
|
+
...(material.workflow === undefined
|
|
667
|
+
? {}
|
|
668
|
+
: { workflow: material.workflow }),
|
|
669
|
+
})),
|
|
670
|
+
...Object.values(document.materialBindings)
|
|
671
|
+
.sort(byId)
|
|
672
|
+
.map((binding) => Object.freeze({
|
|
673
|
+
kind: "materialBinding",
|
|
674
|
+
id: binding.id,
|
|
675
|
+
materialId: binding.materialId,
|
|
676
|
+
entityId: binding.entityId,
|
|
677
|
+
...(binding.targetName === undefined
|
|
678
|
+
? {}
|
|
679
|
+
: { targetName: binding.targetName }),
|
|
680
|
+
})),
|
|
681
|
+
...Object.values(document.textureAnimations)
|
|
682
|
+
.sort(byId)
|
|
683
|
+
.map((animation) => Object.freeze({
|
|
684
|
+
kind: "textureAnimation",
|
|
685
|
+
id: animation.id,
|
|
686
|
+
materialId: animation.materialId,
|
|
687
|
+
slot: animation.slot,
|
|
688
|
+
property: animation.property,
|
|
689
|
+
})),
|
|
690
|
+
...Object.values(document.cameraPaths)
|
|
691
|
+
.sort(byId)
|
|
692
|
+
.map((path) => Object.freeze({
|
|
693
|
+
kind: "cameraPath",
|
|
694
|
+
id: path.id,
|
|
695
|
+
name: path.name,
|
|
696
|
+
durationMs: path.durationMs,
|
|
697
|
+
loop: path.loop,
|
|
698
|
+
keyframeCount: path.keyframes.length,
|
|
699
|
+
})),
|
|
700
|
+
...[...document.lights].sort(byId).map((light) => Object.freeze({
|
|
701
|
+
kind: "light",
|
|
702
|
+
id: light.id,
|
|
703
|
+
lightType: light.type,
|
|
704
|
+
enabled: light.enabled,
|
|
705
|
+
intensity: light.intensity,
|
|
706
|
+
})),
|
|
707
|
+
...Object.values(document.flows)
|
|
708
|
+
.sort(byId)
|
|
709
|
+
.map((flow) => Object.freeze({
|
|
710
|
+
kind: "flow",
|
|
711
|
+
id: flow.id,
|
|
712
|
+
name: flow.name,
|
|
713
|
+
enabled: flow.enabled,
|
|
714
|
+
nodeCount: flow.nodes.length,
|
|
715
|
+
edgeCount: flow.edges.length,
|
|
716
|
+
})),
|
|
717
|
+
];
|
|
718
|
+
if (offset > items.length) {
|
|
719
|
+
throw new Error("ScenePlan outline cursor is outside the current scene");
|
|
720
|
+
}
|
|
721
|
+
const end = Math.min(offset + limit, items.length);
|
|
722
|
+
return Object.freeze({
|
|
723
|
+
items: Object.freeze(items.slice(offset, end)),
|
|
724
|
+
...(end < items.length ? { nextCursor: cursorPrefix + end } : {}),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
function parseSceneOutlineOffset(cursor, expectedPrefix) {
|
|
728
|
+
if (cursor === undefined)
|
|
729
|
+
return 0;
|
|
730
|
+
if (!cursor.startsWith(expectedPrefix)) {
|
|
731
|
+
throw new Error("ScenePlan outline cursor is invalid or stale");
|
|
732
|
+
}
|
|
733
|
+
const offset = Number(cursor.slice(expectedPrefix.length));
|
|
734
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
735
|
+
throw new Error("ScenePlan outline cursor is invalid");
|
|
736
|
+
}
|
|
737
|
+
return offset;
|
|
738
|
+
}
|
|
739
|
+
function createOperationFingerprint(kind, input, options) {
|
|
740
|
+
const plan = parsePlanFingerprintInput(input);
|
|
741
|
+
const canonical = JSON.stringify(canonicalizeOperationValue({
|
|
742
|
+
kind,
|
|
743
|
+
plan,
|
|
744
|
+
expectedRevision: options.expectedRevision,
|
|
745
|
+
expectedRevisionToken: options.expectedRevisionToken,
|
|
746
|
+
expectedDocumentHash: options.expectedDocumentHash,
|
|
747
|
+
label: options.label,
|
|
748
|
+
modelLoadPolicy: options.modelLoadPolicy,
|
|
749
|
+
allowVariables: options.allowVariables,
|
|
750
|
+
}));
|
|
751
|
+
if (canonical === undefined) {
|
|
752
|
+
throw new Error("ScenePlan requests must be JSON-compatible values");
|
|
753
|
+
}
|
|
754
|
+
let hash = 0xcbf29ce484222325n;
|
|
755
|
+
for (const character of canonical) {
|
|
756
|
+
hash ^= BigInt(character.charCodeAt(0));
|
|
757
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
758
|
+
}
|
|
759
|
+
return "fnv1a64:" + hash.toString(16).padStart(16, "0");
|
|
760
|
+
}
|
|
761
|
+
function parsePlanFingerprintInput(input) {
|
|
762
|
+
if (typeof input !== "string")
|
|
763
|
+
return input;
|
|
764
|
+
try {
|
|
765
|
+
return JSON.parse(input);
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return input;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function canonicalizeOperationValue(value, ancestors = new Set()) {
|
|
772
|
+
if (value === null ||
|
|
773
|
+
typeof value === "string" ||
|
|
774
|
+
typeof value === "boolean") {
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
if (typeof value === "number") {
|
|
778
|
+
if (!Number.isFinite(value)) {
|
|
779
|
+
throw new Error("ScenePlan requests must contain finite JSON numbers");
|
|
780
|
+
}
|
|
781
|
+
return Object.is(value, -0) ? 0 : value;
|
|
782
|
+
}
|
|
783
|
+
if (value === undefined)
|
|
784
|
+
return undefined;
|
|
785
|
+
if (typeof value !== "object") {
|
|
786
|
+
throw new Error("ScenePlan requests must be JSON-compatible values");
|
|
787
|
+
}
|
|
788
|
+
if (ancestors.has(value)) {
|
|
789
|
+
throw new Error("ScenePlan requests must not contain circular references");
|
|
790
|
+
}
|
|
791
|
+
ancestors.add(value);
|
|
792
|
+
try {
|
|
793
|
+
if (Array.isArray(value)) {
|
|
794
|
+
return value.map((item) => canonicalizeOperationValue(item, ancestors));
|
|
795
|
+
}
|
|
796
|
+
return Object.fromEntries(Object.entries(value)
|
|
797
|
+
.filter(([, child]) => child !== undefined)
|
|
798
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
799
|
+
.map(([key, child]) => [
|
|
800
|
+
key,
|
|
801
|
+
canonicalizeOperationValue(child, ancestors),
|
|
802
|
+
]));
|
|
803
|
+
}
|
|
804
|
+
finally {
|
|
805
|
+
ancestors.delete(value);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function operationMapKey(operationNamespace, value) {
|
|
809
|
+
if (operationNamespace !== undefined &&
|
|
810
|
+
(!operationNamespace || operationNamespace !== operationNamespace.trim())) {
|
|
811
|
+
throw new Error("ScenePlan operationNamespace must be normalized and non-empty");
|
|
812
|
+
}
|
|
813
|
+
return JSON.stringify([operationNamespace ?? null, value]);
|
|
814
|
+
}
|
|
815
|
+
function resolveOperationId(value) {
|
|
816
|
+
if (value !== undefined) {
|
|
817
|
+
if (!value.trim())
|
|
818
|
+
throw new Error("ScenePlan operationId must be non-empty");
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
operationSequence += 1;
|
|
822
|
+
return "scene-plan-operation-" + Date.now() + "-" + operationSequence;
|
|
823
|
+
}
|
|
824
|
+
function createSessionEpoch() {
|
|
825
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
826
|
+
return "scene-plan-session-" + globalThis.crypto.randomUUID();
|
|
827
|
+
}
|
|
828
|
+
sessionSequence += 1;
|
|
829
|
+
return "scene-plan-session-" + Date.now() + "-" + sessionSequence;
|
|
830
|
+
}
|
|
831
|
+
function unique(values) {
|
|
832
|
+
return Object.freeze([...new Set(values)]);
|
|
833
|
+
}
|
|
834
|
+
function cloneFrozenAvailability(availability) {
|
|
835
|
+
if (!Array.isArray(availability)) {
|
|
836
|
+
throw new TypeError("Capability availability must be an array");
|
|
837
|
+
}
|
|
838
|
+
const clone = structuredClone(availability);
|
|
839
|
+
const ids = new Set();
|
|
840
|
+
for (const item of clone) {
|
|
841
|
+
if (ids.has(item.id)) {
|
|
842
|
+
throw new TypeError("Capability availability IDs must be unique");
|
|
843
|
+
}
|
|
844
|
+
ids.add(item.id);
|
|
845
|
+
deepFreezeObject(item);
|
|
846
|
+
}
|
|
847
|
+
return Object.freeze(clone);
|
|
848
|
+
}
|
|
849
|
+
function cloneFrozenCapabilities(capabilities) {
|
|
850
|
+
const clone = structuredClone(capabilities);
|
|
851
|
+
for (const item of clone)
|
|
852
|
+
deepFreezeObject(item);
|
|
853
|
+
return Object.freeze(clone);
|
|
854
|
+
}
|
|
855
|
+
function deepFreezeObject(value) {
|
|
856
|
+
for (const child of Object.values(value)) {
|
|
857
|
+
if (child && typeof child === "object" && !Object.isFrozen(child)) {
|
|
858
|
+
deepFreezeObject(child);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
Object.freeze(value);
|
|
862
|
+
}
|
|
863
|
+
class CapabilityAvailabilityReadError extends Error {
|
|
864
|
+
constructor() {
|
|
865
|
+
super("Runtime capability availability could not be observed");
|
|
866
|
+
this.name = "CapabilityAvailabilityReadError";
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function diagnostic(code, message, severity = "error") {
|
|
870
|
+
return createDiagnostic({
|
|
871
|
+
code,
|
|
872
|
+
stage: "compile",
|
|
873
|
+
path: [],
|
|
874
|
+
message,
|
|
875
|
+
severity,
|
|
876
|
+
recoverable: severity !== "error",
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
function executionDiagnostic(error) {
|
|
880
|
+
if (error instanceof EditorRevisionConflictError) {
|
|
881
|
+
return diagnostic("editor-revision-conflict", error.message);
|
|
882
|
+
}
|
|
883
|
+
if (isAbortError(error)) {
|
|
884
|
+
return diagnostic("operation-cancelled", "ScenePlan operation was cancelled", "warning");
|
|
885
|
+
}
|
|
886
|
+
if (error instanceof CapabilityAvailabilityReadError) {
|
|
887
|
+
return capabilityAvailabilityDiagnostic();
|
|
888
|
+
}
|
|
889
|
+
if (error instanceof SceneCapabilityUnavailableError) {
|
|
890
|
+
return createDiagnostic({
|
|
891
|
+
code: error.code,
|
|
892
|
+
stage: "compile",
|
|
893
|
+
path: error.usage.path,
|
|
894
|
+
message: error.message,
|
|
895
|
+
recoverable: true,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return diagnostic("scene-plan-execution-failed", error instanceof Error ? error.message : "ScenePlan operation failed");
|
|
899
|
+
}
|
|
900
|
+
function validationSetupDiagnostic(error) {
|
|
901
|
+
return error instanceof CapabilityAvailabilityReadError
|
|
902
|
+
? capabilityAvailabilityDiagnostic()
|
|
903
|
+
: bindingDiagnostic(error);
|
|
904
|
+
}
|
|
905
|
+
function capabilityAvailabilityDiagnostic() {
|
|
906
|
+
return diagnostic("capability-availability-failed", "Runtime capability availability could not be observed");
|
|
907
|
+
}
|
|
908
|
+
function projectionDiagnostic(error) {
|
|
909
|
+
if (error instanceof EditorRuntimeProjectionError) {
|
|
910
|
+
return diagnostic("runtime-projection-failed", error.message);
|
|
911
|
+
}
|
|
912
|
+
return diagnostic("runtime-projection-failed", error instanceof Error ? error.message : "Runtime projection failed");
|
|
913
|
+
}
|
|
914
|
+
function bindingDiagnostic(error) {
|
|
915
|
+
if (error instanceof ResourceBindingValidationError) {
|
|
916
|
+
return diagnostic("resource-binding-invalid", error.message);
|
|
917
|
+
}
|
|
918
|
+
return diagnostic("resource-binding-invalid", error instanceof Error ? error.message : "Resource bindings are invalid");
|
|
919
|
+
}
|
|
920
|
+
function isAbortError(error) {
|
|
921
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
922
|
+
}
|
|
923
|
+
function createAbortError() {
|
|
924
|
+
return new DOMException("ScenePlan operation was cancelled", "AbortError");
|
|
925
|
+
}
|
|
926
|
+
let operationSequence = 0;
|
|
927
|
+
let sessionSequence = 0;
|
|
928
|
+
//# sourceMappingURL=executor.js.map
|