@playcraft/cli 0.1.66 → 0.1.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/assets/client.js +4 -2
  2. package/dist/assets/messages.js +2 -0
  3. package/dist/audio-source/messages.js +35 -0
  4. package/dist/commands/assets.js +1 -1
  5. package/dist/commands/audio-source.js +109 -0
  6. package/dist/commands/audio.js +11 -4
  7. package/dist/commands/build.js +4 -2
  8. package/dist/commands/tools-context.js +46 -0
  9. package/dist/commands/tools.js +2 -0
  10. package/dist/project-skills/builtin-import.js +69 -22
  11. package/dist/project-skills/commands.js +11 -2
  12. package/dist/project-skills/lifecycle.js +1 -1
  13. package/dist/project-skills/local-store.js +90 -31
  14. package/dist/project-skills/messages.js +16 -0
  15. package/dist/project-skills/migrate-lock.js +166 -0
  16. package/dist/project-skills/remote-cache.js +36 -5
  17. package/dist/utils/agent-api-client.js +18 -1
  18. package/dist/utils/agent-generation-provenance.js +8 -2
  19. package/dist/utils/audio-export.js +249 -0
  20. package/dist/utils/audio-processor.js +22 -99
  21. package/dist/utils/execution-context-bridge.js +28 -0
  22. package/dist/workspace-runtime/batch/control.js +13 -2
  23. package/dist/workspace-runtime/batch/generate.js +43 -2
  24. package/dist/workspace-runtime/batch/prepare.js +4 -0
  25. package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
  26. package/dist/workspace-runtime/batch/source-capture.js +6 -8
  27. package/dist/workspace-runtime/codex/app-server.js +26 -2
  28. package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
  29. package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
  30. package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
  31. package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
  32. package/dist/workspace-runtime/codex/model-broker.js +15 -2
  33. package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
  34. package/dist/workspace-runtime/previews/finalize.js +1 -1
  35. package/dist/workspace-runtime/previews/output.js +4 -1
  36. package/dist/workspace-runtime/previews/publish.js +5 -2
  37. package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
  38. package/dist/workspace-runtime/serve.js +6 -0
  39. package/dist/workspace-runtime/server/accept-operation.js +85 -13
  40. package/dist/workspace-runtime/server/audio-assets.js +211 -0
  41. package/dist/workspace-runtime/server/audio-export.js +136 -0
  42. package/dist/workspace-runtime/server/directory-access.js +222 -67
  43. package/dist/workspace-runtime/server/dispatch.js +252 -29
  44. package/dist/workspace-runtime/server/http.js +97 -3
  45. package/dist/workspace-runtime/server/operation-record.js +1 -1
  46. package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
  47. package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
  48. package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
  49. package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
  50. package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +76 -0
  51. package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
  52. package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
  53. package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
  54. package/dist/workspace-runtime/workspaces/audio-assets/service.js +327 -0
  55. package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
  56. package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
  57. package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
  58. package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
  59. package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
  60. package/dist/workspace-runtime/workspaces/files.js +3 -5
  61. package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
  62. package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
  63. package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
  64. package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
  65. package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
  66. package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
  67. package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
  68. package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
  69. package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
  70. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
  71. package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
  72. package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
  73. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
  74. package/dist/workspace-runtime/workspaces/skills.js +48 -13
  75. package/package.json +6 -4
  76. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  77. package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
  78. package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
  79. package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
  80. package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
  81. package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
  82. package/project-template/vite.config.ts +1 -1
  83. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  84. package/project-template-v2/.playcraft/skills.lock.json +9 -2
  85. package/project-template-v2/vite.config.ts +1 -1
  86. package/project-templates/empty/AGENTS.md +2 -0
  87. package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  88. package/project-templates/phaser/vite.config.ts +1 -1
  89. package/project-templates/playable-template-phaser/vite.config.ts +1 -1
  90. package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  91. package/project-templates/threejs/vite.config.ts +1 -1
  92. package/templates/preview-runtime/v1/README.md +12 -0
  93. package/templates/preview-runtime/v1/build-preview.mjs +17 -8
  94. package/templates/preview-runtime/v1/manifest.json +9 -6
  95. package/templates/preview-runtime/v1/release-guard.mjs +1 -1
  96. package/templates/preview-runtime/v1/runtime.d.mts +1 -3
  97. package/templates/preview-runtime/v1/runtime.d.ts +1 -3
  98. package/templates/preview-runtime/v1/runtime.mjs +1142 -257
  99. package/templates/preview-runtime/v1/types/host.d.ts +29 -0
  100. package/templates/preview-runtime/v1/types/index.d.ts +6 -0
  101. package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
