@playcraft/cli 0.1.55-beta → 0.1.56-beta

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 (25) hide show
  1. package/dist/commands/build.js +38 -0
  2. package/dist/workspace-runtime/previews/finalize.js +64 -0
  3. package/dist/workspace-runtime/previews/publish.js +31 -8
  4. package/dist/workspace-runtime/server/http.js +5 -5
  5. package/dist/workspace-runtime/workspaces/artifact-events.js +136 -23
  6. package/dist/workspace-runtime/workspaces/artifact-files.js +3 -1
  7. package/dist/workspace-runtime/workspaces/artifact-receipts.js +27 -4
  8. package/dist/workspace-runtime/workspaces/artifacts.js +115 -17
  9. package/dist/workspace-runtime/workspaces/asset-publication-access.js +5 -1
  10. package/dist/workspace-runtime/workspaces/delivery-build.js +97 -7
  11. package/dist/workspace-runtime/workspaces/directory-access.js +13 -0
  12. package/dist/workspace-runtime/workspaces/git.js +13 -0
  13. package/dist/workspace-runtime/workspaces/parameter-assets.js +38 -15
  14. package/dist/workspace-runtime/workspaces/parameters.js +61 -21
  15. package/dist/workspace-runtime/workspaces/theme-asset-index.js +123 -13
  16. package/package.json +3 -3
  17. package/project-templates/empty/AGENTS.md +1 -1
  18. package/templates/preview-runtime/v1/README.md +8 -0
  19. package/templates/preview-runtime/v1/manifest.json +7 -7
  20. package/templates/preview-runtime/v1/runtime.d.mts +1 -1
  21. package/templates/preview-runtime/v1/runtime.d.ts +1 -1
  22. package/templates/preview-runtime/v1/runtime.mjs +238 -47
  23. package/templates/preview-runtime/v1/types/adapters.d.ts +3 -1
  24. package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
  25. package/templates/preview-runtime/v1/types/protocol.d.ts +37 -0
