@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.
- package/dist/commands/build.js +38 -0
- package/dist/workspace-runtime/previews/finalize.js +64 -0
- package/dist/workspace-runtime/previews/publish.js +31 -8
- package/dist/workspace-runtime/server/http.js +5 -5
- package/dist/workspace-runtime/workspaces/artifact-events.js +136 -23
- package/dist/workspace-runtime/workspaces/artifact-files.js +3 -1
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +27 -4
- package/dist/workspace-runtime/workspaces/artifacts.js +115 -17
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +5 -1
- package/dist/workspace-runtime/workspaces/delivery-build.js +97 -7
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -0
- package/dist/workspace-runtime/workspaces/git.js +13 -0
- package/dist/workspace-runtime/workspaces/parameter-assets.js +38 -15
- package/dist/workspace-runtime/workspaces/parameters.js +61 -21
- package/dist/workspace-runtime/workspaces/theme-asset-index.js +123 -13
- package/package.json +3 -3
- package/project-templates/empty/AGENTS.md +1 -1
- package/templates/preview-runtime/v1/README.md +8 -0
- package/templates/preview-runtime/v1/manifest.json +7 -7
- package/templates/preview-runtime/v1/runtime.d.mts +1 -1
- package/templates/preview-runtime/v1/runtime.d.ts +1 -1
- package/templates/preview-runtime/v1/runtime.mjs +238 -47
- package/templates/preview-runtime/v1/types/adapters.d.ts +3 -1
- package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
- 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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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())
|
|
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)
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|