@playcraft/cli 0.1.55-beta → 0.1.57-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/assets/client.js +6 -4
- package/dist/commands/build.js +38 -0
- package/dist/commands/tools-3d.js +15 -0
- package/dist/commands/tools-generation.js +3 -2
- package/dist/utils/agent-api-client.js +31 -8
- package/dist/utils/agent-generation-provenance.js +45 -0
- package/dist/utils/generation-workspace-scope.js +50 -0
- package/dist/utils/image-generation-retry.js +7 -0
- package/dist/utils/tool-operation-journal.js +34 -4
- package/dist/workspace-runtime/batch/prepare.js +9 -52
- package/dist/workspace-runtime/codex/delivery-tools.js +7 -5
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +80 -0
- package/dist/workspace-runtime/codex/launcher-client.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +23 -4
- package/dist/workspace-runtime/previews/finalize.js +64 -0
- package/dist/workspace-runtime/previews/publish.js +31 -8
- package/dist/workspace-runtime/serve.js +8 -30
- 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/dependencies.js +24 -10
- 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/platform-export.js +185 -0
- package/dist/workspace-runtime/workspaces/theme-asset-index.js +123 -13
- package/package.json +3 -3
- package/project-template-v2/.playcraft/skills.lock.json +15 -8
- package/project-templates/empty/AGENTS.md +1 -1
- package/templates/preview-runtime/v1/README.md +12 -0
- package/templates/preview-runtime/v1/manifest.json +8 -8
- 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 +591 -54
- package/templates/preview-runtime/v1/types/adapters.d.ts +6 -1
- package/templates/preview-runtime/v1/types/preview.d.ts +2 -0
- package/templates/preview-runtime/v1/types/protocol.d.ts +56 -0
- package/templates/preview-runtime/v1/types/runtime.d.ts +2 -1
|
@@ -197,6 +197,8 @@ 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",
|
|
201
|
+
refreshImages: "runtime.parameters.refresh-images",
|
|
200
202
|
capabilities: "runtime.parameters.capabilities",
|
|
201
203
|
result: "runtime.parameters.result",
|
|
202
204
|
resyncRequired: "runtime.parameters.resync-required"
|
|
@@ -220,7 +222,9 @@ var BRIDGE_RPC_NAMES = [
|
|
|
220
222
|
BridgeMethods.runtimeParameters.patch,
|
|
221
223
|
BridgeMethods.runtimeParameters.restart,
|
|
222
224
|
BridgeMethods.runtimeParameters.status,
|
|
223
|
-
BridgeMethods.runtimeParameters.dispose
|
|
225
|
+
BridgeMethods.runtimeParameters.dispose,
|
|
226
|
+
BridgeMethods.runtimeParameters.asset,
|
|
227
|
+
BridgeMethods.runtimeParameters.refreshImages
|
|
224
228
|
];
|
|
225
229
|
|
|
226
230
|
// ../iframe-bridge/src/configs/perspective.ts
|
|
@@ -415,7 +419,7 @@ function safeReply(bridge, message) {
|
|
|
415
419
|
try {
|
|
416
420
|
bridge.postMessage(message);
|
|
417
421
|
} catch (err) {
|
|
418
|
-
if (err instanceof PlaycraftBridgeError && err.code === BridgeErrorCodes.PEER_UNAVAILABLE) {
|
|
422
|
+
if (err instanceof PlaycraftBridgeError && (err.code === BridgeErrorCodes.PEER_UNAVAILABLE || err.code === BridgeErrorCodes.DESTROYED)) {
|
|
419
423
|
return;
|
|
420
424
|
}
|
|
421
425
|
throw err;
|
|
@@ -524,6 +528,19 @@ var Bridge = class {
|
|
|
524
528
|
};
|
|
525
529
|
|
|
526
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
|
+
};
|
|
527
544
|
var ProtocolError = class extends Error {
|
|
528
545
|
constructor(code, message) {
|
|
529
546
|
super(message);
|
|
@@ -622,6 +639,44 @@ var identityKeys = [
|
|
|
622
639
|
"configContractVersion",
|
|
623
640
|
"locale"
|
|
624
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
|
+
}
|
|
625
680
|
function assertSnapshot(value) {
|
|
626
681
|
assertIdentity(value);
|
|
627
682
|
exactKeys(value, [
|
|
@@ -699,12 +754,63 @@ function createParameterRuntime(options) {
|
|
|
699
754
|
let error;
|
|
700
755
|
let disposed = false;
|
|
701
756
|
let cleanupFailure;
|
|
757
|
+
let recoverable = false;
|
|
758
|
+
const resourceErrors = /* @__PURE__ */ new Map();
|
|
702
759
|
let resultSequence = 0;
|
|
703
760
|
const retiredSessions = /* @__PURE__ */ new Set();
|
|
704
761
|
const groups = /* @__PURE__ */ new Map();
|
|
705
762
|
const receipts = /* @__PURE__ */ new Map();
|
|
706
763
|
const listeners = /* @__PURE__ */ new Set();
|
|
707
764
|
const capabilityListeners = /* @__PURE__ */ new Set();
|
|
765
|
+
const installedImages = /* @__PURE__ */ new Map();
|
|
766
|
+
const imageReads = /* @__PURE__ */ new Map();
|
|
767
|
+
const imageLoads = /* @__PURE__ */ new WeakMap();
|
|
768
|
+
const refreshGoals = /* @__PURE__ */ new Map();
|
|
769
|
+
const refreshWork = /* @__PURE__ */ new Set();
|
|
770
|
+
const refreshReceipts = /* @__PURE__ */ new Map();
|
|
771
|
+
let resourceSequence = 0;
|
|
772
|
+
const imageKey = (field) => fingerprint([field.documentId, field.path]);
|
|
773
|
+
async function readImage(request, signal) {
|
|
774
|
+
signal.throwIfAborted();
|
|
775
|
+
const key = imageKey(request);
|
|
776
|
+
const read = new AbortController();
|
|
777
|
+
const cancel = () => read.abort(signal.reason);
|
|
778
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
779
|
+
const active = imageReads.get(key) ?? /* @__PURE__ */ new Set();
|
|
780
|
+
active.add(read);
|
|
781
|
+
imageReads.set(key, active);
|
|
782
|
+
let off = () => {
|
|
783
|
+
};
|
|
784
|
+
try {
|
|
785
|
+
return await Promise.race([
|
|
786
|
+
options.loadImage(request, read.signal),
|
|
787
|
+
new Promise((_, reject) => {
|
|
788
|
+
const cancelled = () => reject(read.signal.reason);
|
|
789
|
+
read.signal.addEventListener("abort", cancelled, { once: true });
|
|
790
|
+
off = () => read.signal.removeEventListener("abort", cancelled);
|
|
791
|
+
if (read.signal.aborted) cancelled();
|
|
792
|
+
})
|
|
793
|
+
]);
|
|
794
|
+
} finally {
|
|
795
|
+
off();
|
|
796
|
+
signal.removeEventListener("abort", cancel);
|
|
797
|
+
active.delete(read);
|
|
798
|
+
if (!active.size && imageReads.get(key) === active) imageReads.delete(key);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function cancelRefreshes() {
|
|
802
|
+
for (const goal of refreshGoals.values()) goal.controller.abort(new SupersededResourceError());
|
|
803
|
+
refreshGoals.clear();
|
|
804
|
+
}
|
|
805
|
+
function commitImages(ctx) {
|
|
806
|
+
for (const [key, image] of imageLoads.get(ctx) ?? []) {
|
|
807
|
+
installedImages.set(key, image);
|
|
808
|
+
resourceErrors.delete(key);
|
|
809
|
+
}
|
|
810
|
+
for (const field of ctx.fields)
|
|
811
|
+
if (field.resource === "image" && readPath(ctx.config, field.effectivePath) === "")
|
|
812
|
+
installedImages.delete(imageKey(field));
|
|
813
|
+
}
|
|
708
814
|
async function release(game) {
|
|
709
815
|
try {
|
|
710
816
|
await game?.dispose();
|
|
@@ -719,6 +825,7 @@ function createParameterRuntime(options) {
|
|
|
719
825
|
...requireIdentity(),
|
|
720
826
|
stage,
|
|
721
827
|
canAcceptSnapshot: true,
|
|
828
|
+
...options.loadImage ? { canRefreshImages: true, canReportMissingImages: true } : {},
|
|
722
829
|
fields: clone(options.fields)
|
|
723
830
|
});
|
|
724
831
|
function requireIdentity() {
|
|
@@ -739,7 +846,9 @@ function createParameterRuntime(options) {
|
|
|
739
846
|
appliedRevision,
|
|
740
847
|
status,
|
|
741
848
|
groups: clone([...groups.values()]),
|
|
742
|
-
...error ? { error } : {}
|
|
849
|
+
...error ? { error } : {},
|
|
850
|
+
...recoverable ? { recoverable: true } : {},
|
|
851
|
+
...resourceErrors.size ? { resourceErrors: clone([...resourceErrors.values()]) } : {}
|
|
743
852
|
});
|
|
744
853
|
function publish() {
|
|
745
854
|
if (!identity || disposed) return;
|
|
@@ -747,19 +856,77 @@ function createParameterRuntime(options) {
|
|
|
747
856
|
const value = result();
|
|
748
857
|
listeners.forEach((fn) => fn(value));
|
|
749
858
|
}
|
|
750
|
-
function context(config, beforeConfig, draftRevision, fields, signal) {
|
|
751
|
-
|
|
859
|
+
function context(config, beforeConfig, draftRevision, fields, signal, refresh) {
|
|
860
|
+
const images = /* @__PURE__ */ new Map();
|
|
861
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
862
|
+
const assertCurrent = () => {
|
|
863
|
+
signal.throwIfAborted();
|
|
864
|
+
if (signal !== controller.signal || disposed)
|
|
865
|
+
throw new ProtocolError("STALE", "Application task expired");
|
|
866
|
+
refresh?.assertCurrent();
|
|
867
|
+
if (images.size && target) {
|
|
868
|
+
const latest = options.resolve(target);
|
|
869
|
+
for (const [key, { field, value, sequence }] of images)
|
|
870
|
+
if (readPath(latest, field.effectivePath) !== value || (refreshGoals.get(key)?.sequence ?? 0) !== sequence)
|
|
871
|
+
throw new SupersededResourceError();
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const ctx = {
|
|
752
875
|
config,
|
|
753
876
|
beforeConfig,
|
|
754
877
|
draftRevision,
|
|
755
878
|
fields,
|
|
756
879
|
signal,
|
|
757
|
-
assertCurrent
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
880
|
+
assertCurrent,
|
|
881
|
+
async loadImage(field, value) {
|
|
882
|
+
const registered = options.fields.find(
|
|
883
|
+
(item) => item.resource === "image" && item.documentId === field.documentId && fingerprint(item.path) === fingerprint(field.path)
|
|
884
|
+
);
|
|
885
|
+
if (!registered || typeof value !== "string" || !value)
|
|
886
|
+
throw new ProtocolError("UNSUPPORTED", "Unregistered image resource field");
|
|
887
|
+
if (!options.loadImage)
|
|
888
|
+
throw new ProtocolError("UNSUPPORTED", "Image resource transport is unavailable");
|
|
889
|
+
const key = fingerprint([registered.documentId, registered.path]);
|
|
890
|
+
const goal = refreshGoals.get(key);
|
|
891
|
+
images.set(key, { field: registered, value, sequence: goal?.sequence ?? 0 });
|
|
892
|
+
assertCurrent();
|
|
893
|
+
try {
|
|
894
|
+
const prepared = refresh?.prepared.get(key);
|
|
895
|
+
const response = prepared?.value === value ? prepared : await readImage(
|
|
896
|
+
{
|
|
897
|
+
...requireIdentity(),
|
|
898
|
+
draftRevision,
|
|
899
|
+
documentId: registered.documentId,
|
|
900
|
+
path: clone(registered.path),
|
|
901
|
+
value,
|
|
902
|
+
...goal?.value === value ? {
|
|
903
|
+
resourceSequence: goal.sequence,
|
|
904
|
+
...goal.contentHash !== null ? { expectedContentHash: goal.contentHash } : {}
|
|
905
|
+
} : {}
|
|
906
|
+
},
|
|
907
|
+
signal
|
|
908
|
+
);
|
|
909
|
+
assertCurrent();
|
|
910
|
+
const image = "image" in response ? response.image : response;
|
|
911
|
+
const contentHash = "image" in response ? response.contentHash : void 0;
|
|
912
|
+
if (goal?.value === value && goal.contentHash !== null && contentHash !== goal.contentHash)
|
|
913
|
+
throw new ResourceLoadError("Image resource content changed during refresh");
|
|
914
|
+
loaded.set(key, { value, image, ...contentHash ? { contentHash } : {} });
|
|
915
|
+
return image;
|
|
916
|
+
} catch (failure) {
|
|
917
|
+
assertCurrent();
|
|
918
|
+
if (failure instanceof ResourceLoadError)
|
|
919
|
+
resourceErrors.set(key, {
|
|
920
|
+
documentId: registered.documentId,
|
|
921
|
+
path: clone(registered.path),
|
|
922
|
+
error: failure.message
|
|
923
|
+
});
|
|
924
|
+
throw failure;
|
|
925
|
+
}
|
|
761
926
|
}
|
|
762
927
|
};
|
|
928
|
+
imageLoads.set(ctx, loaded);
|
|
929
|
+
return ctx;
|
|
763
930
|
}
|
|
764
931
|
function enqueue(work) {
|
|
765
932
|
const signal = controller.signal;
|
|
@@ -769,13 +936,20 @@ function createParameterRuntime(options) {
|
|
|
769
936
|
await work(signal);
|
|
770
937
|
} catch (e) {
|
|
771
938
|
if (signal.aborted) return;
|
|
939
|
+
if (e instanceof SupersededResourceError) return;
|
|
772
940
|
status = "error";
|
|
773
|
-
|
|
941
|
+
recoverable = e instanceof ResourceLoadError;
|
|
942
|
+
if (!recoverable) installedRevision = appliedRevision = -1;
|
|
774
943
|
error = e instanceof Error ? e.message : String(e);
|
|
775
|
-
for (const group of groups.values())
|
|
944
|
+
for (const group of groups.values())
|
|
945
|
+
if (!recoverable || options.fields.some(
|
|
946
|
+
(field) => field.groupId === group.groupId && resourceErrors.has(fingerprint([field.documentId, field.path]))
|
|
947
|
+
))
|
|
948
|
+
group.status = "unknown";
|
|
776
949
|
publish();
|
|
777
950
|
}
|
|
778
951
|
});
|
|
952
|
+
return queue;
|
|
779
953
|
}
|
|
780
954
|
function remember(kind, request) {
|
|
781
955
|
const key = `${kind}:${request.draftRevision}`;
|
|
@@ -795,12 +969,26 @@ function createParameterRuntime(options) {
|
|
|
795
969
|
return receipt;
|
|
796
970
|
}
|
|
797
971
|
function updateState(rev, changed) {
|
|
972
|
+
const previousGroups = new Map(groups);
|
|
798
973
|
for (const field of changed)
|
|
799
974
|
groups.set(field.groupId, {
|
|
800
975
|
groupId: field.groupId,
|
|
801
976
|
appliedRevision: rev,
|
|
802
977
|
status: "applied"
|
|
803
978
|
});
|
|
979
|
+
if (resourceErrors.size) {
|
|
980
|
+
status = "error";
|
|
981
|
+
recoverable = true;
|
|
982
|
+
if (changed.length) installedRevision = appliedRevision = -1;
|
|
983
|
+
error = [...resourceErrors.values()].map((item) => item.error).join("; ");
|
|
984
|
+
for (const field of options.fields)
|
|
985
|
+
if (resourceErrors.has(fingerprint([field.documentId, field.path])))
|
|
986
|
+
groups.set(field.groupId, { ...previousGroups.get(field.groupId), status: "unknown" });
|
|
987
|
+
publish();
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
recoverable = false;
|
|
991
|
+
error = void 0;
|
|
804
992
|
let pending = false, reload = false;
|
|
805
993
|
for (const field of options.fields) {
|
|
806
994
|
if (fingerprint(readPath(installed?.[field.documentId], field.path)) !== fingerprint(readPath(target?.[field.documentId], field.path))) {
|
|
@@ -817,13 +1005,11 @@ function createParameterRuntime(options) {
|
|
|
817
1005
|
} else status = reload ? "requires-reload" : pending ? "pending-commit" : "applying";
|
|
818
1006
|
publish();
|
|
819
1007
|
}
|
|
820
|
-
|
|
821
|
-
if (status === "error") return;
|
|
822
|
-
if (!adapter || !installed)
|
|
823
|
-
throw new ProtocolError("STATE", "Initial snapshot must create the game first");
|
|
1008
|
+
function candidateFor(documents, commit, retryResources) {
|
|
824
1009
|
const operations = [];
|
|
825
1010
|
for (const field of options.fields) {
|
|
826
1011
|
if (field.mode !== "live" && !(commit && field.mode === "restart-scene")) continue;
|
|
1012
|
+
if (resourceErrors.has(imageKey(field)) && !retryResources.has(imageKey(field))) continue;
|
|
827
1013
|
const value = readPath(documents[field.documentId], field.path);
|
|
828
1014
|
operations.push(
|
|
829
1015
|
value === void 0 ? { op: "remove", documentId: field.documentId, path: field.path } : {
|
|
@@ -834,17 +1020,34 @@ function createParameterRuntime(options) {
|
|
|
834
1020
|
}
|
|
835
1021
|
);
|
|
836
1022
|
}
|
|
837
|
-
|
|
1023
|
+
return applyOperations(installed, operations);
|
|
1024
|
+
}
|
|
1025
|
+
async function applyTarget(documents, rev, commit, signal, retryResources = /* @__PURE__ */ new Set(), refresh, rereadImages = /* @__PURE__ */ new Set()) {
|
|
1026
|
+
if (status === "error" && !recoverable) return;
|
|
1027
|
+
if (!adapter || !installed)
|
|
1028
|
+
throw new ProtocolError("STATE", "Initial snapshot must create the game first");
|
|
1029
|
+
const candidate = candidateFor(documents, commit, retryResources);
|
|
838
1030
|
const before = options.source.get();
|
|
839
1031
|
const config = freeze(options.resolve(freeze(clone(candidate))));
|
|
840
1032
|
const changed = options.fields.filter(
|
|
841
|
-
(field) => fingerprint(readPath(before, field.effectivePath)) !== fingerprint(readPath(config, field.effectivePath))
|
|
1033
|
+
(field) => rereadImages.has(imageKey(field)) || refresh?.prepared.has(imageKey(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))
|
|
1034
|
+
);
|
|
1035
|
+
const ctx = context(
|
|
1036
|
+
config,
|
|
1037
|
+
before,
|
|
1038
|
+
rev,
|
|
1039
|
+
changed,
|
|
1040
|
+
signal,
|
|
1041
|
+
refresh ? {
|
|
1042
|
+
prepared: refresh.prepared,
|
|
1043
|
+
assertCurrent: () => refresh.assertCurrent(config)
|
|
1044
|
+
} : void 0
|
|
842
1045
|
);
|
|
843
|
-
const ctx = context(config, before, rev, changed, signal);
|
|
844
1046
|
ctx.assertCurrent();
|
|
845
1047
|
status = "applying";
|
|
846
1048
|
publish();
|
|
847
1049
|
options.source.install(config);
|
|
1050
|
+
const previousInstalledRevision = installedRevision;
|
|
848
1051
|
installedRevision = fingerprint(candidate) === fingerprint(documents) ? rev : -1;
|
|
849
1052
|
try {
|
|
850
1053
|
if (changed.some((field) => field.mode === "restart-scene")) await adapter.restartScene(ctx);
|
|
@@ -852,10 +1055,29 @@ function createParameterRuntime(options) {
|
|
|
852
1055
|
ctx.assertCurrent();
|
|
853
1056
|
await adapter.afterRender(ctx);
|
|
854
1057
|
ctx.assertCurrent();
|
|
1058
|
+
commitImages(ctx);
|
|
1059
|
+
for (const field of changed)
|
|
1060
|
+
if (field.resource === "image" && readPath(config, field.effectivePath) === "")
|
|
1061
|
+
resourceErrors.delete(fingerprint([field.documentId, field.path]));
|
|
855
1062
|
installed = candidate;
|
|
856
1063
|
updateState(rev, changed);
|
|
857
1064
|
} catch (e) {
|
|
858
|
-
if (!signal.aborted)
|
|
1065
|
+
if (!signal.aborted) {
|
|
1066
|
+
options.source.install(before);
|
|
1067
|
+
installedRevision = previousInstalledRevision;
|
|
1068
|
+
if (e instanceof ResourceLoadError)
|
|
1069
|
+
for (const field of changed) {
|
|
1070
|
+
if (field.resource !== "image") continue;
|
|
1071
|
+
const key = imageKey(field);
|
|
1072
|
+
installedImages.delete(key);
|
|
1073
|
+
if (!resourceErrors.has(key))
|
|
1074
|
+
resourceErrors.set(key, {
|
|
1075
|
+
documentId: field.documentId,
|
|
1076
|
+
path: clone(field.path),
|
|
1077
|
+
error: e.message
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
859
1081
|
throw e;
|
|
860
1082
|
}
|
|
861
1083
|
}
|
|
@@ -882,12 +1104,18 @@ function createParameterRuntime(options) {
|
|
|
882
1104
|
cleanupFailure = e;
|
|
883
1105
|
});
|
|
884
1106
|
identity = freeze({ ...clone(value), runtimeInstanceId });
|
|
1107
|
+
cancelRefreshes();
|
|
1108
|
+
installedImages.clear();
|
|
1109
|
+
refreshReceipts.clear();
|
|
1110
|
+
resourceSequence = 0;
|
|
885
1111
|
target = installed = void 0;
|
|
886
1112
|
receivedRevision = installedRevision = appliedRevision = -1;
|
|
887
1113
|
receipts.clear();
|
|
888
1114
|
stage = "bootstrap-ready";
|
|
889
1115
|
status = "idle";
|
|
890
1116
|
error = void 0;
|
|
1117
|
+
recoverable = false;
|
|
1118
|
+
resourceErrors.clear();
|
|
891
1119
|
resultSequence = 0;
|
|
892
1120
|
for (const group of groups.values()) {
|
|
893
1121
|
group.appliedRevision = -1;
|
|
@@ -907,6 +1135,8 @@ function createParameterRuntime(options) {
|
|
|
907
1135
|
const rev = value.draftRevision;
|
|
908
1136
|
const documents = freeze(clone(value.documents));
|
|
909
1137
|
options.resolve(documents);
|
|
1138
|
+
cancelRefreshes();
|
|
1139
|
+
installedImages.clear();
|
|
910
1140
|
target = documents;
|
|
911
1141
|
const receipt = accept("snapshot", value);
|
|
912
1142
|
enqueue(async (signal) => {
|
|
@@ -915,44 +1145,57 @@ function createParameterRuntime(options) {
|
|
|
915
1145
|
"RECOVERY",
|
|
916
1146
|
"Previous game cleanup failed; reload the preview frame"
|
|
917
1147
|
);
|
|
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
1148
|
status = "applying";
|
|
923
1149
|
publish();
|
|
924
1150
|
const previousAdapter = adapter;
|
|
925
1151
|
adapter = void 0;
|
|
926
1152
|
stage = "bootstrap-ready";
|
|
927
1153
|
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);
|
|
1154
|
+
let nextDocuments = documents;
|
|
1155
|
+
let nextRevision = rev;
|
|
1156
|
+
for (; ; ) {
|
|
1157
|
+
const before = options.source.get();
|
|
1158
|
+
resourceErrors.clear();
|
|
1159
|
+
recoverable = false;
|
|
1160
|
+
const config = freeze(options.resolve(nextDocuments));
|
|
1161
|
+
const ctx = context(config, before, nextRevision, clone(options.fields), signal);
|
|
943
1162
|
ctx.assertCurrent();
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1163
|
+
options.source.install(config);
|
|
1164
|
+
installedRevision = nextRevision;
|
|
1165
|
+
let created;
|
|
1166
|
+
try {
|
|
1167
|
+
created = await options.createGame(ctx);
|
|
1168
|
+
if (signal.aborted) {
|
|
1169
|
+
await release(created);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
ctx.assertCurrent();
|
|
1173
|
+
adapter = created;
|
|
1174
|
+
stage = "game-ready";
|
|
952
1175
|
capabilityListeners.forEach((fn) => fn(capabilities()));
|
|
1176
|
+
await created.afterRender(ctx);
|
|
1177
|
+
ctx.assertCurrent();
|
|
1178
|
+
commitImages(ctx);
|
|
1179
|
+
installed = clone(nextDocuments);
|
|
1180
|
+
if (resourceErrors.size) installedRevision = -1;
|
|
1181
|
+
error = void 0;
|
|
1182
|
+
updateState(nextRevision, options.fields);
|
|
1183
|
+
return;
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
if (!signal.aborted) {
|
|
1186
|
+
adapter = void 0;
|
|
1187
|
+
stage = "bootstrap-ready";
|
|
1188
|
+
options.source.install(before);
|
|
1189
|
+
capabilityListeners.forEach((fn) => fn(capabilities()));
|
|
1190
|
+
}
|
|
1191
|
+
await release(created);
|
|
1192
|
+
if (e instanceof SupersededResourceError && !signal.aborted && target) {
|
|
1193
|
+
nextDocuments = freeze(clone(target));
|
|
1194
|
+
nextRevision = receivedRevision;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
throw e;
|
|
953
1198
|
}
|
|
954
|
-
await release(created);
|
|
955
|
-
throw e;
|
|
956
1199
|
}
|
|
957
1200
|
});
|
|
958
1201
|
return receipt;
|
|
@@ -964,7 +1207,7 @@ function createParameterRuntime(options) {
|
|
|
964
1207
|
if (previous) return previous;
|
|
965
1208
|
if (!target || value.baseDraftRevision !== receivedRevision)
|
|
966
1209
|
throw new ProtocolError("RESYNC", "Patch base does not match received revision");
|
|
967
|
-
if (status === "error")
|
|
1210
|
+
if (status === "error" && !recoverable)
|
|
968
1211
|
throw new ProtocolError(
|
|
969
1212
|
"RECOVERY",
|
|
970
1213
|
"Application state is unknown; send a recovery snapshot"
|
|
@@ -978,13 +1221,227 @@ function createParameterRuntime(options) {
|
|
|
978
1221
|
const rev = value.draftRevision, commit = value.phase === "commit";
|
|
979
1222
|
options.resolve(documents);
|
|
980
1223
|
target = documents;
|
|
1224
|
+
const latestConfig = options.resolve(documents);
|
|
1225
|
+
const rereadImages = /* @__PURE__ */ new Set();
|
|
1226
|
+
for (const [key, goal] of refreshGoals)
|
|
1227
|
+
if (readPath(latestConfig, goal.field.effectivePath) !== goal.value || fingerprint(readPath(documents[goal.field.documentId], goal.field.path)) !== fingerprint(goal.raw)) {
|
|
1228
|
+
const image = installedImages.get(key);
|
|
1229
|
+
if (goal.contentHash !== null && (goal.applying || image?.value !== goal.value || image.contentHash !== goal.contentHash))
|
|
1230
|
+
rereadImages.add(key);
|
|
1231
|
+
goal.controller.abort(new SupersededResourceError());
|
|
1232
|
+
refreshGoals.delete(key);
|
|
1233
|
+
}
|
|
981
1234
|
const receipt = accept("patch", value);
|
|
982
|
-
|
|
1235
|
+
const retryResources = new Set(
|
|
1236
|
+
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]))
|
|
1237
|
+
);
|
|
1238
|
+
enqueue(
|
|
1239
|
+
(signal) => applyTarget(documents, rev, commit, signal, retryResources, void 0, rereadImages)
|
|
1240
|
+
);
|
|
1241
|
+
return receipt;
|
|
1242
|
+
},
|
|
1243
|
+
refreshImages(value) {
|
|
1244
|
+
assertImageRefresh(value);
|
|
1245
|
+
check(value);
|
|
1246
|
+
const request = freeze(clone(value));
|
|
1247
|
+
const previous = refreshReceipts.get(request.resourceSequence);
|
|
1248
|
+
if (previous) {
|
|
1249
|
+
if (previous.fingerprint !== fingerprint(request))
|
|
1250
|
+
throw new ProtocolError("CONFLICT", "Image refresh sequence already accepted");
|
|
1251
|
+
return previous.receipt;
|
|
1252
|
+
}
|
|
1253
|
+
if (request.resourceSequence <= resourceSequence || request.draftRevision !== receivedRevision || !target)
|
|
1254
|
+
throw new ProtocolError("STALE", "Image refresh must name the current target and sequence");
|
|
1255
|
+
if (!adapter || !installed || !options.loadImage || status === "error" && !recoverable)
|
|
1256
|
+
throw new ProtocolError("STATE", "Image refresh requires an available game");
|
|
1257
|
+
if (refreshWork.size >= 64)
|
|
1258
|
+
throw new ProtocolError("LIMIT", "Too many pending image refresh requests");
|
|
1259
|
+
const config = options.resolve(target);
|
|
1260
|
+
const uncertain = /* @__PURE__ */ new Set();
|
|
1261
|
+
const goals = request.images.map((image) => {
|
|
1262
|
+
const field = options.fields.find(
|
|
1263
|
+
(field2) => imageKey(field2) === imageKey(image) && field2.resource === "image" && field2.mode === "live"
|
|
1264
|
+
);
|
|
1265
|
+
const current = field && readPath(config, field.effectivePath);
|
|
1266
|
+
if (!field || typeof current !== "string" || !current)
|
|
1267
|
+
throw new ProtocolError("UNSUPPORTED", "Image refresh requires a live nonempty image");
|
|
1268
|
+
if (refreshGoals.get(imageKey(field))?.applying) uncertain.add(imageKey(field));
|
|
1269
|
+
return {
|
|
1270
|
+
field,
|
|
1271
|
+
value: current,
|
|
1272
|
+
raw: clone(readPath(target[field.documentId], field.path)),
|
|
1273
|
+
contentHash: image.contentHash,
|
|
1274
|
+
sequence: request.resourceSequence,
|
|
1275
|
+
controller: new AbortController()
|
|
1276
|
+
};
|
|
1277
|
+
});
|
|
1278
|
+
const receipt = {
|
|
1279
|
+
accepted: true,
|
|
1280
|
+
receivedRevision,
|
|
1281
|
+
resourceSequence: request.resourceSequence
|
|
1282
|
+
};
|
|
1283
|
+
resourceSequence = request.resourceSequence;
|
|
1284
|
+
refreshReceipts.set(resourceSequence, { fingerprint: fingerprint(request), receipt });
|
|
1285
|
+
if (refreshReceipts.size > 256) refreshReceipts.delete(refreshReceipts.keys().next().value);
|
|
1286
|
+
for (const goal of goals) {
|
|
1287
|
+
for (const read of imageReads.get(imageKey(goal.field)) ?? [])
|
|
1288
|
+
read.abort(new SupersededResourceError());
|
|
1289
|
+
refreshGoals.get(imageKey(goal.field))?.controller.abort(new SupersededResourceError());
|
|
1290
|
+
refreshGoals.set(imageKey(goal.field), goal);
|
|
1291
|
+
}
|
|
1292
|
+
const active = goals.filter((goal) => {
|
|
1293
|
+
if (goal.contentHash === null) return false;
|
|
1294
|
+
const image = installedImages.get(imageKey(goal.field));
|
|
1295
|
+
return uncertain.has(imageKey(goal.field)) || resourceErrors.has(imageKey(goal.field)) || image?.value !== goal.value || image.contentHash !== goal.contentHash;
|
|
1296
|
+
});
|
|
1297
|
+
const sessionSignal = controller.signal;
|
|
1298
|
+
const assertSession = () => {
|
|
1299
|
+
sessionSignal.throwIfAborted();
|
|
1300
|
+
if (disposed || sessionSignal !== controller.signal || !target)
|
|
1301
|
+
throw new SupersededResourceError();
|
|
1302
|
+
};
|
|
1303
|
+
const isCurrent = (goal) => {
|
|
1304
|
+
assertSession();
|
|
1305
|
+
const latest = options.resolve(target);
|
|
1306
|
+
return !goal.controller.signal.aborted && refreshGoals.get(imageKey(goal.field)) === goal && readPath(latest, goal.field.effectivePath) === goal.value;
|
|
1307
|
+
};
|
|
1308
|
+
const assertGoal = (goal) => {
|
|
1309
|
+
if (!isCurrent(goal)) throw new SupersededResourceError();
|
|
1310
|
+
};
|
|
1311
|
+
const missing = goals.filter((goal) => goal.contentHash === null);
|
|
1312
|
+
for (const goal of missing)
|
|
1313
|
+
resourceErrors.set(imageKey(goal.field), {
|
|
1314
|
+
documentId: goal.field.documentId,
|
|
1315
|
+
path: clone(goal.field.path),
|
|
1316
|
+
error: "Image resource file is missing"
|
|
1317
|
+
});
|
|
1318
|
+
if (missing.length)
|
|
1319
|
+
void enqueue(async () => {
|
|
1320
|
+
assertSession();
|
|
1321
|
+
if (missing.some(isCurrent)) updateState(receivedRevision, []);
|
|
1322
|
+
});
|
|
1323
|
+
if (!active.length) return receipt;
|
|
1324
|
+
const work = (async () => {
|
|
1325
|
+
const prepared = /* @__PURE__ */ new Map();
|
|
1326
|
+
const cancel = () => goals.forEach((goal) => goal.controller.abort(sessionSignal.reason));
|
|
1327
|
+
sessionSignal.addEventListener("abort", cancel, { once: true });
|
|
1328
|
+
try {
|
|
1329
|
+
await Promise.all(
|
|
1330
|
+
active.map(async (goal) => {
|
|
1331
|
+
try {
|
|
1332
|
+
assertGoal(goal);
|
|
1333
|
+
const response = await readImage(
|
|
1334
|
+
{
|
|
1335
|
+
...requireIdentity(),
|
|
1336
|
+
draftRevision: request.draftRevision,
|
|
1337
|
+
documentId: goal.field.documentId,
|
|
1338
|
+
path: clone(goal.field.path),
|
|
1339
|
+
value: goal.value,
|
|
1340
|
+
resourceSequence: goal.sequence,
|
|
1341
|
+
expectedContentHash: goal.contentHash
|
|
1342
|
+
},
|
|
1343
|
+
goal.controller.signal
|
|
1344
|
+
);
|
|
1345
|
+
assertGoal(goal);
|
|
1346
|
+
if (!("image" in response) || response.contentHash !== goal.contentHash)
|
|
1347
|
+
throw new ResourceLoadError("Image resource content changed during refresh");
|
|
1348
|
+
prepared.set(imageKey(goal.field), {
|
|
1349
|
+
value: goal.value,
|
|
1350
|
+
image: response.image,
|
|
1351
|
+
contentHash: response.contentHash
|
|
1352
|
+
});
|
|
1353
|
+
} catch (failure) {
|
|
1354
|
+
if (!isCurrent(goal)) return;
|
|
1355
|
+
if (failure instanceof ResourceLoadError)
|
|
1356
|
+
resourceErrors.set(imageKey(goal.field), {
|
|
1357
|
+
documentId: goal.field.documentId,
|
|
1358
|
+
path: clone(goal.field.path),
|
|
1359
|
+
error: failure.message
|
|
1360
|
+
});
|
|
1361
|
+
throw failure;
|
|
1362
|
+
}
|
|
1363
|
+
})
|
|
1364
|
+
);
|
|
1365
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
1366
|
+
let rebase = false;
|
|
1367
|
+
await enqueue(async (signal) => {
|
|
1368
|
+
assertSession();
|
|
1369
|
+
const current = active.filter(
|
|
1370
|
+
(goal) => prepared.has(imageKey(goal.field)) && isCurrent(goal)
|
|
1371
|
+
);
|
|
1372
|
+
if (!current.length) return;
|
|
1373
|
+
const selected = new Map(
|
|
1374
|
+
current.map((goal) => [imageKey(goal.field), prepared.get(imageKey(goal.field))])
|
|
1375
|
+
);
|
|
1376
|
+
const retry = new Set(selected.keys());
|
|
1377
|
+
const documents = clone(target);
|
|
1378
|
+
const rev = receivedRevision;
|
|
1379
|
+
try {
|
|
1380
|
+
current.forEach((goal) => {
|
|
1381
|
+
goal.applying = true;
|
|
1382
|
+
});
|
|
1383
|
+
await applyTarget(documents, rev, false, signal, retry, {
|
|
1384
|
+
prepared: selected,
|
|
1385
|
+
assertCurrent(config2) {
|
|
1386
|
+
assertSession();
|
|
1387
|
+
if (current.some((goal) => !isCurrent(goal))) {
|
|
1388
|
+
rebase = true;
|
|
1389
|
+
throw new SupersededResourceError();
|
|
1390
|
+
}
|
|
1391
|
+
const latest = options.resolve(candidateFor(target, false, retry));
|
|
1392
|
+
if (fingerprint(latest) !== fingerprint(config2)) {
|
|
1393
|
+
rebase = true;
|
|
1394
|
+
throw new SupersededResourceError();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
} catch (failure) {
|
|
1399
|
+
if (rebase && failure instanceof SupersededResourceError) return;
|
|
1400
|
+
throw failure;
|
|
1401
|
+
} finally {
|
|
1402
|
+
current.forEach((goal) => {
|
|
1403
|
+
goal.applying = false;
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
if (!rebase) return;
|
|
1408
|
+
}
|
|
1409
|
+
throw new ResourceLoadError("Image refresh was interrupted by parameter changes; retry");
|
|
1410
|
+
} catch (failure) {
|
|
1411
|
+
if (!sessionSignal.aborted && !(failure instanceof SupersededResourceError)) {
|
|
1412
|
+
try {
|
|
1413
|
+
assertSession();
|
|
1414
|
+
} catch {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (failure instanceof ResourceLoadError) {
|
|
1418
|
+
for (const goal of active.filter(isCurrent))
|
|
1419
|
+
if (!resourceErrors.has(imageKey(goal.field)))
|
|
1420
|
+
resourceErrors.set(imageKey(goal.field), {
|
|
1421
|
+
documentId: goal.field.documentId,
|
|
1422
|
+
path: clone(goal.field.path),
|
|
1423
|
+
error: failure.message
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
await enqueue(async () => {
|
|
1427
|
+
throw failure;
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
} finally {
|
|
1431
|
+
goals.forEach((goal) => goal.controller.abort(new SupersededResourceError()));
|
|
1432
|
+
active.forEach((goal) => {
|
|
1433
|
+
goal.applying = false;
|
|
1434
|
+
});
|
|
1435
|
+
sessionSignal.removeEventListener("abort", cancel);
|
|
1436
|
+
}
|
|
1437
|
+
})();
|
|
1438
|
+
refreshWork.add(work);
|
|
1439
|
+
void work.finally(() => refreshWork.delete(work));
|
|
983
1440
|
return receipt;
|
|
984
1441
|
},
|
|
985
1442
|
restart(value) {
|
|
986
1443
|
check(value);
|
|
987
|
-
if (status === "error")
|
|
1444
|
+
if (status === "error" && !recoverable)
|
|
988
1445
|
throw new ProtocolError(
|
|
989
1446
|
"RECOVERY",
|
|
990
1447
|
"Application state is unknown; send a recovery snapshot"
|
|
@@ -993,7 +1450,17 @@ function createParameterRuntime(options) {
|
|
|
993
1450
|
if (rev !== receivedRevision || !target)
|
|
994
1451
|
throw new ProtocolError("STALE", "Restart must name the current target");
|
|
995
1452
|
const documents = clone(target);
|
|
996
|
-
enqueue(
|
|
1453
|
+
enqueue(
|
|
1454
|
+
(signal) => applyTarget(
|
|
1455
|
+
documents,
|
|
1456
|
+
rev,
|
|
1457
|
+
true,
|
|
1458
|
+
signal,
|
|
1459
|
+
new Set(
|
|
1460
|
+
options.fields.filter((field) => field.resource === "image").map((field) => fingerprint([field.documentId, field.path]))
|
|
1461
|
+
)
|
|
1462
|
+
)
|
|
1463
|
+
);
|
|
997
1464
|
return { accepted: true, receivedRevision };
|
|
998
1465
|
},
|
|
999
1466
|
status(value) {
|
|
@@ -1009,15 +1476,22 @@ function createParameterRuntime(options) {
|
|
|
1009
1476
|
return () => capabilityListeners.delete(fn);
|
|
1010
1477
|
},
|
|
1011
1478
|
async settled() {
|
|
1012
|
-
|
|
1479
|
+
for (; ; ) {
|
|
1480
|
+
const current = queue;
|
|
1481
|
+
await Promise.all([...refreshWork]);
|
|
1482
|
+
await queue;
|
|
1483
|
+
if (!refreshWork.size && current === queue) return;
|
|
1484
|
+
}
|
|
1013
1485
|
},
|
|
1014
1486
|
async dispose() {
|
|
1015
1487
|
if (disposed) return;
|
|
1016
1488
|
disposed = true;
|
|
1017
1489
|
controller.abort();
|
|
1490
|
+
cancelRefreshes();
|
|
1018
1491
|
listeners.clear();
|
|
1019
1492
|
capabilityListeners.clear();
|
|
1020
1493
|
await queue;
|
|
1494
|
+
await Promise.all([...refreshWork]);
|
|
1021
1495
|
await adapter?.dispose();
|
|
1022
1496
|
adapter = void 0;
|
|
1023
1497
|
}
|
|
@@ -1025,6 +1499,42 @@ function createParameterRuntime(options) {
|
|
|
1025
1499
|
}
|
|
1026
1500
|
|
|
1027
1501
|
// ../preview-runtime/src/preview.ts
|
|
1502
|
+
function decodeImage(dataUrl, signal) {
|
|
1503
|
+
return new Promise((resolve, reject) => {
|
|
1504
|
+
signal.throwIfAborted();
|
|
1505
|
+
const image = new Image();
|
|
1506
|
+
const timer = setTimeout(
|
|
1507
|
+
() => finish(new ResourceLoadError("Image decoding timed out")),
|
|
1508
|
+
15e3
|
|
1509
|
+
);
|
|
1510
|
+
const cleanup = () => {
|
|
1511
|
+
clearTimeout(timer);
|
|
1512
|
+
image.onload = image.onerror = null;
|
|
1513
|
+
signal.removeEventListener("abort", cancel);
|
|
1514
|
+
};
|
|
1515
|
+
const finish = (error) => {
|
|
1516
|
+
if (!error && (!Number.isSafeInteger(image.naturalWidth) || !Number.isSafeInteger(image.naturalHeight) || image.naturalWidth <= 0 || image.naturalHeight <= 0 || image.naturalWidth * image.naturalHeight > 32 * 1024 * 1024))
|
|
1517
|
+
error = new ResourceLoadError("Image dimensions exceed the pixel limit");
|
|
1518
|
+
cleanup();
|
|
1519
|
+
if (error) {
|
|
1520
|
+
image.src = "";
|
|
1521
|
+
reject(error);
|
|
1522
|
+
} else resolve(image);
|
|
1523
|
+
};
|
|
1524
|
+
const cancel = () => finish(signal.reason);
|
|
1525
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
1526
|
+
image.onerror = () => finish(new ResourceLoadError("Image decoding failed"));
|
|
1527
|
+
image.onload = () => {
|
|
1528
|
+
if (typeof image.decode !== "function") finish();
|
|
1529
|
+
};
|
|
1530
|
+
image.src = dataUrl;
|
|
1531
|
+
if (typeof image.decode === "function")
|
|
1532
|
+
void image.decode().then(
|
|
1533
|
+
() => finish(),
|
|
1534
|
+
() => finish(new ResourceLoadError("Image decoding failed"))
|
|
1535
|
+
);
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1028
1538
|
function readWorkspacePreviewContext(expectedContractVersion) {
|
|
1029
1539
|
try {
|
|
1030
1540
|
const mark = "playcraft-preview-storage:";
|
|
@@ -1065,7 +1575,6 @@ function connectPreviewRuntime(options) {
|
|
|
1065
1575
|
const origin = new URL(options.parentOrigin).origin;
|
|
1066
1576
|
if (origin === "null" || options.parentOrigin !== origin)
|
|
1067
1577
|
throw new ProtocolError("ORIGIN", "An exact parent origin is required");
|
|
1068
|
-
const runtime = createParameterRuntime(options);
|
|
1069
1578
|
const bridge = new Bridge({
|
|
1070
1579
|
peer: () => window.parent,
|
|
1071
1580
|
targetOrigin: origin,
|
|
@@ -1073,6 +1582,32 @@ function connectPreviewRuntime(options) {
|
|
|
1073
1582
|
strictSource: true
|
|
1074
1583
|
});
|
|
1075
1584
|
const names = BridgeMethods.runtimeParameters;
|
|
1585
|
+
const runtime = createParameterRuntime({
|
|
1586
|
+
...options,
|
|
1587
|
+
async loadImage(request, signal) {
|
|
1588
|
+
const value = await bridge.emit(names.asset, request, { timeout: 16e3, signal });
|
|
1589
|
+
if (!sameIdentity(request, value))
|
|
1590
|
+
throw new ProtocolError("STALE", "Image response belongs to a different runtime");
|
|
1591
|
+
const response = value;
|
|
1592
|
+
if (response.draftRevision !== request.draftRevision || response.documentId !== request.documentId || fingerprint(response.path) !== fingerprint(request.path) || response.value !== request.value || response.resourceSequence !== request.resourceSequence || response.expectedContentHash !== request.expectedContentHash)
|
|
1593
|
+
throw new ProtocolError("STALE", "Image response does not match its request");
|
|
1594
|
+
if (response.status === "stale") throw new SupersededResourceError();
|
|
1595
|
+
if (response.status === "failed") {
|
|
1596
|
+
if (typeof response.error !== "string")
|
|
1597
|
+
throw new ProtocolError("INVALID", "Invalid image failure response");
|
|
1598
|
+
throw new ResourceLoadError(response.error);
|
|
1599
|
+
}
|
|
1600
|
+
if (response.status !== "ready")
|
|
1601
|
+
throw new ProtocolError("INVALID", "Invalid image resource response");
|
|
1602
|
+
assertImageResourceData(response);
|
|
1603
|
+
if (request.expectedContentHash !== void 0 && response.contentHash !== request.expectedContentHash)
|
|
1604
|
+
throw new ResourceLoadError("Image resource content changed during refresh");
|
|
1605
|
+
return {
|
|
1606
|
+
image: await decodeImage(response.dataUrl, signal),
|
|
1607
|
+
contentHash: response.contentHash
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1076
1611
|
const off = [
|
|
1077
1612
|
bridge.on(names.hello, (value) => runtime.hello(value)),
|
|
1078
1613
|
bridge.on(names.snapshot, (value) => runtime.snapshot(value)),
|
|
@@ -1091,6 +1626,7 @@ function connectPreviewRuntime(options) {
|
|
|
1091
1626
|
}
|
|
1092
1627
|
}),
|
|
1093
1628
|
bridge.on(names.restart, (value) => runtime.restart(value)),
|
|
1629
|
+
bridge.on(names.refreshImages, (value) => runtime.refreshImages(value)),
|
|
1094
1630
|
bridge.on(names.status, (value) => runtime.status(value)),
|
|
1095
1631
|
bridge.on(names.dispose, async (value) => {
|
|
1096
1632
|
runtime.status(value);
|
|
@@ -1116,6 +1652,7 @@ function connectPreviewRuntime(options) {
|
|
|
1116
1652
|
};
|
|
1117
1653
|
}
|
|
1118
1654
|
export {
|
|
1655
|
+
ResourceLoadError,
|
|
1119
1656
|
connectPreviewRuntime,
|
|
1120
1657
|
readWorkspacePreviewContext
|
|
1121
1658
|
};
|