@@ -1,3 +1,231 @@
1
+ // ../preview-runtime/src/protocol.ts
2
+ var PROTOCOL_VERSION = 1;
3
+ var MAX_IMAGE_RESOURCE_BYTES = 32 * 1024 * 1024;
4
+ var ResourceLoadError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "ResourceLoadError";
8
+ }
9
+ };
10
+ var SupersededResourceError = class extends Error {
11
+ constructor() {
12
+ super("A newer image value replaced this resource request");
13
+ this.name = "SupersededResourceError";
14
+ }
15
+ };
16
+ var ProtocolError = class extends Error {
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.code = code;
20
+ this.name = "ProtocolError";
21
+ }
22
+ code;
23
+ };
24
+ var forbidden = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
25
+ var record = (value) => !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
26
+ var exactKeys = (value, allowed) => {
27
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
28
+ throw new ProtocolError("INVALID", "Unexpected message field");
29
+ };
30
+ var string = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024;
31
+ var revision = (v) => Number.isSafeInteger(v) && v >= 0;
32
+ function assertJson(value, depth = 0) {
33
+ if (depth > 64) throw new ProtocolError("LIMIT", "Configuration nesting exceeds limit");
34
+ if (value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value))
35
+ return;
36
+ if (Array.isArray(value)) {
37
+ for (const child of value) assertJson(child, depth + 1);
38
+ return;
39
+ }
40
+ if (record(value)) {
41
+ for (const [key, child] of Object.entries(value)) {
42
+ if (forbidden.has(key)) throw new ProtocolError("INVALID", "Unsafe configuration key");
43
+ assertJson(child, depth + 1);
44
+ }
45
+ return;
46
+ }
47
+ throw new ProtocolError("INVALID", "Configuration must contain finite JSON data");
48
+ }
49
+ function clone(value) {
50
+ return structuredClone(value);
51
+ }
52
+ function freeze(value) {
53
+ if (value && typeof value === "object") {
54
+ Object.freeze(value);
55
+ for (const child of Object.values(value)) freeze(child);
56
+ }
57
+ return value;
58
+ }
59
+ function fingerprint(value) {
60
+ if (Array.isArray(value)) return "[" + value.map(fingerprint).join(",") + "]";
61
+ if (record(value))
62
+ return "{" + Object.keys(value).sort().map((k) => JSON.stringify(k) + ":" + fingerprint(value[k])).join(",") + "}";
63
+ return JSON.stringify(value);
64
+ }
65
+ function assertHello(value) {
66
+ if (!record(value) || value.protocolVersion !== 1 || !string(value.sessionId) || !revision(value.frameEpoch) || !string(value.schemaVersion) || !string(value.configContractVersion) || !string(value.locale) || !record(value.scope))
67
+ throw new ProtocolError("IDENTITY", "Invalid runtime session binding");
68
+ if (!["projectId", "branchName", "themeId"].every(
69
+ (k) => string(value.scope[k])
70
+ ))
71
+ throw new ProtocolError("IDENTITY", "Invalid configuration scope");
72
+ exactKeys(value.scope, ["projectId", "branchName", "themeId"]);
73
+ }
74
+ function assertIdentity(value) {
75
+ assertHello(value);
76
+ if (!string(value.runtimeInstanceId))
77
+ throw new ProtocolError("IDENTITY", "Missing runtime instance");
78
+ }
79
+ function sameIdentity(a, b) {
80
+ try {
81
+ assertIdentity(b);
82
+ } catch {
83
+ return false;
84
+ }
85
+ return [
86
+ "protocolVersion",
87
+ "sessionId",
88
+ "frameEpoch",
89
+ "runtimeInstanceId",
90
+ "schemaVersion",
91
+ "configContractVersion",
92
+ "locale"
93
+ ].every((k) => a[k] === b[k]) && fingerprint(a.scope) === fingerprint(b.scope);
94
+ }
95
+ function assertBinding(expected, actual) {
96
+ if (fingerprint(expected.scope) !== fingerprint(actual.scope) || expected.locale !== actual.locale || expected.schemaVersion !== actual.schemaVersion || expected.configContractVersion !== actual.configContractVersion)
97
+ throw new ProtocolError("CONTRACT", "Scope, locale, schema or configuration contract mismatch");
98
+ }
99
+ function assertDocuments(value) {
100
+ assertJson(value);
101
+ if (!record(value) || Object.values(value).some((v) => !record(v)))
102
+ throw new ProtocolError("INVALID", "Expected raw document objects");
103
+ }
104
+ var identityKeys = [
105
+ "protocolVersion",
106
+ "sessionId",
107
+ "frameEpoch",
108
+ "runtimeInstanceId",
109
+ "scope",
110
+ "schemaVersion",
111
+ "configContractVersion",
112
+ "locale"
113
+ ];
114
+ function assertImageResourceRequest(value) {
115
+ assertIdentity(value);
116
+ const request = value;
117
+ exactKeys(value, [
118
+ ...identityKeys,
119
+ "draftRevision",
120
+ "documentId",
121
+ "path",
122
+ "value",
123
+ "resourceSequence",
124
+ "expectedContentHash"
125
+ ]);
126
+ if (!revision(request.draftRevision) || !string(request.documentId) || forbidden.has(request.documentId) || !Array.isArray(request.path) || !request.path.length || request.path.length > 32 || request.path.some((part) => !string(part) || forbidden.has(part)) || !string(request.value) || (request.resourceSequence !== void 0 || request.expectedContentHash !== void 0) && (!revision(request.resourceSequence) || request.resourceSequence === 0 || request.expectedContentHash !== void 0 && (typeof request.expectedContentHash !== "string" || !/^[a-f0-9]{64}$/.test(request.expectedContentHash))))
127
+ throw new ProtocolError("INVALID", "Invalid image resource request");
128
+ }
129
+ function assertImageRefresh(value) {
130
+ assertIdentity(value);
131
+ const request = value;
132
+ exactKeys(value, [
133
+ ...identityKeys,
134
+ "draftRevision",
135
+ "resourceSequence",
136
+ "images"
137
+ ]);
138
+ if (!revision(request.draftRevision) || !revision(request.resourceSequence) || request.resourceSequence === 0 || !Array.isArray(request.images) || !request.images.length || request.images.length > 64)
139
+ throw new ProtocolError("INVALID", "Invalid image refresh sequence or target count");
140
+ const seen = /* @__PURE__ */ new Set();
141
+ for (const image of request.images) {
142
+ if (!record(image) || !string(image.documentId) || forbidden.has(image.documentId) || !Array.isArray(image.path) || !image.path.length || image.path.length > 32 || image.path.some((part) => !string(part) || forbidden.has(part)) || image.contentHash !== null && (typeof image.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(image.contentHash)))
143
+ throw new ProtocolError("INVALID", "Invalid image refresh target");
144
+ exactKeys(image, ["documentId", "path", "contentHash"]);
145
+ const key = fingerprint([image.documentId, image.path]);
146
+ if (seen.has(key)) throw new ProtocolError("INVALID", "Duplicate image refresh target");
147
+ seen.add(key);
148
+ }
149
+ limit(value, 64 * 1024);
150
+ }
151
+ function assertImageResourceData(value) {
152
+ if (!record(value) || typeof value.mimeType !== "string" || typeof value.dataUrl !== "string")
153
+ throw new ProtocolError("INVALID", "Invalid image resource response");
154
+ const allowed = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif", "image/bmp"];
155
+ if (!allowed.includes(value.mimeType))
156
+ throw new ProtocolError("INVALID", "Unsupported image resource type");
157
+ const prefix = `data:${value.mimeType};base64,`;
158
+ const data = value.dataUrl.startsWith(prefix) ? value.dataUrl.slice(prefix.length) : "";
159
+ if (!data || data.length % 4 !== 0 || data.length > Math.ceil(MAX_IMAGE_RESOURCE_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
160
+ throw new ProtocolError("LIMIT", "Invalid or oversized image resource data");
161
+ const bytes = data.length / 4 * 3 - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
162
+ if (bytes > MAX_IMAGE_RESOURCE_BYTES)
163
+ throw new ProtocolError("LIMIT", "Image resource exceeds the byte limit");
164
+ if (value.contentHash !== void 0 && !/^[a-f0-9]{64}$/.test(String(value.contentHash)))
165
+ throw new ProtocolError("INVALID", "Invalid image content fingerprint");
166
+ }
167
+ function assertSnapshot(value) {
168
+ assertIdentity(value);
169
+ exactKeys(value, [
170
+ ...identityKeys,
171
+ "draftRevision",
172
+ "documents"
173
+ ]);
174
+ if (!revision(value.draftRevision))
175
+ throw new ProtocolError("INVALID", "Invalid snapshot revision");
176
+ assertDocuments(value.documents);
177
+ limit(value, 512 * 1024);
178
+ }
179
+ function assertPatch(value) {
180
+ assertIdentity(value);
181
+ const v = value;
182
+ exactKeys(v, [
183
+ ...identityKeys,
184
+ "baseDraftRevision",
185
+ "draftRevision",
186
+ "phase",
187
+ "operations"
188
+ ]);
189
+ if (!revision(v.baseDraftRevision) || !revision(v.draftRevision) || v.draftRevision !== v.baseDraftRevision + 1 || !["preview", "commit"].includes(v.phase) || !Array.isArray(v.operations) || v.operations.length > 256)
190
+ throw new ProtocolError("INVALID", "Invalid patch sequence or operation count");
191
+ for (const op of v.operations) {
192
+ if (!record(op) || !["set", "remove"].includes(op.op) || !string(op.documentId) || forbidden.has(op.documentId) || !Array.isArray(op.path) || !op.path.length || op.path.length > 32 || op.path.some((p) => !string(p) || forbidden.has(p)))
193
+ throw new ProtocolError("INVALID", "Unsafe or invalid operation path");
194
+ exactKeys(
195
+ op,
196
+ op.op === "set" ? ["op", "documentId", "path", "value"] : ["op", "documentId", "path"]
197
+ );
198
+ if (op.op === "set") assertJson(op.value);
199
+ }
200
+ limit(v, 64 * 1024);
201
+ }
202
+ function limit(value, bytes) {
203
+ if (new TextEncoder().encode(JSON.stringify(value)).length > bytes)
204
+ throw new ProtocolError("LIMIT", "Runtime message exceeds byte limit");
205
+ }
206
+ function applyOperations(documents, operations) {
207
+ const next = clone(documents);
208
+ for (const op of operations) {
209
+ if (op.op === "remove" && readPath(next[op.documentId], op.path) === void 0) continue;
210
+ let target = next[op.documentId] ??= {};
211
+ for (const part of op.path.slice(0, -1)) {
212
+ if (!record(target[part])) target[part] = {};
213
+ target = target[part];
214
+ }
215
+ const key = op.path[op.path.length - 1];
216
+ if (op.op === "set") target[key] = clone(op.value);
217
+ else delete target[key];
218
+ }
219
+ return next;
220
+ }
221
+ function readPath(value, path) {
222
+ for (const key of path) {
223
+ if (!record(value) && !Array.isArray(value)) return void 0;
224
+ value = value[key];
225
+ }
226
+ return value;
227
+ }
228
+
1
229
  // ../iframe-bridge/package.json
2
230
  var package_default = {
3
231
  name: "@playcraft/iframe-bridge",
@@ -48,7 +276,7 @@ var package_default = {
48
276
  };
49
277
 
50
278
  // ../iframe-bridge/src/configs/constants.ts
51
- var PROTOCOL_VERSION = package_default.version;
279
+ var PROTOCOL_VERSION2 = package_default.version;
52
280
  var DEFAULT_REQUEST_TIMEOUT_MS = 15e4;
53
281
  var BridgeErrorCodes = {
54
282
  ABORTED: "ABORTED",
@@ -110,11 +338,11 @@ function isPlaycraftEnvelopeShape(data) {
110
338
  function getPlaycraftEnvelopeVersionMismatch(data) {
111
339
  if (!isPlaycraftEnvelopeShape(data)) return void 0;
112
340
  const peer = data.__playcraft__;
113
- if (peer === PROTOCOL_VERSION) return void 0;
114
- return { local: PROTOCOL_VERSION, peer };
341
+ if (peer === PROTOCOL_VERSION2) return void 0;
342
+ return { local: PROTOCOL_VERSION2, peer };
115
343
  }
116
344
  function createEnvelope(kind, id, name, data) {
117
- return { __playcraft__: PROTOCOL_VERSION, kind, id, name, data };
345
+ return { __playcraft__: PROTOCOL_VERSION2, kind, id, name, data };
118
346
  }
119
347
  function createSuccessResponse(id, name, data) {
120
348
  return { ...createEnvelope("res", id, name), ret: RetCodes.OK, data };
@@ -202,6 +430,12 @@ var BridgeMethods = {
202
430
  capabilities: "runtime.parameters.capabilities",
203
431
  result: "runtime.parameters.result",
204
432
  resyncRequired: "runtime.parameters.resync-required"
433
+ },
434
+ levelEditor: {
435
+ hello: "runtime.level-editor.hello",
436
+ snapshot: "runtime.level-editor.snapshot",
437
+ cancel: "runtime.level-editor.cancel",
438
+ dispose: "runtime.level-editor.dispose"
205
439
  }
206
440
  };
207
441
  var BRIDGE_EVENT_NAMES = [
@@ -224,7 +458,11 @@ var BRIDGE_RPC_NAMES = [
224
458
  BridgeMethods.runtimeParameters.status,
225
459
  BridgeMethods.runtimeParameters.dispose,
226
460
  BridgeMethods.runtimeParameters.asset,
227
- BridgeMethods.runtimeParameters.refreshImages
461
+ BridgeMethods.runtimeParameters.refreshImages,
462
+ BridgeMethods.levelEditor.hello,
463
+ BridgeMethods.levelEditor.snapshot,
464
+ BridgeMethods.levelEditor.cancel,
465
+ BridgeMethods.levelEditor.dispose
228
466
  ];
229
467
 
230
468
  // ../iframe-bridge/src/configs/perspective.ts
@@ -479,264 +717,471 @@ var Bridge = class {
479
717
  addEventListener(type, listener) {
480
718
  this.assertAlive();
481
719
  this.listeners.add(listener);
482
- return () => this.listeners.delete(listener);
483
- }
484
- destroy() {
485
- if (this.destroyed) return;
486
- this.destroyed = true;
487
- this.messageTarget.removeEventListener("message", this.onWindowMessage);
488
- this.listeners.clear();
489
- }
490
- assertAlive() {
491
- if (this.destroyed) {
492
- throw new PlaycraftBridgeError(BridgeErrorCodes.DESTROYED, "[playcraft] bridge destroyed");
493
- }
494
- }
495
- log(event, msg) {
496
- const { debug, perspective } = this.options;
497
- if (!debug) return;
498
- if (typeof debug === "function") debug(event, msg);
499
- else logBridgeDebug(perspective, event, msg);
500
- }
501
- warnVersionMismatch(localVersion, peerVersion) {
502
- if (this.warnedPeerVersions.has(peerVersion)) return;
503
- this.warnedPeerVersions.add(peerVersion);
504
- console.warn(
505
- `[playcraft] protocol version mismatch: local ${localVersion}, peer ${peerVersion}. Messages are still processed; align @playcraft/iframe-bridge versions when possible.`
506
- );
507
- }
508
- onWindowMessage = (e) => {
509
- const msgEvent = e;
510
- if (this.destroyed || !this.options.allowedOrigins.includes(msgEvent.origin)) return;
511
- if (this.options.strictSource) {
512
- const peer = this.options.peer();
513
- if (!peer || msgEvent.source !== peer) return;
514
- }
515
- if (!isPlaycraftEnvelope(msgEvent.data)) return;
516
- const mismatch = getPlaycraftEnvelopeVersionMismatch(msgEvent.data);
517
- if (mismatch) {
518
- this.warnVersionMismatch(mismatch.local, mismatch.peer);
519
- }
520
- this.log("recv", msgEvent.data);
521
- const event = {
522
- data: msgEvent.data,
523
- origin: msgEvent.origin,
524
- source: msgEvent.source
525
- };
526
- this.listeners.forEach((fn) => fn(event));
527
- };
528
- };
529
-
530
- // ../preview-runtime/src/protocol.ts
531
- var MAX_IMAGE_RESOURCE_BYTES = 32 * 1024 * 1024;
532
- var ResourceLoadError = class extends Error {
533
- constructor(message) {
534
- super(message);
535
- this.name = "ResourceLoadError";
536
- }
537
- };
538
- var SupersededResourceError = class extends Error {
539
- constructor() {
540
- super("A newer image value replaced this resource request");
541
- this.name = "SupersededResourceError";
542
- }
543
- };
544
- var ProtocolError = class extends Error {
545
- constructor(code, message) {
546
- super(message);
547
- this.code = code;
548
- this.name = "ProtocolError";
549
- }
550
- code;
551
- };
552
- var forbidden = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
553
- var record = (value) => !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
554
- var exactKeys = (value, allowed) => {
555
- if (Object.keys(value).some((key) => !allowed.includes(key)))
556
- throw new ProtocolError("INVALID", "Unexpected message field");
557
- };
558
- var string = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024;
559
- var revision = (v) => Number.isSafeInteger(v) && v >= 0;
560
- function assertJson(value, depth = 0) {
561
- if (depth > 64) throw new ProtocolError("LIMIT", "Configuration nesting exceeds limit");
562
- if (value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value))
563
- return;
564
- if (Array.isArray(value)) {
565
- for (const child of value) assertJson(child, depth + 1);
566
- return;
567
- }
568
- if (record(value)) {
569
- for (const [key, child] of Object.entries(value)) {
570
- if (forbidden.has(key)) throw new ProtocolError("INVALID", "Unsafe configuration key");
571
- assertJson(child, depth + 1);
572
- }
573
- return;
574
- }
575
- throw new ProtocolError("INVALID", "Configuration must contain finite JSON data");
576
- }
577
- function clone(value) {
578
- return structuredClone(value);
579
- }
580
- function freeze(value) {
581
- if (value && typeof value === "object") {
582
- Object.freeze(value);
583
- for (const child of Object.values(value)) freeze(child);
584
- }
585
- return value;
586
- }
587
- function fingerprint(value) {
588
- if (Array.isArray(value)) return "[" + value.map(fingerprint).join(",") + "]";
589
- if (record(value))
590
- return "{" + Object.keys(value).sort().map((k) => JSON.stringify(k) + ":" + fingerprint(value[k])).join(",") + "}";
591
- return JSON.stringify(value);
592
- }
593
- function assertHello(value) {
594
- if (!record(value) || value.protocolVersion !== 1 || !string(value.sessionId) || !revision(value.frameEpoch) || !string(value.schemaVersion) || !string(value.configContractVersion) || !string(value.locale) || !record(value.scope))
595
- throw new ProtocolError("IDENTITY", "Invalid runtime session binding");
596
- if (!["projectId", "branchName", "themeId"].every(
597
- (k) => string(value.scope[k])
598
- ))
599
- throw new ProtocolError("IDENTITY", "Invalid configuration scope");
600
- exactKeys(value.scope, ["projectId", "branchName", "themeId"]);
601
- }
602
- function assertIdentity(value) {
603
- assertHello(value);
604
- if (!string(value.runtimeInstanceId))
605
- throw new ProtocolError("IDENTITY", "Missing runtime instance");
606
- }
607
- function sameIdentity(a, b) {
608
- try {
609
- assertIdentity(b);
610
- } catch {
611
- return false;
612
- }
613
- return [
614
- "protocolVersion",
615
- "sessionId",
616
- "frameEpoch",
617
- "runtimeInstanceId",
618
- "schemaVersion",
619
- "configContractVersion",
620
- "locale"
621
- ].every((k) => a[k] === b[k]) && fingerprint(a.scope) === fingerprint(b.scope);
622
- }
623
- function assertBinding(expected, actual) {
624
- if (fingerprint(expected.scope) !== fingerprint(actual.scope) || expected.locale !== actual.locale || expected.schemaVersion !== actual.schemaVersion || expected.configContractVersion !== actual.configContractVersion)
625
- throw new ProtocolError("CONTRACT", "Scope, locale, schema or configuration contract mismatch");
626
- }
627
- function assertDocuments(value) {
628
- assertJson(value);
629
- if (!record(value) || Object.values(value).some((v) => !record(v)))
630
- throw new ProtocolError("INVALID", "Expected raw document objects");
631
- }
632
- var identityKeys = [
633
- "protocolVersion",
634
- "sessionId",
635
- "frameEpoch",
636
- "runtimeInstanceId",
637
- "scope",
638
- "schemaVersion",
639
- "configContractVersion",
640
- "locale"
641
- ];
642
- function assertImageRefresh(value) {
643
- assertIdentity(value);
644
- const request = value;
645
- exactKeys(value, [
646
- ...identityKeys,
647
- "draftRevision",
648
- "resourceSequence",
649
- "images"
650
- ]);
651
- if (!revision(request.draftRevision) || !revision(request.resourceSequence) || request.resourceSequence === 0 || !Array.isArray(request.images) || !request.images.length || request.images.length > 64)
652
- throw new ProtocolError("INVALID", "Invalid image refresh sequence or target count");
653
- const seen = /* @__PURE__ */ new Set();
654
- for (const image of request.images) {
655
- if (!record(image) || !string(image.documentId) || forbidden.has(image.documentId) || !Array.isArray(image.path) || !image.path.length || image.path.length > 32 || image.path.some((part) => !string(part) || forbidden.has(part)) || image.contentHash !== null && (typeof image.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(image.contentHash)))
656
- throw new ProtocolError("INVALID", "Invalid image refresh target");
657
- exactKeys(image, ["documentId", "path", "contentHash"]);
658
- const key = fingerprint([image.documentId, image.path]);
659
- if (seen.has(key)) throw new ProtocolError("INVALID", "Duplicate image refresh target");
660
- seen.add(key);
661
- }
662
- limit(value, 64 * 1024);
663
- }
664
- function assertImageResourceData(value) {
665
- if (!record(value) || typeof value.mimeType !== "string" || typeof value.dataUrl !== "string")
666
- throw new ProtocolError("INVALID", "Invalid image resource response");
667
- const allowed = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif", "image/bmp"];
668
- if (!allowed.includes(value.mimeType))
669
- throw new ProtocolError("INVALID", "Unsupported image resource type");
670
- const prefix = `data:${value.mimeType};base64,`;
671
- const data = value.dataUrl.startsWith(prefix) ? value.dataUrl.slice(prefix.length) : "";
672
- if (!data || data.length % 4 !== 0 || data.length > Math.ceil(MAX_IMAGE_RESOURCE_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
673
- throw new ProtocolError("LIMIT", "Invalid or oversized image resource data");
674
- const bytes = data.length / 4 * 3 - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
675
- if (bytes > MAX_IMAGE_RESOURCE_BYTES)
676
- throw new ProtocolError("LIMIT", "Image resource exceeds the byte limit");
677
- if (value.contentHash !== void 0 && !/^[a-f0-9]{64}$/.test(String(value.contentHash)))
678
- throw new ProtocolError("INVALID", "Invalid image content fingerprint");
679
- }
680
- function assertSnapshot(value) {
681
- assertIdentity(value);
682
- exactKeys(value, [
683
- ...identityKeys,
684
- "draftRevision",
685
- "documents"
686
- ]);
687
- if (!revision(value.draftRevision))
688
- throw new ProtocolError("INVALID", "Invalid snapshot revision");
689
- assertDocuments(value.documents);
690
- limit(value, 512 * 1024);
691
- }
692
- function assertPatch(value) {
693
- assertIdentity(value);
694
- const v = value;
695
- exactKeys(v, [
696
- ...identityKeys,
697
- "baseDraftRevision",
698
- "draftRevision",
699
- "phase",
700
- "operations"
701
- ]);
702
- if (!revision(v.baseDraftRevision) || !revision(v.draftRevision) || v.draftRevision !== v.baseDraftRevision + 1 || !["preview", "commit"].includes(v.phase) || !Array.isArray(v.operations) || v.operations.length > 256)
703
- throw new ProtocolError("INVALID", "Invalid patch sequence or operation count");
704
- for (const op of v.operations) {
705
- if (!record(op) || !["set", "remove"].includes(op.op) || !string(op.documentId) || forbidden.has(op.documentId) || !Array.isArray(op.path) || !op.path.length || op.path.length > 32 || op.path.some((p) => !string(p) || forbidden.has(p)))
706
- throw new ProtocolError("INVALID", "Unsafe or invalid operation path");
707
- exactKeys(
708
- op,
709
- op.op === "set" ? ["op", "documentId", "path", "value"] : ["op", "documentId", "path"]
720
+ return () => this.listeners.delete(listener);
721
+ }
722
+ destroy() {
723
+ if (this.destroyed) return;
724
+ this.destroyed = true;
725
+ this.messageTarget.removeEventListener("message", this.onWindowMessage);
726
+ this.listeners.clear();
727
+ }
728
+ assertAlive() {
729
+ if (this.destroyed) {
730
+ throw new PlaycraftBridgeError(BridgeErrorCodes.DESTROYED, "[playcraft] bridge destroyed");
731
+ }
732
+ }
733
+ log(event, msg) {
734
+ const { debug, perspective } = this.options;
735
+ if (!debug) return;
736
+ if (typeof debug === "function") debug(event, msg);
737
+ else logBridgeDebug(perspective, event, msg);
738
+ }
739
+ warnVersionMismatch(localVersion, peerVersion) {
740
+ if (this.warnedPeerVersions.has(peerVersion)) return;
741
+ this.warnedPeerVersions.add(peerVersion);
742
+ console.warn(
743
+ `[playcraft] protocol version mismatch: local ${localVersion}, peer ${peerVersion}. Messages are still processed; align @playcraft/iframe-bridge versions when possible.`
710
744
  );
711
- if (op.op === "set") assertJson(op.value);
712
745
  }
713
- limit(v, 64 * 1024);
746
+ onWindowMessage = (e) => {
747
+ const msgEvent = e;
748
+ if (this.destroyed || !this.options.allowedOrigins.includes(msgEvent.origin)) return;
749
+ if (this.options.strictSource) {
750
+ const peer = this.options.peer();
751
+ if (!peer || msgEvent.source !== peer) return;
752
+ }
753
+ if (!isPlaycraftEnvelope(msgEvent.data)) return;
754
+ const mismatch = getPlaycraftEnvelopeVersionMismatch(msgEvent.data);
755
+ if (mismatch) {
756
+ this.warnVersionMismatch(mismatch.local, mismatch.peer);
757
+ }
758
+ this.log("recv", msgEvent.data);
759
+ const event = {
760
+ data: msgEvent.data,
761
+ origin: msgEvent.origin,
762
+ source: msgEvent.source
763
+ };
764
+ this.listeners.forEach((fn) => fn(event));
765
+ };
766
+ };
767
+
768
+ // ../preview-runtime/src/host.ts
769
+ function validateCapabilities(value, binding) {
770
+ assertIdentity(value);
771
+ assertBinding(binding, value);
772
+ const cap = value;
773
+ if (!["bootstrap-ready", "game-ready"].includes(cap.stage) || cap.canAcceptSnapshot !== true || cap.canRefreshImages !== void 0 && cap.canRefreshImages !== true || cap.canReportMissingImages !== void 0 && (cap.canReportMissingImages !== true || cap.canRefreshImages !== true) || !Array.isArray(cap.fields) || cap.fields.some(
774
+ (field) => !field || typeof field.documentId !== "string" || !Array.isArray(field.path) || !field.path.every((p) => typeof p === "string") || !Array.isArray(field.effectivePath) || !field.effectivePath.every((p) => typeof p === "string") || typeof field.groupId !== "string" || field.resource !== void 0 && field.resource !== "image" || !["live", "restart-scene", "reload"].includes(field.mode)
775
+ ))
776
+ throw new ProtocolError("INVALID", "Invalid runtime capabilities");
777
+ return clone(cap);
714
778
  }
715
- function limit(value, bytes) {
716
- if (new TextEncoder().encode(JSON.stringify(value)).length > bytes)
717
- throw new ProtocolError("LIMIT", "Runtime message exceeds byte limit");
779
+ function validateResult(value, identity) {
780
+ if (!sameIdentity(identity, value)) throw new ProtocolError("STALE", "Stale result identity");
781
+ const result = value;
782
+ if (!revision(result.resultSequence) || ![result.receivedRevision, result.installedRevision, result.appliedRevision].every(
783
+ (v) => Number.isSafeInteger(v) && v >= -1
784
+ ) || result.appliedRevision > result.receivedRevision || result.installedRevision > result.receivedRevision || !["idle", "applying", "applied", "pending-commit", "requires-reload", "error"].includes(
785
+ result.status
786
+ ) || result.recoverable !== void 0 && result.recoverable !== true || result.resourceErrors !== void 0 && (!Array.isArray(result.resourceErrors) || result.resourceErrors.some(
787
+ (item) => !item || typeof item.documentId !== "string" || !Array.isArray(item.path) || !item.path.every((part) => typeof part === "string") || typeof item.error !== "string"
788
+ )) || !Array.isArray(result.groups) || result.groups.some(
789
+ (g) => !g || typeof g.groupId !== "string" || !Number.isSafeInteger(g.appliedRevision) || !["applied", "pending-commit", "requires-reload", "unknown"].includes(g.status)
790
+ ))
791
+ throw new ProtocolError("INVALID", "Invalid application result");
792
+ return clone(result);
718
793
  }
719
- function applyOperations(documents, operations) {
720
- const next = clone(documents);
721
- for (const op of operations) {
722
- if (op.op === "remove" && readPath(next[op.documentId], op.path) === void 0) continue;
723
- let target = next[op.documentId] ??= {};
724
- for (const part of op.path.slice(0, -1)) {
725
- if (!record(target[part])) target[part] = {};
726
- target = target[part];
794
+ function createParameterHost(options) {
795
+ const origin = new URL(options.targetOrigin).origin;
796
+ if (origin === "null" || origin !== options.targetOrigin)
797
+ throw new ProtocolError("ORIGIN", "An exact preview origin is required");
798
+ const opaque = options.transport === "opaque-sandbox";
799
+ if (options.transport !== void 0 && options.transport !== "origin" && !opaque)
800
+ throw new ProtocolError("ORIGIN", "Unknown preview transport");
801
+ function assertFrameOrigin() {
802
+ if (!opaque) return;
803
+ let frameUrl;
804
+ try {
805
+ frameUrl = new URL(options.iframe.src);
806
+ } catch {
807
+ throw new ProtocolError("ORIGIN", "An absolute preview address is required");
727
808
  }
728
- const key = op.path[op.path.length - 1];
729
- if (op.op === "set") target[key] = clone(op.value);
730
- else delete target[key];
809
+ if (!["http:", "https:"].includes(frameUrl.protocol) || frameUrl.origin !== origin)
810
+ throw new ProtocolError("ORIGIN", "The preview address does not match its expected origin");
731
811
  }
732
- return next;
733
- }
734
- function readPath(value, path) {
735
- for (const key of path) {
736
- if (!record(value) && !Array.isArray(value)) return void 0;
737
- value = value[key];
812
+ assertFrameOrigin();
813
+ assertDocuments(options.documents);
814
+ const binding = freeze(clone(options.binding));
815
+ const sessionId = crypto.randomUUID();
816
+ let frameEpoch = 0, identity;
817
+ let bridge;
818
+ let abort = new AbortController();
819
+ let serial = Promise.resolve();
820
+ let state = {
821
+ connection: "idle",
822
+ draftRevision: 0,
823
+ documents: freeze(clone(options.documents))
824
+ };
825
+ let disposed = false;
826
+ const documentHistory = /* @__PURE__ */ new Map([[0, state.documents]]);
827
+ const imageRequests = /* @__PURE__ */ new Map();
828
+ const refreshTargets = /* @__PURE__ */ new Map();
829
+ let resourceSequence = 0;
830
+ let imageReads = 0;
831
+ const imageWaiters = /* @__PURE__ */ new Set();
832
+ async function imageSlot(signal) {
833
+ while (imageReads >= 2) {
834
+ await new Promise((resolve, reject) => {
835
+ const wake = () => {
836
+ imageWaiters.delete(wake);
837
+ signal.removeEventListener("abort", cancelled);
838
+ resolve();
839
+ };
840
+ const cancelled = () => {
841
+ imageWaiters.delete(wake);
842
+ reject(signal.reason);
843
+ };
844
+ signal.throwIfAborted();
845
+ signal.addEventListener("abort", cancelled, { once: true });
846
+ imageWaiters.add(wake);
847
+ });
848
+ }
849
+ signal.throwIfAborted();
850
+ imageReads++;
851
+ return () => {
852
+ imageReads--;
853
+ [...imageWaiters].forEach((wake) => wake());
854
+ };
738
855
  }
739
- return value;
856
+ const names = BridgeMethods.runtimeParameters;
857
+ const getState = () => freeze(clone(state));
858
+ const publish = () => options.onState?.(getState());
859
+ function receiveResult(value, currentIdentity) {
860
+ const result = validateResult(value, currentIdentity);
861
+ if (state.result && result.resultSequence < state.result.resultSequence) return;
862
+ state = { ...state, result };
863
+ publish();
864
+ }
865
+ function failed(error, epoch) {
866
+ if (epoch !== frameEpoch || disposed) return;
867
+ state = {
868
+ ...state,
869
+ connection: "error",
870
+ error: error instanceof Error ? error.message : String(error)
871
+ };
872
+ publish();
873
+ }
874
+ function receipt(value, expected) {
875
+ const r = value;
876
+ if (!r || r.accepted !== true || r.receivedRevision !== expected)
877
+ throw new ProtocolError("INVALID", "Invalid acceptance receipt");
878
+ return r;
879
+ }
880
+ const host = {
881
+ getState,
882
+ async connect() {
883
+ if (disposed) throw new ProtocolError("DISPOSED", "Preview host is disposed");
884
+ const epoch = ++frameEpoch;
885
+ abort.abort();
886
+ abort = new AbortController();
887
+ const signal = abort.signal;
888
+ bridge?.destroy();
889
+ bridge = void 0;
890
+ identity = void 0;
891
+ serial = Promise.resolve();
892
+ refreshTargets.clear();
893
+ resourceSequence = 0;
894
+ state = {
895
+ ...state,
896
+ connection: "connecting",
897
+ capabilities: void 0,
898
+ result: void 0,
899
+ error: void 0
900
+ };
901
+ publish();
902
+ try {
903
+ assertFrameOrigin();
904
+ } catch (error) {
905
+ failed(error, epoch);
906
+ throw error;
907
+ }
908
+ const current = new Bridge({
909
+ peer: () => options.iframe.contentWindow,
910
+ targetOrigin: opaque ? "*" : origin,
911
+ allowedOrigins: [opaque ? "null" : origin],
912
+ strictSource: true
913
+ });
914
+ bridge = current;
915
+ const hello = { ...binding, protocolVersion: PROTOCOL_VERSION, sessionId, frameEpoch: epoch };
916
+ assertHello(hello);
917
+ current.on(names.capabilities, (value) => {
918
+ if (!identity || signal.aborted || !sameIdentity(identity, value)) return;
919
+ try {
920
+ state = { ...state, capabilities: validateCapabilities(value, binding) };
921
+ publish();
922
+ } catch (error) {
923
+ failed(error, epoch);
924
+ }
925
+ });
926
+ current.on(names.result, (value) => {
927
+ if (!identity || signal.aborted || !sameIdentity(identity, value)) return;
928
+ try {
929
+ receiveResult(value, identity);
930
+ } catch (error) {
931
+ failed(error, epoch);
932
+ }
933
+ });
934
+ current.on(names.resyncRequired, (value) => {
935
+ if (!identity || signal.aborted || !sameIdentity(identity, value)) return;
936
+ failed(
937
+ new ProtocolError(
938
+ "RESYNC",
939
+ "Runtime requested a complete snapshot; reconnect to recover"
940
+ ),
941
+ epoch
942
+ );
943
+ });
944
+ current.on(names.asset, async (value) => {
945
+ assertImageResourceRequest(value);
946
+ if (!identity || signal.aborted || !sameIdentity(identity, value))
947
+ throw new ProtocolError("STALE", "Stale image resource identity");
948
+ assertFrameOrigin();
949
+ const frameAddress = options.iframe.src;
950
+ if (!state.capabilities?.fields.some(
951
+ (field) => field.resource === "image" && field.documentId === value.documentId && fingerprint(field.path) === fingerprint(value.path)
952
+ ))
953
+ throw new ProtocolError("UNSUPPORTED", "Unregistered image resource field");
954
+ if (!options.resolveImage)
955
+ throw new ProtocolError("UNSUPPORTED", "Image resource reader is unavailable");
956
+ const key = fingerprint([value.documentId, value.path]);
957
+ const refresh = value.resourceSequence === void 0 ? void 0 : refreshTargets.get(key);
958
+ if (value.resourceSequence !== void 0 && (!refresh || refresh.sequence !== value.resourceSequence || (refresh.contentHash === null ? value.expectedContentHash !== void 0 : refresh.contentHash !== value.expectedContentHash) || value.draftRevision < refresh.draftRevision || value.draftRevision > state.draftRevision))
959
+ return { ...value, status: "stale" };
960
+ const version = documentHistory.get(value.draftRevision);
961
+ if (!version && !refresh)
962
+ throw new ProtocolError("STALE", "Expired image resource revision");
963
+ const raw = version ? readPath(version[value.documentId], value.path) : refresh.raw;
964
+ if (raw !== void 0 && raw !== value.value)
965
+ throw new ProtocolError("INVALID", "Image resource value does not match its parameter");
966
+ const stale = () => fingerprint(readPath(state.documents[value.documentId], value.path)) !== fingerprint(raw) || refresh !== void 0 && refreshTargets.get(key) !== refresh;
967
+ if (stale()) return { ...value, status: "stale" };
968
+ if (imageRequests.size >= 64)
969
+ throw new ProtocolError("LIMIT", "Too many pending image resource reads");
970
+ const read = new AbortController();
971
+ const cancel = () => read.abort(signal.reason);
972
+ signal.addEventListener("abort", cancel, { once: true });
973
+ imageRequests.set(read, { request: clone(value), raw });
974
+ const timer = setTimeout(
975
+ () => read.abort(new ResourceLoadError("Image resource read timed out")),
976
+ 15e3
977
+ );
978
+ let release;
979
+ let offAbort = () => {
980
+ };
981
+ try {
982
+ release = await imageSlot(read.signal);
983
+ read.signal.throwIfAborted();
984
+ const cancelled = new Promise((_, reject) => {
985
+ const onAbort = () => reject(read.signal.reason);
986
+ read.signal.addEventListener("abort", onAbort, { once: true });
987
+ offAbort = () => read.signal.removeEventListener("abort", onAbort);
988
+ });
989
+ const data = await Promise.race([
990
+ options.resolveImage(
991
+ { documentId: value.documentId, path: clone(value.path), value: value.value },
992
+ read.signal
993
+ ),
994
+ cancelled
995
+ ]);
996
+ read.signal.throwIfAborted();
997
+ assertFrameOrigin();
998
+ if (options.iframe.src !== frameAddress)
999
+ throw new ProtocolError("ORIGIN", "The preview address changed during an image read");
1000
+ if (!identity || !sameIdentity(identity, value) || stale())
1001
+ return { ...value, status: "stale" };
1002
+ assertImageResourceData(data);
1003
+ if (value.expectedContentHash !== void 0 && data.contentHash !== value.expectedContentHash)
1004
+ throw new ResourceLoadError("Image resource content changed during refresh");
1005
+ return { ...value, ...data, status: "ready" };
1006
+ } catch (error) {
1007
+ if (error instanceof SupersededResourceError || stale())
1008
+ return { ...value, status: "stale" };
1009
+ if (signal.aborted || !identity || !sameIdentity(identity, value))
1010
+ throw new ProtocolError("STALE", "Image resource session expired");
1011
+ if (error instanceof ProtocolError) throw error;
1012
+ return {
1013
+ ...value,
1014
+ status: "failed",
1015
+ error: error instanceof Error ? error.message : "Image resource read failed"
1016
+ };
1017
+ } finally {
1018
+ clearTimeout(timer);
1019
+ offAbort();
1020
+ signal.removeEventListener("abort", cancel);
1021
+ imageRequests.delete(read);
1022
+ release?.();
1023
+ }
1024
+ });
1025
+ try {
1026
+ let cap;
1027
+ const deadline = Date.now() + 1e4;
1028
+ while (!cap) {
1029
+ signal.throwIfAborted();
1030
+ try {
1031
+ cap = validateCapabilities(
1032
+ await current.emit(names.hello, hello, { timeout: 800, signal }),
1033
+ binding
1034
+ );
1035
+ } catch (error) {
1036
+ if (signal.aborted || Date.now() >= deadline || error instanceof ProtocolError)
1037
+ throw error;
1038
+ }
1039
+ }
1040
+ if (cap.sessionId !== sessionId || cap.frameEpoch !== epoch)
1041
+ throw new ProtocolError("STALE", "Handshake returned a different session");
1042
+ signal.throwIfAborted();
1043
+ identity = { ...hello, runtimeInstanceId: cap.runtimeInstanceId };
1044
+ state = { ...state, capabilities: cap };
1045
+ publish();
1046
+ const draftRevision = state.draftRevision;
1047
+ const request = { ...identity, draftRevision, documents: clone(state.documents) };
1048
+ receipt(
1049
+ await current.emit(names.snapshot, request, { timeout: 5e3, signal }),
1050
+ draftRevision
1051
+ );
1052
+ signal.throwIfAborted();
1053
+ state = { ...state, connection: "ready" };
1054
+ publish();
1055
+ const result = await current.emit(names.status, identity, { timeout: 2e3, signal });
1056
+ signal.throwIfAborted();
1057
+ receiveResult(result, identity);
1058
+ return state.capabilities;
1059
+ } catch (error) {
1060
+ failed(error, epoch);
1061
+ throw error;
1062
+ }
1063
+ },
1064
+ patch(operations, phase = "preview") {
1065
+ if (!identity || !bridge || disposed || state.connection !== "ready")
1066
+ return Promise.reject(new ProtocolError("SESSION", "Preview is not connected"));
1067
+ const epoch = frameEpoch, signal = abort.signal, current = bridge;
1068
+ const request = {
1069
+ ...identity,
1070
+ baseDraftRevision: state.draftRevision,
1071
+ draftRevision: state.draftRevision + 1,
1072
+ operations: clone(operations),
1073
+ phase
1074
+ };
1075
+ assertPatch(request);
1076
+ state = {
1077
+ ...state,
1078
+ draftRevision: request.draftRevision,
1079
+ documents: freeze(applyOperations(state.documents, request.operations))
1080
+ };
1081
+ for (const [key, refresh] of refreshTargets) {
1082
+ const field = state.capabilities?.fields.find(
1083
+ (field2) => fingerprint([field2.documentId, field2.path]) === key
1084
+ );
1085
+ if (field && fingerprint(readPath(state.documents[field.documentId], field.path)) !== fingerprint(refresh.raw))
1086
+ refreshTargets.delete(key);
1087
+ }
1088
+ documentHistory.set(request.draftRevision, state.documents);
1089
+ if (documentHistory.size > 64) documentHistory.delete(documentHistory.keys().next().value);
1090
+ for (const [controller, image] of imageRequests)
1091
+ if (fingerprint(readPath(state.documents[image.request.documentId], image.request.path)) !== fingerprint(image.raw))
1092
+ controller.abort(new SupersededResourceError());
1093
+ publish();
1094
+ const work = serial.then(async () => {
1095
+ signal.throwIfAborted();
1096
+ if (state.connection === "error")
1097
+ throw new ProtocolError("RECOVERY", "Reconnect to replay the retained draft");
1098
+ return receipt(
1099
+ await current.emit(names.patch, request, { timeout: 3e3, signal }),
1100
+ request.draftRevision
1101
+ );
1102
+ }).catch((error) => {
1103
+ failed(error, epoch);
1104
+ throw error;
1105
+ });
1106
+ serial = work.catch(() => {
1107
+ });
1108
+ return work;
1109
+ },
1110
+ refreshImages(images) {
1111
+ if (!identity || !bridge || disposed || state.connection !== "ready")
1112
+ return Promise.reject(new ProtocolError("SESSION", "Preview is not connected"));
1113
+ if (state.capabilities?.canRefreshImages !== true || state.capabilities.stage !== "game-ready")
1114
+ return Promise.reject(
1115
+ new ProtocolError("UNSUPPORTED", "This preview cannot refresh images")
1116
+ );
1117
+ const request = {
1118
+ ...identity,
1119
+ draftRevision: state.draftRevision,
1120
+ resourceSequence: resourceSequence + 1,
1121
+ images: clone(images)
1122
+ };
1123
+ assertImageRefresh(request);
1124
+ for (const image of request.images) {
1125
+ if (image.contentHash === null && state.capabilities.canReportMissingImages !== true)
1126
+ return Promise.reject(
1127
+ new ProtocolError("UNSUPPORTED", "This preview cannot report missing images")
1128
+ );
1129
+ if (!state.capabilities.fields.some(
1130
+ (field) => field.resource === "image" && field.mode === "live" && field.documentId === image.documentId && fingerprint(field.path) === fingerprint(image.path)
1131
+ ))
1132
+ return Promise.reject(new ProtocolError("UNSUPPORTED", "Unregistered live image field"));
1133
+ if (readPath(state.documents[image.documentId], image.path) === "")
1134
+ return Promise.reject(
1135
+ new ProtocolError("UNSUPPORTED", "Image refresh requires a nonempty image")
1136
+ );
1137
+ }
1138
+ resourceSequence = request.resourceSequence;
1139
+ for (const image of request.images) {
1140
+ const key = fingerprint([image.documentId, image.path]);
1141
+ refreshTargets.set(key, {
1142
+ sequence: resourceSequence,
1143
+ contentHash: image.contentHash,
1144
+ draftRevision: request.draftRevision,
1145
+ raw: clone(readPath(state.documents[image.documentId], image.path))
1146
+ });
1147
+ for (const [controller, pending] of imageRequests)
1148
+ if (fingerprint([pending.request.documentId, pending.request.path]) === key)
1149
+ controller.abort(new SupersededResourceError());
1150
+ }
1151
+ const epoch = frameEpoch, signal = abort.signal, current = bridge;
1152
+ const work = serial.then(async () => {
1153
+ signal.throwIfAborted();
1154
+ if (state.connection === "error")
1155
+ throw new ProtocolError("RECOVERY", "Reconnect to recover the preview");
1156
+ const value = await current.emit(names.refreshImages, request, { timeout: 3e3, signal });
1157
+ receipt(value, request.draftRevision);
1158
+ const response = value;
1159
+ if (response.resourceSequence !== request.resourceSequence)
1160
+ throw new ProtocolError("INVALID", "Invalid image refresh receipt");
1161
+ return response;
1162
+ }).catch((error) => {
1163
+ if (!(error instanceof ProtocolError && ["STALE", "UNSUPPORTED"].includes(error.code)))
1164
+ failed(error, epoch);
1165
+ throw error;
1166
+ });
1167
+ serial = work.catch(() => {
1168
+ });
1169
+ return work;
1170
+ },
1171
+ dispose(options2 = {}) {
1172
+ if (disposed) return;
1173
+ disposed = true;
1174
+ if (options2.notifyRuntime !== false && bridge && identity)
1175
+ void bridge.emit(names.dispose, identity, { timeout: 1e3 }).catch(() => {
1176
+ });
1177
+ abort.abort();
1178
+ bridge?.destroy();
1179
+ bridge = void 0;
1180
+ state = { ...state, connection: "disposed" };
1181
+ publish();
1182
+ }
1183
+ };
1184
+ return host;
740
1185
  }
741
1186
 
742
1187
  // ../preview-runtime/src/runtime.ts
@@ -1651,8 +2096,448 @@ function connectPreviewRuntime(options) {
1651
2096
  }
1652
2097
  };
1653
2098
  }
2099
+
2100
+ // ../preview-runtime/src/level-editor.ts
2101
+ var frameResetPrefix = "level_editor_frame_reset_required:";
2102
+ var LevelEditorFrameResetError = class extends Error {
2103
+ constructor(reason, cause) {
2104
+ super(
2105
+ `${frameResetPrefix}${reason}${cause instanceof Error ? `:${cause.message.slice(0, 512)}` : ""}`
2106
+ );
2107
+ this.reason = reason;
2108
+ this.name = "LevelEditorFrameResetError";
2109
+ }
2110
+ reason;
2111
+ code = "LEVEL_EDITOR_FRAME_RESET_REQUIRED";
2112
+ requiresNewFrame = true;
2113
+ };
2114
+ function isLevelEditorFrameResetError(value) {
2115
+ if (typeof value === "string") return value.startsWith(frameResetPrefix);
2116
+ return !!value && typeof value === "object" && "message" in value && typeof value.message === "string" && value.message.startsWith(frameResetPrefix);
2117
+ }
2118
+ function boundedInteger(value, fallback, maximum) {
2119
+ const result = value ?? fallback;
2120
+ if (!Number.isSafeInteger(result) || result < 1 || result > maximum)
2121
+ throw new Error("level_editor_invalid_deadline");
2122
+ return result;
2123
+ }
2124
+ var bindingKeys = [
2125
+ "workspaceId",
2126
+ "directoryName",
2127
+ "editorId",
2128
+ "dataContractVersion",
2129
+ "executionContractHash"
2130
+ ];
2131
+ var identityKeys2 = [
2132
+ ...bindingKeys,
2133
+ "protocolVersion",
2134
+ "hostSessionId",
2135
+ "frameEpoch",
2136
+ "runtimeInstanceId"
2137
+ ];
2138
+ var object = (v) => {
2139
+ if (!v || typeof v !== "object" || Array.isArray(v))
2140
+ throw new Error("level_editor_invalid_message");
2141
+ return v;
2142
+ };
2143
+ function assertBinding2(value) {
2144
+ const v = object(value);
2145
+ if (["workspaceId", "directoryName", "editorId"].some(
2146
+ (k) => typeof v[k] !== "string" || !v[k].length
2147
+ ) || !Number.isSafeInteger(v.dataContractVersion) || Number(v.dataContractVersion) < 1 || typeof v.executionContractHash !== "string" || !/^[a-f0-9]{64}$/.test(v.executionContractHash))
2148
+ throw new Error("level_editor_invalid_binding");
2149
+ }
2150
+ function match(value, expected, keys) {
2151
+ const v = object(value);
2152
+ if (keys.some((k) => v[k] !== expected[k])) throw new Error("level_editor_stale_runtime");
2153
+ return v;
2154
+ }
2155
+ function cloneDocuments(value) {
2156
+ const v = object(value);
2157
+ const walk = (item, depth = 0) => {
2158
+ if (depth > 64) throw new Error("level_editor_snapshot_limit");
2159
+ if (item === null || typeof item === "string" || typeof item === "boolean") return;
2160
+ if (typeof item === "number" && Number.isFinite(item)) return;
2161
+ if (!item || typeof item !== "object") throw new Error("level_editor_invalid_snapshot");
2162
+ if (Array.isArray(item)) {
2163
+ for (const child of item) walk(child, depth + 1);
2164
+ } else
2165
+ for (const [key, child] of Object.entries(item)) {
2166
+ if (["__proto__", "constructor", "prototype"].includes(key))
2167
+ throw new Error("level_editor_unsafe_key");
2168
+ walk(child, depth + 1);
2169
+ }
2170
+ };
2171
+ walk(v);
2172
+ if (JSON.stringify(v).length > 16 * 1024 * 1024) throw new Error("level_editor_snapshot_limit");
2173
+ return structuredClone(v);
2174
+ }
2175
+ function createLevelEditorRuntime(options) {
2176
+ assertBinding2(options.binding);
2177
+ if (!options.commands.length || options.commands.length > 256 || new Set(options.commands).size !== options.commands.length || options.commands.some((k) => typeof k !== "string" || !k || k.length > 128))
2178
+ throw new Error("level_editor_invalid_capabilities");
2179
+ const binding = structuredClone(options.binding);
2180
+ const lifecycle = {
2181
+ version: 1,
2182
+ canCancel: true,
2183
+ requiresNewFrameOnFailure: true,
2184
+ applicationTimeoutMs: boundedInteger(options.applicationTimeoutMs, 25e3, 25e3),
2185
+ disposeTimeoutMs: boundedInteger(options.disposeTimeoutMs, 2e3, 5e3),
2186
+ maxPendingSnapshots: boundedInteger(options.maxPendingSnapshots, 8, 64)
2187
+ };
2188
+ let identity;
2189
+ const retiredHosts = /* @__PURE__ */ new Set();
2190
+ let revision2 = -1, documentsKey = "", disposed = false;
2191
+ let queue = Promise.resolve();
2192
+ let abort = new AbortController();
2193
+ let retired;
2194
+ let cleanup;
2195
+ const pending = /* @__PURE__ */ new Set();
2196
+ const assertAlive = () => {
2197
+ if (retired) throw retired;
2198
+ if (disposed) throw new Error("level_editor_disposed");
2199
+ };
2200
+ function finish(job, error, result) {
2201
+ if (!pending.delete(job)) return;
2202
+ clearTimeout(job.timer);
2203
+ if (error !== void 0) job.reject(error);
2204
+ else job.resolve(result);
2205
+ }
2206
+ function startCleanup(reason) {
2207
+ if (cleanup) return cleanup;
2208
+ const controller = new AbortController();
2209
+ cleanup = new Promise((resolve, reject) => {
2210
+ const timer = setTimeout(() => {
2211
+ const error = new LevelEditorFrameResetError("dispose_timeout");
2212
+ controller.abort(error);
2213
+ reject(error);
2214
+ }, lifecycle.disposeTimeoutMs);
2215
+ Promise.resolve().then(() => options.adapter.dispose({ signal: controller.signal, reason })).then(
2216
+ () => {
2217
+ clearTimeout(timer);
2218
+ resolve();
2219
+ },
2220
+ (error) => {
2221
+ clearTimeout(timer);
2222
+ const failure = new LevelEditorFrameResetError("dispose_failed", error);
2223
+ controller.abort(failure);
2224
+ reject(failure);
2225
+ }
2226
+ );
2227
+ });
2228
+ void cleanup.catch(() => {
2229
+ });
2230
+ return cleanup;
2231
+ }
2232
+ function retire(reason, cause) {
2233
+ if (retired) return retired;
2234
+ retired = new LevelEditorFrameResetError(reason, cause);
2235
+ abort.abort(retired);
2236
+ for (const job of pending) finish(job, retired);
2237
+ void startCleanup(reason);
2238
+ return retired;
2239
+ }
2240
+ return {
2241
+ hello(value) {
2242
+ assertAlive();
2243
+ const v = match(value, binding, bindingKeys);
2244
+ if (v.protocolVersion !== 1 || typeof v.hostSessionId !== "string" || !v.hostSessionId || !Number.isSafeInteger(v.frameEpoch) || Number(v.frameEpoch) < 0)
2245
+ throw new Error("level_editor_invalid_hello");
2246
+ if (retiredHosts.has(v.hostSessionId) || identity && identity.hostSessionId === v.hostSessionId && Number(v.frameEpoch) < identity.frameEpoch)
2247
+ throw new Error("level_editor_stale_runtime");
2248
+ if (identity && (identity.hostSessionId !== v.hostSessionId || identity.frameEpoch !== v.frameEpoch)) {
2249
+ if (pending.size) throw retire("host_replaced_during_application");
2250
+ if (identity.hostSessionId !== v.hostSessionId) retiredHosts.add(identity.hostSessionId);
2251
+ abort.abort();
2252
+ abort = new AbortController();
2253
+ revision2 = -1;
2254
+ documentsKey = "";
2255
+ identity = void 0;
2256
+ }
2257
+ identity ??= {
2258
+ ...binding,
2259
+ protocolVersion: 1,
2260
+ hostSessionId: v.hostSessionId,
2261
+ frameEpoch: Number(v.frameEpoch),
2262
+ runtimeInstanceId: crypto.randomUUID()
2263
+ };
2264
+ return { ...identity, commands: [...options.commands], lifecycle: { ...lifecycle } };
2265
+ },
2266
+ snapshot(value) {
2267
+ if (retired) return Promise.reject(retired);
2268
+ if (!identity || disposed) return Promise.reject(new Error("level_editor_not_connected"));
2269
+ const v = match(value, identity, identityKeys2);
2270
+ if (!Number.isSafeInteger(v.draftRevision) || Number(v.draftRevision) < 0 || !["edit", "play"].includes(String(v.mode)))
2271
+ return Promise.reject(new Error("level_editor_invalid_snapshot"));
2272
+ const documents = cloneDocuments(v.documents), selection = object(v.selection);
2273
+ if (Object.entries(selection).some(
2274
+ ([key2, item]) => ["__proto__", "constructor", "prototype"].includes(key2) || !(typeof item === "string" || typeof item === "number" && Number.isFinite(item))
2275
+ ))
2276
+ return Promise.reject(new Error("level_editor_invalid_selection"));
2277
+ const incoming = Number(v.draftRevision), key = JSON.stringify(documents);
2278
+ const requestIdentity = { ...identity };
2279
+ const context = {
2280
+ documents,
2281
+ selection: structuredClone(selection),
2282
+ draftRevision: incoming,
2283
+ mode: v.mode,
2284
+ signal: abort.signal
2285
+ };
2286
+ if (pending.size >= lifecycle.maxPendingSnapshots)
2287
+ return Promise.reject(retire("pending_limit"));
2288
+ return new Promise((resolve, reject) => {
2289
+ const job = {
2290
+ revision: incoming,
2291
+ resolve,
2292
+ reject,
2293
+ timer: setTimeout(() => retire("application_timeout"), lifecycle.applicationTimeoutMs)
2294
+ };
2295
+ pending.add(job);
2296
+ queue = queue.catch(() => {
2297
+ }).then(async () => {
2298
+ if (!pending.has(job)) return;
2299
+ try {
2300
+ assertAlive();
2301
+ if (incoming < revision2 || incoming === revision2 && key !== documentsKey) {
2302
+ finish(job, new Error("level_editor_stale_snapshot"));
2303
+ return;
2304
+ }
2305
+ if (context.mode === "edit") await options.adapter.enterEdit(context);
2306
+ else await options.adapter.loadLevel(context);
2307
+ assertAlive();
2308
+ context.signal.throwIfAborted();
2309
+ await options.adapter.afterRender(context);
2310
+ assertAlive();
2311
+ context.signal.throwIfAborted();
2312
+ match(identity, requestIdentity, identityKeys2);
2313
+ revision2 = incoming;
2314
+ documentsKey = key;
2315
+ finish(job, void 0, {
2316
+ ...requestIdentity,
2317
+ applied: true,
2318
+ draftRevision: incoming,
2319
+ mode: context.mode
2320
+ });
2321
+ } catch (error) {
2322
+ retire("adapter_failed", error);
2323
+ }
2324
+ });
2325
+ });
2326
+ },
2327
+ cancel(value) {
2328
+ assertAlive();
2329
+ if (!identity) throw new Error("level_editor_not_connected");
2330
+ const v = match(value, identity, identityKeys2);
2331
+ if (!Number.isSafeInteger(v.draftRevision) || Number(v.draftRevision) < 0)
2332
+ throw new Error("level_editor_invalid_snapshot");
2333
+ const cancelled = [...pending].some((job) => job.revision === v.draftRevision);
2334
+ if (cancelled) retire("application_cancelled");
2335
+ return { ...identity, cancelled, requiresNewFrame: cancelled };
2336
+ },
2337
+ dispose(value) {
2338
+ if (value !== void 0) {
2339
+ if (!identity) throw new Error("level_editor_not_connected");
2340
+ match(value, identity, identityKeys2);
2341
+ }
2342
+ disposed = true;
2343
+ retire("disposed");
2344
+ return startCleanup("disposed");
2345
+ }
2346
+ };
2347
+ }
2348
+ function readLevelEditorPreviewContext() {
2349
+ const profiles = document.querySelectorAll(
2350
+ 'script#playcraft-level-editor-profile[type="application/json"]'
2351
+ );
2352
+ if (profiles.length !== 1) throw new Error("level_editor_build_profile_missing");
2353
+ const profile = object(JSON.parse(profiles[0].textContent ?? ""));
2354
+ if (profile.protocolVersion !== 1) throw new Error("level_editor_build_profile_invalid");
2355
+ assertBinding2(profile.binding);
2356
+ const mark = "playcraft-preview-storage:";
2357
+ if (!window.name.startsWith(mark)) throw new Error("level_editor_parent_missing");
2358
+ const context = object(
2359
+ object(JSON.parse(window.name.slice(mark.length))).__playcraft_level_editor_context__
2360
+ );
2361
+ const parentOrigin = String(context.parentOrigin);
2362
+ if (new URL(parentOrigin).origin !== parentOrigin || !/^https?:/.test(parentOrigin))
2363
+ throw new Error("level_editor_parent_invalid");
2364
+ return { binding: structuredClone(profile.binding), parentOrigin };
2365
+ }
2366
+ function connectLevelEditorRuntime(options) {
2367
+ if (new URL(options.parentOrigin).origin !== options.parentOrigin || !/^https?:/.test(options.parentOrigin))
2368
+ throw new Error("level_editor_parent_invalid");
2369
+ const runtime = createLevelEditorRuntime(options);
2370
+ const bridge = new Bridge({
2371
+ peer: () => window.parent,
2372
+ targetOrigin: options.parentOrigin,
2373
+ allowedOrigins: [options.parentOrigin],
2374
+ strictSource: true
2375
+ });
2376
+ const off = [
2377
+ bridge.on(BridgeMethods.levelEditor.hello, (value) => runtime.hello(value)),
2378
+ bridge.on(BridgeMethods.levelEditor.snapshot, (value) => runtime.snapshot(value)),
2379
+ bridge.on(BridgeMethods.levelEditor.cancel, (value) => runtime.cancel(value)),
2380
+ bridge.on(BridgeMethods.levelEditor.dispose, async (value) => {
2381
+ await runtime.dispose(value);
2382
+ return { disposed: true };
2383
+ })
2384
+ ];
2385
+ return {
2386
+ runtime,
2387
+ async dispose() {
2388
+ off.forEach((fn) => fn());
2389
+ bridge.destroy();
2390
+ await runtime.dispose();
2391
+ }
2392
+ };
2393
+ }
2394
+ function createLevelEditorHost(options) {
2395
+ assertBinding2(options.binding);
2396
+ const requestTimeoutMs = boundedInteger(options.requestTimeoutMs, 3e4, 3e4);
2397
+ const connectTimeoutMs = boundedInteger(options.connectTimeoutMs, 8e3, 8e3);
2398
+ const hello = {
2399
+ ...structuredClone(options.binding),
2400
+ protocolVersion: 1,
2401
+ hostSessionId: crypto.randomUUID(),
2402
+ frameEpoch: options.frameEpoch ?? 0
2403
+ };
2404
+ const bridge = new Bridge({
2405
+ peer: () => options.iframe.contentWindow,
2406
+ targetOrigin: options.opaque ? "*" : options.targetOrigin,
2407
+ allowedOrigins: options.opaque ? ["null"] : [options.targetOrigin],
2408
+ strictSource: true
2409
+ });
2410
+ let identity;
2411
+ let disposed = false;
2412
+ let retired;
2413
+ const connecting = /* @__PURE__ */ new Set();
2414
+ const pending = /* @__PURE__ */ new Map();
2415
+ function cancelRemote(revision2) {
2416
+ if (!identity?.lifecycle?.canCancel) return;
2417
+ const controller = new AbortController();
2418
+ void bridge.emit(
2419
+ BridgeMethods.levelEditor.cancel,
2420
+ { ...identity, draftRevision: revision2 },
2421
+ { timeout: 1e3, signal: controller.signal }
2422
+ ).catch(() => {
2423
+ });
2424
+ controller.abort();
2425
+ }
2426
+ function retire(reason, cause) {
2427
+ if (retired) return retired;
2428
+ retired = new LevelEditorFrameResetError(reason, cause);
2429
+ for (const controller of connecting) controller.abort(retired);
2430
+ for (const [controller, revision2] of pending) {
2431
+ cancelRemote(revision2);
2432
+ controller.abort(retired);
2433
+ }
2434
+ return retired;
2435
+ }
2436
+ return {
2437
+ async connect() {
2438
+ if (retired) throw retired;
2439
+ if (disposed) throw new Error("level_editor_host_disposed");
2440
+ const controller = new AbortController();
2441
+ connecting.add(controller);
2442
+ try {
2443
+ const value = object(
2444
+ await bridge.emit(BridgeMethods.levelEditor.hello, hello, {
2445
+ timeout: connectTimeoutMs,
2446
+ signal: controller.signal
2447
+ })
2448
+ );
2449
+ if (retired) throw retired;
2450
+ match(value, hello, [...bindingKeys, "protocolVersion", "hostSessionId", "frameEpoch"]);
2451
+ if (disposed || typeof value.runtimeInstanceId !== "string" || !value.runtimeInstanceId || !Array.isArray(value.commands) || !value.commands.length || value.commands.length > 256 || value.commands.some((k) => typeof k !== "string" || !k))
2452
+ throw new Error("level_editor_invalid_capabilities");
2453
+ if (value.lifecycle !== void 0) {
2454
+ const policy = object(value.lifecycle);
2455
+ if (policy.version !== 1 || policy.canCancel !== true || policy.requiresNewFrameOnFailure !== true)
2456
+ throw new Error("level_editor_invalid_capabilities");
2457
+ if (typeof policy.applicationTimeoutMs !== "number" || typeof policy.disposeTimeoutMs !== "number" || typeof policy.maxPendingSnapshots !== "number")
2458
+ throw new Error("level_editor_invalid_capabilities");
2459
+ boundedInteger(policy.applicationTimeoutMs, 25e3, 25e3);
2460
+ boundedInteger(policy.disposeTimeoutMs, 2e3, 5e3);
2461
+ boundedInteger(policy.maxPendingSnapshots, 8, 64);
2462
+ }
2463
+ identity = value;
2464
+ return identity;
2465
+ } catch (error) {
2466
+ if (retired) throw retired;
2467
+ if (isLevelEditorFrameResetError(error)) throw retire("runtime_retired", error);
2468
+ throw error;
2469
+ } finally {
2470
+ connecting.delete(controller);
2471
+ }
2472
+ },
2473
+ async snapshot(input, request = {}) {
2474
+ if (retired) throw retired;
2475
+ if (!identity || disposed) throw new Error("level_editor_not_connected");
2476
+ if (request.signal?.aborted) throw new Error("level_editor_request_aborted");
2477
+ const controller = new AbortController();
2478
+ pending.set(controller, input.draftRevision);
2479
+ const cancelled = () => retire("application_cancelled");
2480
+ request.signal?.addEventListener("abort", cancelled, { once: true });
2481
+ try {
2482
+ const value = object(
2483
+ await bridge.emit(
2484
+ BridgeMethods.levelEditor.snapshot,
2485
+ { ...identity, ...input },
2486
+ { timeout: requestTimeoutMs, signal: controller.signal }
2487
+ )
2488
+ );
2489
+ if (retired) throw retired;
2490
+ match(value, identity, identityKeys2);
2491
+ if (disposed || value.applied !== true || value.draftRevision !== input.draftRevision || value.mode !== input.mode)
2492
+ throw new Error("level_editor_application_mismatch");
2493
+ return value;
2494
+ } catch (error) {
2495
+ throw retire("application_failed", error);
2496
+ } finally {
2497
+ pending.delete(controller);
2498
+ request.signal?.removeEventListener("abort", cancelled);
2499
+ }
2500
+ },
2501
+ dispose() {
2502
+ if (disposed) return;
2503
+ if (pending.size) retire("host_disposed_during_application");
2504
+ disposed = true;
2505
+ for (const controller of connecting) controller.abort();
2506
+ bridge.destroy();
2507
+ }
2508
+ };
2509
+ }
1654
2510
  export {
2511
+ LevelEditorFrameResetError,
2512
+ MAX_IMAGE_RESOURCE_BYTES,
2513
+ PROTOCOL_VERSION,
2514
+ ProtocolError,
1655
2515
  ResourceLoadError,
2516
+ SupersededResourceError,
2517
+ applyOperations,
2518
+ assertBinding,
2519
+ assertDocuments,
2520
+ assertHello,
2521
+ assertIdentity,
2522
+ assertImageRefresh,
2523
+ assertImageResourceData,
2524
+ assertImageResourceRequest,
2525
+ assertJson,
2526
+ assertPatch,
2527
+ assertSnapshot,
2528
+ clone,
2529
+ connectLevelEditorRuntime,
1656
2530
  connectPreviewRuntime,
1657
- readWorkspacePreviewContext
2531
+ createLevelEditorHost,
2532
+ createLevelEditorRuntime,
2533
+ createParameterHost,
2534
+ createParameterRuntime,
2535
+ fingerprint,
2536
+ freeze,
2537
+ isLevelEditorFrameResetError,
2538
+ readLevelEditorPreviewContext,
2539
+ readPath,
2540
+ readWorkspacePreviewContext,
2541
+ revision,
2542
+ sameIdentity
1658
2543
  };