@@ -197,6 +197,7 @@ var BridgeMethods = {
197
197
  restart: "runtime.parameters.restart",
198
198
  status: "runtime.parameters.status",
199
199
  dispose: "runtime.parameters.dispose",
200
+ asset: "runtime.parameters.asset",
200
201
  capabilities: "runtime.parameters.capabilities",
201
202
  result: "runtime.parameters.result",
202
203
  resyncRequired: "runtime.parameters.resync-required"
@@ -220,7 +221,8 @@ var BRIDGE_RPC_NAMES = [
220
221
  BridgeMethods.runtimeParameters.patch,
221
222
  BridgeMethods.runtimeParameters.restart,
222
223
  BridgeMethods.runtimeParameters.status,
223
- BridgeMethods.runtimeParameters.dispose
224
+ BridgeMethods.runtimeParameters.dispose,
225
+ BridgeMethods.runtimeParameters.asset
224
226
  ];
225
227
 
226
228
  // ../iframe-bridge/src/configs/perspective.ts
@@ -415,7 +417,7 @@ function safeReply(bridge, message) {
415
417
  try {
416
418
  bridge.postMessage(message);
417
419
  } catch (err) {
418
- if (err instanceof PlaycraftBridgeError && err.code === BridgeErrorCodes.PEER_UNAVAILABLE) {
420
+ if (err instanceof PlaycraftBridgeError && (err.code === BridgeErrorCodes.PEER_UNAVAILABLE || err.code === BridgeErrorCodes.DESTROYED)) {
419
421
  return;
420
422
  }
421
423
  throw err;
@@ -524,6 +526,19 @@ var Bridge = class {
524
526
  };
525
527
 
526
528
  // ../preview-runtime/src/protocol.ts
529
+ var MAX_IMAGE_RESOURCE_BYTES = 32 * 1024 * 1024;
530
+ var ResourceLoadError = class extends Error {
531
+ constructor(message) {
532
+ super(message);
533
+ this.name = "ResourceLoadError";
534
+ }
535
+ };
536
+ var SupersededResourceError = class extends Error {
537
+ constructor() {
538
+ super("A newer image value replaced this resource request");
539
+ this.name = "SupersededResourceError";
540
+ }
541
+ };
527
542
  var ProtocolError = class extends Error {
528
543
  constructor(code, message) {
529
544
  super(message);
@@ -622,6 +637,22 @@ var identityKeys = [
622
637
  "configContractVersion",
623
638
  "locale"
624
639
  ];
640
+ function assertImageResourceData(value) {
641
+ if (!record(value) || typeof value.mimeType !== "string" || typeof value.dataUrl !== "string")
642
+ throw new ProtocolError("INVALID", "Invalid image resource response");
643
+ const allowed = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif", "image/bmp"];
644
+ if (!allowed.includes(value.mimeType))
645
+ throw new ProtocolError("INVALID", "Unsupported image resource type");
646
+ const prefix = `data:${value.mimeType};base64,`;
647
+ const data = value.dataUrl.startsWith(prefix) ? value.dataUrl.slice(prefix.length) : "";
648
+ if (!data || data.length % 4 !== 0 || data.length > Math.ceil(MAX_IMAGE_RESOURCE_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
649
+ throw new ProtocolError("LIMIT", "Invalid or oversized image resource data");
650
+ const bytes = data.length / 4 * 3 - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
651
+ if (bytes > MAX_IMAGE_RESOURCE_BYTES)
652
+ throw new ProtocolError("LIMIT", "Image resource exceeds the byte limit");
653
+ if (value.contentHash !== void 0 && !/^[a-f0-9]{64}$/.test(String(value.contentHash)))
654
+ throw new ProtocolError("INVALID", "Invalid image content fingerprint");
655
+ }
625
656
  function assertSnapshot(value) {
626
657
  assertIdentity(value);
627
658
  exactKeys(value, [
@@ -699,6 +730,8 @@ function createParameterRuntime(options) {
699
730
  let error;
700
731
  let disposed = false;
701
732
  let cleanupFailure;
733
+ let recoverable = false;
734
+ const resourceErrors = /* @__PURE__ */ new Map();
702
735
  let resultSequence = 0;
703
736
  const retiredSessions = /* @__PURE__ */ new Set();
704
737
  const groups = /* @__PURE__ */ new Map();
@@ -739,7 +772,9 @@ function createParameterRuntime(options) {
739
772
  appliedRevision,
740
773
  status,
741
774
  groups: clone([...groups.values()]),
742
- ...error ? { error } : {}
775
+ ...error ? { error } : {},
776
+ ...recoverable ? { recoverable: true } : {},
777
+ ...resourceErrors.size ? { resourceErrors: clone([...resourceErrors.values()]) } : {}
743
778
  });
744
779
  function publish() {
745
780
  if (!identity || disposed) return;
@@ -748,16 +783,59 @@ function createParameterRuntime(options) {
748
783
  listeners.forEach((fn) => fn(value));
749
784
  }
750
785
  function context(config, beforeConfig, draftRevision, fields, signal) {
786
+ const images = /* @__PURE__ */ new Map();
787
+ const assertCurrent = () => {
788
+ signal.throwIfAborted();
789
+ if (signal !== controller.signal || disposed)
790
+ throw new ProtocolError("STALE", "Application task expired");
791
+ if (images.size && target) {
792
+ const latest = options.resolve(target);
793
+ for (const { field, value } of images.values())
794
+ if (readPath(latest, field.effectivePath) !== value) throw new SupersededResourceError();
795
+ }
796
+ };
751
797
  return {
752
798
  config,
753
799
  beforeConfig,
754
800
  draftRevision,
755
801
  fields,
756
802
  signal,
757
- assertCurrent() {
758
- signal.throwIfAborted();
759
- if (signal !== controller.signal || disposed)
760
- throw new ProtocolError("STALE", "Application task expired");
803
+ assertCurrent,
804
+ async loadImage(field, value) {
805
+ const registered = options.fields.find(
806
+ (item) => item.resource === "image" && item.documentId === field.documentId && fingerprint(item.path) === fingerprint(field.path)
807
+ );
808
+ if (!registered || typeof value !== "string" || !value)
809
+ throw new ProtocolError("UNSUPPORTED", "Unregistered image resource field");
810
+ if (!options.loadImage)
811
+ throw new ProtocolError("UNSUPPORTED", "Image resource transport is unavailable");
812
+ const key = fingerprint([registered.documentId, registered.path]);
813
+ images.set(key, { field: registered, value });
814
+ assertCurrent();
815
+ try {
816
+ const image = await options.loadImage(
817
+ {
818
+ ...requireIdentity(),
819
+ draftRevision,
820
+ documentId: registered.documentId,
821
+ path: clone(registered.path),
822
+ value
823
+ },
824
+ signal
825
+ );
826
+ assertCurrent();
827
+ resourceErrors.delete(key);
828
+ return image;
829
+ } catch (failure) {
830
+ assertCurrent();
831
+ if (failure instanceof ResourceLoadError)
832
+ resourceErrors.set(key, {
833
+ documentId: registered.documentId,
834
+ path: clone(registered.path),
835
+ error: failure.message
836
+ });
837
+ throw failure;
838
+ }
761
839
  }
762
840
  };
763
841
  }
@@ -769,10 +847,16 @@ function createParameterRuntime(options) {
769
847
  await work(signal);
770
848
  } catch (e) {
771
849
  if (signal.aborted) return;
850
+ if (e instanceof SupersededResourceError) return;
772
851
  status = "error";
773
- installedRevision = appliedRevision = -1;
852
+ recoverable = e instanceof ResourceLoadError;
853
+ if (!recoverable) installedRevision = appliedRevision = -1;
774
854
  error = e instanceof Error ? e.message : String(e);
775
- for (const group of groups.values()) group.status = "unknown";
855
+ for (const group of groups.values())
856
+ if (!recoverable || options.fields.some(
857
+ (field) => field.groupId === group.groupId && resourceErrors.has(fingerprint([field.documentId, field.path]))
858
+ ))
859
+ group.status = "unknown";
776
860
  publish();
777
861
  }
778
862
  });
@@ -795,12 +879,26 @@ function createParameterRuntime(options) {
795
879
  return receipt;
796
880
  }
797
881
  function updateState(rev, changed) {
882
+ const previousGroups = new Map(groups);
798
883
  for (const field of changed)
799
884
  groups.set(field.groupId, {
800
885
  groupId: field.groupId,
801
886
  appliedRevision: rev,
802
887
  status: "applied"
803
888
  });
889
+ if (resourceErrors.size) {
890
+ status = "error";
891
+ recoverable = true;
892
+ if (changed.length) installedRevision = appliedRevision = -1;
893
+ error = [...resourceErrors.values()].map((item) => item.error).join("; ");
894
+ for (const field of options.fields)
895
+ if (resourceErrors.has(fingerprint([field.documentId, field.path])))
896
+ groups.set(field.groupId, { ...previousGroups.get(field.groupId), status: "unknown" });
897
+ publish();
898
+ return;
899
+ }
900
+ recoverable = false;
901
+ error = void 0;
804
902
  let pending = false, reload = false;
805
903
  for (const field of options.fields) {
806
904
  if (fingerprint(readPath(installed?.[field.documentId], field.path)) !== fingerprint(readPath(target?.[field.documentId], field.path))) {
@@ -817,13 +915,15 @@ function createParameterRuntime(options) {
817
915
  } else status = reload ? "requires-reload" : pending ? "pending-commit" : "applying";
818
916
  publish();
819
917
  }
820
- async function applyTarget(documents, rev, commit, signal) {
821
- if (status === "error") return;
918
+ async function applyTarget(documents, rev, commit, signal, retryResources = /* @__PURE__ */ new Set()) {
919
+ if (status === "error" && !recoverable) return;
822
920
  if (!adapter || !installed)
823
921
  throw new ProtocolError("STATE", "Initial snapshot must create the game first");
824
922
  const operations = [];
825
923
  for (const field of options.fields) {
826
924
  if (field.mode !== "live" && !(commit && field.mode === "restart-scene")) continue;
925
+ const resourceKey = fingerprint([field.documentId, field.path]);
926
+ if (resourceErrors.has(resourceKey) && !retryResources.has(resourceKey)) continue;
827
927
  const value = readPath(documents[field.documentId], field.path);
828
928
  operations.push(
829
929
  value === void 0 ? { op: "remove", documentId: field.documentId, path: field.path } : {
@@ -838,13 +938,14 @@ function createParameterRuntime(options) {
838
938
  const before = options.source.get();
839
939
  const config = freeze(options.resolve(freeze(clone(candidate))));
840
940
  const changed = options.fields.filter(
841
- (field) => fingerprint(readPath(before, field.effectivePath)) !== fingerprint(readPath(config, field.effectivePath))
941
+ (field) => resourceErrors.has(fingerprint([field.documentId, field.path])) && retryResources.has(fingerprint([field.documentId, field.path])) && (field.mode === "live" || commit && field.mode === "restart-scene") || fingerprint(readPath(before, field.effectivePath)) !== fingerprint(readPath(config, field.effectivePath))
842
942
  );
843
943
  const ctx = context(config, before, rev, changed, signal);
844
944
  ctx.assertCurrent();
845
945
  status = "applying";
846
946
  publish();
847
947
  options.source.install(config);
948
+ const previousInstalledRevision = installedRevision;
848
949
  installedRevision = fingerprint(candidate) === fingerprint(documents) ? rev : -1;
849
950
  try {
850
951
  if (changed.some((field) => field.mode === "restart-scene")) await adapter.restartScene(ctx);
@@ -852,10 +953,16 @@ function createParameterRuntime(options) {
852
953
  ctx.assertCurrent();
853
954
  await adapter.afterRender(ctx);
854
955
  ctx.assertCurrent();
956
+ for (const field of changed)
957
+ if (field.resource === "image" && readPath(config, field.effectivePath) === "")
958
+ resourceErrors.delete(fingerprint([field.documentId, field.path]));
855
959
  installed = candidate;
856
960
  updateState(rev, changed);
857
961
  } catch (e) {
858
- if (!signal.aborted) options.source.install(before);
962
+ if (!signal.aborted) {
963
+ options.source.install(before);
964
+ installedRevision = previousInstalledRevision;
965
+ }
859
966
  throw e;
860
967
  }
861
968
  }
@@ -888,6 +995,8 @@ function createParameterRuntime(options) {
888
995
  stage = "bootstrap-ready";
889
996
  status = "idle";
890
997
  error = void 0;
998
+ recoverable = false;
999
+ resourceErrors.clear();
891
1000
  resultSequence = 0;
892
1001
  for (const group of groups.values()) {
893
1002
  group.appliedRevision = -1;
@@ -915,44 +1024,56 @@ function createParameterRuntime(options) {
915
1024
  "RECOVERY",
916
1025
  "Previous game cleanup failed; reload the preview frame"
917
1026
  );
918
- const before = options.source.get();
919
- const config = freeze(options.resolve(documents));
920
- const ctx = context(config, before, rev, clone(options.fields), signal);
921
- ctx.assertCurrent();
922
1027
  status = "applying";
923
1028
  publish();
924
1029
  const previousAdapter = adapter;
925
1030
  adapter = void 0;
926
1031
  stage = "bootstrap-ready";
927
1032
  await release(previousAdapter);
928
- ctx.assertCurrent();
929
- options.source.install(config);
930
- installedRevision = rev;
931
- let created;
932
- try {
933
- created = await options.createGame(ctx);
934
- if (signal.aborted) {
935
- await release(created);
936
- return;
937
- }
938
- ctx.assertCurrent();
939
- adapter = created;
940
- stage = "game-ready";
941
- capabilityListeners.forEach((fn) => fn(capabilities()));
942
- await created.afterRender(ctx);
1033
+ let nextDocuments = documents;
1034
+ let nextRevision = rev;
1035
+ for (; ; ) {
1036
+ const before = options.source.get();
1037
+ resourceErrors.clear();
1038
+ recoverable = false;
1039
+ const config = freeze(options.resolve(nextDocuments));
1040
+ const ctx = context(config, before, nextRevision, clone(options.fields), signal);
943
1041
  ctx.assertCurrent();
944
- installed = clone(documents);
945
- error = void 0;
946
- updateState(rev, options.fields);
947
- } catch (e) {
948
- if (!signal.aborted) {
949
- adapter = void 0;
950
- stage = "bootstrap-ready";
951
- options.source.install(before);
1042
+ options.source.install(config);
1043
+ installedRevision = nextRevision;
1044
+ let created;
1045
+ try {
1046
+ created = await options.createGame(ctx);
1047
+ if (signal.aborted) {
1048
+ await release(created);
1049
+ return;
1050
+ }
1051
+ ctx.assertCurrent();
1052
+ adapter = created;
1053
+ stage = "game-ready";
952
1054
  capabilityListeners.forEach((fn) => fn(capabilities()));
1055
+ await created.afterRender(ctx);
1056
+ ctx.assertCurrent();
1057
+ installed = clone(nextDocuments);
1058
+ if (resourceErrors.size) installedRevision = -1;
1059
+ error = void 0;
1060
+ updateState(nextRevision, options.fields);
1061
+ return;
1062
+ } catch (e) {
1063
+ if (!signal.aborted) {
1064
+ adapter = void 0;
1065
+ stage = "bootstrap-ready";
1066
+ options.source.install(before);
1067
+ capabilityListeners.forEach((fn) => fn(capabilities()));
1068
+ }
1069
+ await release(created);
1070
+ if (e instanceof SupersededResourceError && !signal.aborted && target) {
1071
+ nextDocuments = freeze(clone(target));
1072
+ nextRevision = receivedRevision;
1073
+ continue;
1074
+ }
1075
+ throw e;
953
1076
  }
954
- await release(created);
955
- throw e;
956
1077
  }
957
1078
  });
958
1079
  return receipt;
@@ -964,7 +1085,7 @@ function createParameterRuntime(options) {
964
1085
  if (previous) return previous;
965
1086
  if (!target || value.baseDraftRevision !== receivedRevision)
966
1087
  throw new ProtocolError("RESYNC", "Patch base does not match received revision");
967
- if (status === "error")
1088
+ if (status === "error" && !recoverable)
968
1089
  throw new ProtocolError(
969
1090
  "RECOVERY",
970
1091
  "Application state is unknown; send a recovery snapshot"
@@ -979,12 +1100,15 @@ function createParameterRuntime(options) {
979
1100
  options.resolve(documents);
980
1101
  target = documents;
981
1102
  const receipt = accept("patch", value);
982
- enqueue((signal) => applyTarget(documents, rev, commit, signal));
1103
+ const retryResources = new Set(
1104
+ value.operations.length ? value.operations.map((operation) => fingerprint([operation.documentId, operation.path])) : options.fields.filter((field) => field.resource === "image").map((field) => fingerprint([field.documentId, field.path]))
1105
+ );
1106
+ enqueue((signal) => applyTarget(documents, rev, commit, signal, retryResources));
983
1107
  return receipt;
984
1108
  },
985
1109
  restart(value) {
986
1110
  check(value);
987
- if (status === "error")
1111
+ if (status === "error" && !recoverable)
988
1112
  throw new ProtocolError(
989
1113
  "RECOVERY",
990
1114
  "Application state is unknown; send a recovery snapshot"
@@ -993,7 +1117,17 @@ function createParameterRuntime(options) {
993
1117
  if (rev !== receivedRevision || !target)
994
1118
  throw new ProtocolError("STALE", "Restart must name the current target");
995
1119
  const documents = clone(target);
996
- enqueue((signal) => applyTarget(documents, rev, true, signal));
1120
+ enqueue(
1121
+ (signal) => applyTarget(
1122
+ documents,
1123
+ rev,
1124
+ true,
1125
+ signal,
1126
+ new Set(
1127
+ options.fields.filter((field) => field.resource === "image").map((field) => fingerprint([field.documentId, field.path]))
1128
+ )
1129
+ )
1130
+ );
997
1131
  return { accepted: true, receivedRevision };
998
1132
  },
999
1133
  status(value) {
@@ -1025,6 +1159,42 @@ function createParameterRuntime(options) {
1025
1159
  }
1026
1160
 
1027
1161
  // ../preview-runtime/src/preview.ts
1162
+ function decodeImage(dataUrl, signal) {
1163
+ return new Promise((resolve, reject) => {
1164
+ signal.throwIfAborted();
1165
+ const image = new Image();
1166
+ const timer = setTimeout(
1167
+ () => finish(new ResourceLoadError("Image decoding timed out")),
1168
+ 15e3
1169
+ );
1170
+ const cleanup = () => {
1171
+ clearTimeout(timer);
1172
+ image.onload = image.onerror = null;
1173
+ signal.removeEventListener("abort", cancel);
1174
+ };
1175
+ const finish = (error) => {
1176
+ if (!error && (!Number.isSafeInteger(image.naturalWidth) || !Number.isSafeInteger(image.naturalHeight) || image.naturalWidth <= 0 || image.naturalHeight <= 0 || image.naturalWidth * image.naturalHeight > 32 * 1024 * 1024))
1177
+ error = new ResourceLoadError("Image dimensions exceed the pixel limit");
1178
+ cleanup();
1179
+ if (error) {
1180
+ image.src = "";
1181
+ reject(error);
1182
+ } else resolve(image);
1183
+ };
1184
+ const cancel = () => finish(signal.reason);
1185
+ signal.addEventListener("abort", cancel, { once: true });
1186
+ image.onerror = () => finish(new ResourceLoadError("Image decoding failed"));
1187
+ image.onload = () => {
1188
+ if (typeof image.decode !== "function") finish();
1189
+ };
1190
+ image.src = dataUrl;
1191
+ if (typeof image.decode === "function")
1192
+ void image.decode().then(
1193
+ () => finish(),
1194
+ () => finish(new ResourceLoadError("Image decoding failed"))
1195
+ );
1196
+ });
1197
+ }
1028
1198
  function readWorkspacePreviewContext(expectedContractVersion) {
1029
1199
  try {
1030
1200
  const mark = "playcraft-preview-storage:";
@@ -1065,7 +1235,6 @@ function connectPreviewRuntime(options) {
1065
1235
  const origin = new URL(options.parentOrigin).origin;
1066
1236
  if (origin === "null" || options.parentOrigin !== origin)
1067
1237
  throw new ProtocolError("ORIGIN", "An exact parent origin is required");
1068
- const runtime = createParameterRuntime(options);
1069
1238
  const bridge = new Bridge({
1070
1239
  peer: () => window.parent,
1071
1240
  targetOrigin: origin,
@@ -1073,6 +1242,27 @@ function connectPreviewRuntime(options) {
1073
1242
  strictSource: true
1074
1243
  });
1075
1244
  const names = BridgeMethods.runtimeParameters;
1245
+ const runtime = createParameterRuntime({
1246
+ ...options,
1247
+ async loadImage(request, signal) {
1248
+ const value = await bridge.emit(names.asset, request, { timeout: 16e3, signal });
1249
+ if (!sameIdentity(request, value))
1250
+ throw new ProtocolError("STALE", "Image response belongs to a different runtime");
1251
+ const response = value;
1252
+ if (response.draftRevision !== request.draftRevision || response.documentId !== request.documentId || fingerprint(response.path) !== fingerprint(request.path) || response.value !== request.value)
1253
+ throw new ProtocolError("STALE", "Image response does not match its request");
1254
+ if (response.status === "stale") throw new SupersededResourceError();
1255
+ if (response.status === "failed") {
1256
+ if (typeof response.error !== "string")
1257
+ throw new ProtocolError("INVALID", "Invalid image failure response");
1258
+ throw new ResourceLoadError(response.error);
1259
+ }
1260
+ if (response.status !== "ready")
1261
+ throw new ProtocolError("INVALID", "Invalid image resource response");
1262
+ assertImageResourceData(response);
1263
+ return decodeImage(response.dataUrl, signal);
1264
+ }
1265
+ });
1076
1266
  const off = [
1077
1267
  bridge.on(names.hello, (value) => runtime.hello(value)),
1078
1268
  bridge.on(names.snapshot, (value) => runtime.snapshot(value)),
@@ -1116,6 +1306,7 @@ function connectPreviewRuntime(options) {
1116
1306
  };
1117
1307
  }
1118
1308
  export {
1309
+ ResourceLoadError,
1119
1310
  connectPreviewRuntime,
1120
1311
  readWorkspacePreviewContext
1121
1312
  };
@@ -1,4 +1,4 @@
1
- import type { FieldCapability, RawDocuments } from './protocol.js';
1
+ import type { FieldCapability, ImageResourceRequest, RawDocuments } from './protocol.js';
2
2
  export interface ApplyContext<C> {
3
3
  beforeConfig: C;
4
4
  config: C;
@@ -6,6 +6,7 @@ export interface ApplyContext<C> {
6
6
  fields: FieldCapability[];
7
7
  signal: AbortSignal;
8
8
  assertCurrent(): void;
9
+ loadImage(field: FieldCapability, value: string): Promise<HTMLImageElement>;
9
10
  }
10
11
  /** All callbacks must check cancellation before late asynchronous side effects. */
11
12
  export interface GameAdapter<C> {
@@ -23,4 +24,5 @@ export interface RuntimeOptions<C> {
23
24
  install(config: C): unknown;
24
25
  };
25
26
  createGame(context: ApplyContext<C>): Promise<GameAdapter<C>>;
27
+ loadImage?(request: ImageResourceRequest, signal: AbortSignal): Promise<HTMLImageElement>;
26
28
  }
@@ -1,5 +1,6 @@
1
1
  import type { RuntimeOptions } from './adapters.js';
2
2
  import type { SessionBinding } from './protocol.js';
3
+ export { ResourceLoadError } from './protocol.js';
3
4
  /** Reads only the platform's explicit context; a game receives no platform credentials. */
4
5
  export declare function readWorkspacePreviewContext(expectedContractVersion: string): {
5
6
  binding: SessionBinding;
@@ -30,6 +30,7 @@ export interface FieldCapability {
30
30
  effectivePath: string[];
31
31
  groupId: string;
32
32
  mode: ApplyMode;
33
+ resource?: 'image';
33
34
  }
34
35
  export interface Capabilities extends Identity {
35
36
  stage: 'bootstrap-ready' | 'game-ready';
@@ -73,6 +74,40 @@ export interface ApplicationResult extends Identity {
73
74
  status: 'idle' | 'applying' | 'applied' | 'pending-commit' | 'requires-reload' | 'error';
74
75
  groups: GroupResult[];
75
76
  error?: string;
77
+ recoverable?: true;
78
+ resourceErrors?: Array<{
79
+ documentId: string;
80
+ path: string[];
81
+ error: string;
82
+ }>;
83
+ }
84
+ export interface ImageResourceRequest extends Identity {
85
+ draftRevision: number;
86
+ documentId: string;
87
+ path: string[];
88
+ value: string;
89
+ }
90
+ export type ImageResourceData = {
91
+ mimeType: string;
92
+ dataUrl: string;
93
+ contentHash?: string;
94
+ };
95
+ export type ImageResourceResponse = ImageResourceRequest & (({
96
+ status: 'ready';
97
+ } & ImageResourceData) | {
98
+ status: 'failed';
99
+ error: string;
100
+ } | {
101
+ status: 'stale';
102
+ });
103
+ export declare const MAX_IMAGE_RESOURCE_BYTES: number;
104
+ /** Loading failures retain the last rendered image and allow another parameter patch. */
105
+ export declare class ResourceLoadError extends Error {
106
+ constructor(message: string);
107
+ }
108
+ /** A newer value replaced this resource while it was being prepared. */
109
+ export declare class SupersededResourceError extends Error {
110
+ constructor();
76
111
  }
77
112
  export declare class ProtocolError extends Error {
78
113
  readonly code: string;
@@ -88,6 +123,8 @@ export declare function assertIdentity(value: unknown): asserts value is Identit
88
123
  export declare function sameIdentity(a: Identity, b: unknown): b is Identity;
89
124
  export declare function assertBinding(expected: SessionBinding, actual: Hello): void;
90
125
  export declare function assertDocuments(value: unknown): asserts value is RawDocuments;
126
+ export declare function assertImageResourceRequest(value: unknown): asserts value is ImageResourceRequest;
127
+ export declare function assertImageResourceData(value: unknown): asserts value is ImageResourceData;
91
128
  export declare function assertSnapshot(value: unknown): asserts value is SnapshotRequest;
92
129
  export declare function assertPatch(value: unknown): asserts value is PatchRequest;
93
130
  export declare function applyOperations(documents: RawDocuments, operations: Operation[]): RawDocuments;