@playcraft/cli 0.1.56-beta → 0.1.58-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/tools-3d.js +15 -0
- package/dist/commands/tools-generation.js +3 -2
- package/dist/commands/workspace.js +43 -0
- 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/messages.js +10 -0
- package/dist/workspace-runtime/batch/deliver.js +140 -80
- package/dist/workspace-runtime/batch/freeze.js +18 -6
- package/dist/workspace-runtime/batch/generate.js +68 -64
- package/dist/workspace-runtime/batch/prepare.js +109 -137
- package/dist/workspace-runtime/batch/records.js +4 -0
- package/dist/workspace-runtime/batch/routes.js +10 -2
- package/dist/workspace-runtime/batch/source-candidate.js +2 -0
- package/dist/workspace-runtime/batch/source-capture.js +1 -0
- package/dist/workspace-runtime/batch/source-failure.js +2 -0
- package/dist/workspace-runtime/batch/source-publish.js +3 -0
- package/dist/workspace-runtime/batch/writer-proof.js +37 -0
- package/dist/workspace-runtime/codex/app-server.js +1 -0
- package/dist/workspace-runtime/codex/config-toml.js +4 -0
- 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/persistence/journal.js +34 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
- package/dist/workspace-runtime/previews/output.js +2 -0
- package/dist/workspace-runtime/previews/publish.js +20 -5
- package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
- package/dist/workspace-runtime/processes/managed-writes.js +47 -5
- package/dist/workspace-runtime/serve.js +19 -32
- package/dist/workspace-runtime/server/accept-operation.js +22 -2
- package/dist/workspace-runtime/server/directory-access.js +414 -0
- package/dist/workspace-runtime/server/dispatch.js +61 -23
- package/dist/workspace-runtime/server/endpoints.js +1 -0
- package/dist/workspace-runtime/server/http.js +52 -10
- package/dist/workspace-runtime/server/operation-record.js +2 -0
- package/dist/workspace-runtime/server/terminal-host.js +43 -8
- package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
- package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
- package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
- package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
- package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
- package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
- package/dist/workspace-runtime/workspaces/attachments.js +9 -0
- package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
- package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
- package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
- package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +105 -71
- package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
- package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
- package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
- package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
- package/dist/workspace-runtime/workspaces/files.js +4 -0
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
- package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
- package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
- package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
- package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
- package/dist/workspace-runtime/workspaces/parameters.js +95 -3
- package/dist/workspace-runtime/workspaces/platform-export.js +190 -0
- package/dist/workspace-runtime/workspaces/prepare.js +4 -0
- package/dist/workspace-runtime/workspaces/registry.js +11 -0
- package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
- package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
- package/dist/workspace-runtime/workspaces/skills.js +14 -15
- package/dist/workspace-runtime/workspaces/transfer.js +4 -1
- package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
- package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
- package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
- package/package.json +5 -4
- package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
- package/project-template-v2/.playcraft/skills.lock.json +15 -8
- package/templates/preview-runtime/v1/README.md +4 -0
- package/templates/preview-runtime/v1/manifest.json +6 -6
- package/templates/preview-runtime/v1/runtime.mjs +368 -22
- package/templates/preview-runtime/v1/types/adapters.d.ts +4 -1
- package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
- package/templates/preview-runtime/v1/types/protocol.d.ts +19 -0
- package/templates/preview-runtime/v1/types/runtime.d.ts +2 -1
|
@@ -198,6 +198,7 @@ var BridgeMethods = {
|
|
|
198
198
|
status: "runtime.parameters.status",
|
|
199
199
|
dispose: "runtime.parameters.dispose",
|
|
200
200
|
asset: "runtime.parameters.asset",
|
|
201
|
+
refreshImages: "runtime.parameters.refresh-images",
|
|
201
202
|
capabilities: "runtime.parameters.capabilities",
|
|
202
203
|
result: "runtime.parameters.result",
|
|
203
204
|
resyncRequired: "runtime.parameters.resync-required"
|
|
@@ -222,7 +223,8 @@ var BRIDGE_RPC_NAMES = [
|
|
|
222
223
|
BridgeMethods.runtimeParameters.restart,
|
|
223
224
|
BridgeMethods.runtimeParameters.status,
|
|
224
225
|
BridgeMethods.runtimeParameters.dispose,
|
|
225
|
-
BridgeMethods.runtimeParameters.asset
|
|
226
|
+
BridgeMethods.runtimeParameters.asset,
|
|
227
|
+
BridgeMethods.runtimeParameters.refreshImages
|
|
226
228
|
];
|
|
227
229
|
|
|
228
230
|
// ../iframe-bridge/src/configs/perspective.ts
|
|
@@ -637,6 +639,28 @@ var identityKeys = [
|
|
|
637
639
|
"configContractVersion",
|
|
638
640
|
"locale"
|
|
639
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
|
+
}
|
|
640
664
|
function assertImageResourceData(value) {
|
|
641
665
|
if (!record(value) || typeof value.mimeType !== "string" || typeof value.dataUrl !== "string")
|
|
642
666
|
throw new ProtocolError("INVALID", "Invalid image resource response");
|
|
@@ -738,6 +762,55 @@ function createParameterRuntime(options) {
|
|
|
738
762
|
const receipts = /* @__PURE__ */ new Map();
|
|
739
763
|
const listeners = /* @__PURE__ */ new Set();
|
|
740
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
|
+
}
|
|
741
814
|
async function release(game) {
|
|
742
815
|
try {
|
|
743
816
|
await game?.dispose();
|
|
@@ -752,6 +825,7 @@ function createParameterRuntime(options) {
|
|
|
752
825
|
...requireIdentity(),
|
|
753
826
|
stage,
|
|
754
827
|
canAcceptSnapshot: true,
|
|
828
|
+
...options.loadImage ? { canRefreshImages: true, canReportMissingImages: true } : {},
|
|
755
829
|
fields: clone(options.fields)
|
|
756
830
|
});
|
|
757
831
|
function requireIdentity() {
|
|
@@ -782,19 +856,22 @@ function createParameterRuntime(options) {
|
|
|
782
856
|
const value = result();
|
|
783
857
|
listeners.forEach((fn) => fn(value));
|
|
784
858
|
}
|
|
785
|
-
function context(config, beforeConfig, draftRevision, fields, signal) {
|
|
859
|
+
function context(config, beforeConfig, draftRevision, fields, signal, refresh) {
|
|
786
860
|
const images = /* @__PURE__ */ new Map();
|
|
861
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
787
862
|
const assertCurrent = () => {
|
|
788
863
|
signal.throwIfAborted();
|
|
789
864
|
if (signal !== controller.signal || disposed)
|
|
790
865
|
throw new ProtocolError("STALE", "Application task expired");
|
|
866
|
+
refresh?.assertCurrent();
|
|
791
867
|
if (images.size && target) {
|
|
792
868
|
const latest = options.resolve(target);
|
|
793
|
-
for (const { field, value } of images
|
|
794
|
-
if (readPath(latest, field.effectivePath) !== value)
|
|
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();
|
|
795
872
|
}
|
|
796
873
|
};
|
|
797
|
-
|
|
874
|
+
const ctx = {
|
|
798
875
|
config,
|
|
799
876
|
beforeConfig,
|
|
800
877
|
draftRevision,
|
|
@@ -810,21 +887,31 @@ function createParameterRuntime(options) {
|
|
|
810
887
|
if (!options.loadImage)
|
|
811
888
|
throw new ProtocolError("UNSUPPORTED", "Image resource transport is unavailable");
|
|
812
889
|
const key = fingerprint([registered.documentId, registered.path]);
|
|
813
|
-
|
|
890
|
+
const goal = refreshGoals.get(key);
|
|
891
|
+
images.set(key, { field: registered, value, sequence: goal?.sequence ?? 0 });
|
|
814
892
|
assertCurrent();
|
|
815
893
|
try {
|
|
816
|
-
const
|
|
894
|
+
const prepared = refresh?.prepared.get(key);
|
|
895
|
+
const response = prepared?.value === value ? prepared : await readImage(
|
|
817
896
|
{
|
|
818
897
|
...requireIdentity(),
|
|
819
898
|
draftRevision,
|
|
820
899
|
documentId: registered.documentId,
|
|
821
900
|
path: clone(registered.path),
|
|
822
|
-
value
|
|
901
|
+
value,
|
|
902
|
+
...goal?.value === value ? {
|
|
903
|
+
resourceSequence: goal.sequence,
|
|
904
|
+
...goal.contentHash !== null ? { expectedContentHash: goal.contentHash } : {}
|
|
905
|
+
} : {}
|
|
823
906
|
},
|
|
824
907
|
signal
|
|
825
908
|
);
|
|
826
909
|
assertCurrent();
|
|
827
|
-
|
|
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 } : {} });
|
|
828
915
|
return image;
|
|
829
916
|
} catch (failure) {
|
|
830
917
|
assertCurrent();
|
|
@@ -838,6 +925,8 @@ function createParameterRuntime(options) {
|
|
|
838
925
|
}
|
|
839
926
|
}
|
|
840
927
|
};
|
|
928
|
+
imageLoads.set(ctx, loaded);
|
|
929
|
+
return ctx;
|
|
841
930
|
}
|
|
842
931
|
function enqueue(work) {
|
|
843
932
|
const signal = controller.signal;
|
|
@@ -860,6 +949,7 @@ function createParameterRuntime(options) {
|
|
|
860
949
|
publish();
|
|
861
950
|
}
|
|
862
951
|
});
|
|
952
|
+
return queue;
|
|
863
953
|
}
|
|
864
954
|
function remember(kind, request) {
|
|
865
955
|
const key = `${kind}:${request.draftRevision}`;
|
|
@@ -915,15 +1005,11 @@ function createParameterRuntime(options) {
|
|
|
915
1005
|
} else status = reload ? "requires-reload" : pending ? "pending-commit" : "applying";
|
|
916
1006
|
publish();
|
|
917
1007
|
}
|
|
918
|
-
|
|
919
|
-
if (status === "error" && !recoverable) return;
|
|
920
|
-
if (!adapter || !installed)
|
|
921
|
-
throw new ProtocolError("STATE", "Initial snapshot must create the game first");
|
|
1008
|
+
function candidateFor(documents, commit, retryResources) {
|
|
922
1009
|
const operations = [];
|
|
923
1010
|
for (const field of options.fields) {
|
|
924
1011
|
if (field.mode !== "live" && !(commit && field.mode === "restart-scene")) continue;
|
|
925
|
-
|
|
926
|
-
if (resourceErrors.has(resourceKey) && !retryResources.has(resourceKey)) continue;
|
|
1012
|
+
if (resourceErrors.has(imageKey(field)) && !retryResources.has(imageKey(field))) continue;
|
|
927
1013
|
const value = readPath(documents[field.documentId], field.path);
|
|
928
1014
|
operations.push(
|
|
929
1015
|
value === void 0 ? { op: "remove", documentId: field.documentId, path: field.path } : {
|
|
@@ -934,13 +1020,29 @@ function createParameterRuntime(options) {
|
|
|
934
1020
|
}
|
|
935
1021
|
);
|
|
936
1022
|
}
|
|
937
|
-
|
|
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);
|
|
938
1030
|
const before = options.source.get();
|
|
939
1031
|
const config = freeze(options.resolve(freeze(clone(candidate))));
|
|
940
1032
|
const changed = options.fields.filter(
|
|
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))
|
|
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
|
|
942
1045
|
);
|
|
943
|
-
const ctx = context(config, before, rev, changed, signal);
|
|
944
1046
|
ctx.assertCurrent();
|
|
945
1047
|
status = "applying";
|
|
946
1048
|
publish();
|
|
@@ -953,6 +1055,7 @@ function createParameterRuntime(options) {
|
|
|
953
1055
|
ctx.assertCurrent();
|
|
954
1056
|
await adapter.afterRender(ctx);
|
|
955
1057
|
ctx.assertCurrent();
|
|
1058
|
+
commitImages(ctx);
|
|
956
1059
|
for (const field of changed)
|
|
957
1060
|
if (field.resource === "image" && readPath(config, field.effectivePath) === "")
|
|
958
1061
|
resourceErrors.delete(fingerprint([field.documentId, field.path]));
|
|
@@ -962,6 +1065,18 @@ function createParameterRuntime(options) {
|
|
|
962
1065
|
if (!signal.aborted) {
|
|
963
1066
|
options.source.install(before);
|
|
964
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
|
+
}
|
|
965
1080
|
}
|
|
966
1081
|
throw e;
|
|
967
1082
|
}
|
|
@@ -989,6 +1104,10 @@ function createParameterRuntime(options) {
|
|
|
989
1104
|
cleanupFailure = e;
|
|
990
1105
|
});
|
|
991
1106
|
identity = freeze({ ...clone(value), runtimeInstanceId });
|
|
1107
|
+
cancelRefreshes();
|
|
1108
|
+
installedImages.clear();
|
|
1109
|
+
refreshReceipts.clear();
|
|
1110
|
+
resourceSequence = 0;
|
|
992
1111
|
target = installed = void 0;
|
|
993
1112
|
receivedRevision = installedRevision = appliedRevision = -1;
|
|
994
1113
|
receipts.clear();
|
|
@@ -1016,6 +1135,8 @@ function createParameterRuntime(options) {
|
|
|
1016
1135
|
const rev = value.draftRevision;
|
|
1017
1136
|
const documents = freeze(clone(value.documents));
|
|
1018
1137
|
options.resolve(documents);
|
|
1138
|
+
cancelRefreshes();
|
|
1139
|
+
installedImages.clear();
|
|
1019
1140
|
target = documents;
|
|
1020
1141
|
const receipt = accept("snapshot", value);
|
|
1021
1142
|
enqueue(async (signal) => {
|
|
@@ -1054,6 +1175,7 @@ function createParameterRuntime(options) {
|
|
|
1054
1175
|
capabilityListeners.forEach((fn) => fn(capabilities()));
|
|
1055
1176
|
await created.afterRender(ctx);
|
|
1056
1177
|
ctx.assertCurrent();
|
|
1178
|
+
commitImages(ctx);
|
|
1057
1179
|
installed = clone(nextDocuments);
|
|
1058
1180
|
if (resourceErrors.size) installedRevision = -1;
|
|
1059
1181
|
error = void 0;
|
|
@@ -1099,11 +1221,222 @@ function createParameterRuntime(options) {
|
|
|
1099
1221
|
const rev = value.draftRevision, commit = value.phase === "commit";
|
|
1100
1222
|
options.resolve(documents);
|
|
1101
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
|
+
}
|
|
1102
1234
|
const receipt = accept("patch", value);
|
|
1103
1235
|
const retryResources = new Set(
|
|
1104
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]))
|
|
1105
1237
|
);
|
|
1106
|
-
enqueue(
|
|
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));
|
|
1107
1440
|
return receipt;
|
|
1108
1441
|
},
|
|
1109
1442
|
restart(value) {
|
|
@@ -1143,15 +1476,22 @@ function createParameterRuntime(options) {
|
|
|
1143
1476
|
return () => capabilityListeners.delete(fn);
|
|
1144
1477
|
},
|
|
1145
1478
|
async settled() {
|
|
1146
|
-
|
|
1479
|
+
for (; ; ) {
|
|
1480
|
+
const current = queue;
|
|
1481
|
+
await Promise.all([...refreshWork]);
|
|
1482
|
+
await queue;
|
|
1483
|
+
if (!refreshWork.size && current === queue) return;
|
|
1484
|
+
}
|
|
1147
1485
|
},
|
|
1148
1486
|
async dispose() {
|
|
1149
1487
|
if (disposed) return;
|
|
1150
1488
|
disposed = true;
|
|
1151
1489
|
controller.abort();
|
|
1490
|
+
cancelRefreshes();
|
|
1152
1491
|
listeners.clear();
|
|
1153
1492
|
capabilityListeners.clear();
|
|
1154
1493
|
await queue;
|
|
1494
|
+
await Promise.all([...refreshWork]);
|
|
1155
1495
|
await adapter?.dispose();
|
|
1156
1496
|
adapter = void 0;
|
|
1157
1497
|
}
|
|
@@ -1249,7 +1589,7 @@ function connectPreviewRuntime(options) {
|
|
|
1249
1589
|
if (!sameIdentity(request, value))
|
|
1250
1590
|
throw new ProtocolError("STALE", "Image response belongs to a different runtime");
|
|
1251
1591
|
const response = value;
|
|
1252
|
-
if (response.draftRevision !== request.draftRevision || response.documentId !== request.documentId || fingerprint(response.path) !== fingerprint(request.path) || response.value !== request.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)
|
|
1253
1593
|
throw new ProtocolError("STALE", "Image response does not match its request");
|
|
1254
1594
|
if (response.status === "stale") throw new SupersededResourceError();
|
|
1255
1595
|
if (response.status === "failed") {
|
|
@@ -1260,7 +1600,12 @@ function connectPreviewRuntime(options) {
|
|
|
1260
1600
|
if (response.status !== "ready")
|
|
1261
1601
|
throw new ProtocolError("INVALID", "Invalid image resource response");
|
|
1262
1602
|
assertImageResourceData(response);
|
|
1263
|
-
|
|
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
|
+
};
|
|
1264
1609
|
}
|
|
1265
1610
|
});
|
|
1266
1611
|
const off = [
|
|
@@ -1281,6 +1626,7 @@ function connectPreviewRuntime(options) {
|
|
|
1281
1626
|
}
|
|
1282
1627
|
}),
|
|
1283
1628
|
bridge.on(names.restart, (value) => runtime.restart(value)),
|
|
1629
|
+
bridge.on(names.refreshImages, (value) => runtime.refreshImages(value)),
|
|
1284
1630
|
bridge.on(names.status, (value) => runtime.status(value)),
|
|
1285
1631
|
bridge.on(names.dispose, async (value) => {
|
|
1286
1632
|
runtime.status(value);
|
|
@@ -24,5 +24,8 @@ export interface RuntimeOptions<C> {
|
|
|
24
24
|
install(config: C): unknown;
|
|
25
25
|
};
|
|
26
26
|
createGame(context: ApplyContext<C>): Promise<GameAdapter<C>>;
|
|
27
|
-
loadImage?(request: ImageResourceRequest, signal: AbortSignal): Promise<HTMLImageElement
|
|
27
|
+
loadImage?(request: ImageResourceRequest, signal: AbortSignal): Promise<HTMLImageElement | {
|
|
28
|
+
image: HTMLImageElement;
|
|
29
|
+
contentHash?: string;
|
|
30
|
+
}>;
|
|
28
31
|
}
|
|
@@ -13,6 +13,7 @@ export declare function connectPreviewRuntime<C>(options: RuntimeOptions<C> & {
|
|
|
13
13
|
hello(value: unknown): import("./protocol.js").Capabilities;
|
|
14
14
|
snapshot(value: unknown): import("./protocol.js").Receipt;
|
|
15
15
|
patch(value: unknown): import("./protocol.js").Receipt;
|
|
16
|
+
refreshImages(value: unknown): import("./protocol.js").ImageRefreshReceipt;
|
|
16
17
|
restart(value: unknown): import("./protocol.js").Receipt;
|
|
17
18
|
status(value: unknown): import("./protocol.js").ApplicationResult;
|
|
18
19
|
subscribe(fn: (result: import("./protocol.js").ApplicationResult) => void): () => boolean;
|
|
@@ -36,6 +36,8 @@ export interface Capabilities extends Identity {
|
|
|
36
36
|
stage: 'bootstrap-ready' | 'game-ready';
|
|
37
37
|
canAcceptSnapshot: boolean;
|
|
38
38
|
fields: FieldCapability[];
|
|
39
|
+
canRefreshImages?: true;
|
|
40
|
+
canReportMissingImages?: true;
|
|
39
41
|
}
|
|
40
42
|
export type Operation = {
|
|
41
43
|
op: 'set';
|
|
@@ -61,6 +63,20 @@ export interface Receipt {
|
|
|
61
63
|
accepted: true;
|
|
62
64
|
receivedRevision: number;
|
|
63
65
|
}
|
|
66
|
+
export type ImageRefreshTarget = {
|
|
67
|
+
documentId: string;
|
|
68
|
+
path: string[];
|
|
69
|
+
/** Null reports a confirmed missing file; it does not clear the configured image. */
|
|
70
|
+
contentHash: string | null;
|
|
71
|
+
};
|
|
72
|
+
export interface ImageRefreshRequest extends Identity {
|
|
73
|
+
draftRevision: number;
|
|
74
|
+
resourceSequence: number;
|
|
75
|
+
images: ImageRefreshTarget[];
|
|
76
|
+
}
|
|
77
|
+
export interface ImageRefreshReceipt extends Receipt {
|
|
78
|
+
resourceSequence: number;
|
|
79
|
+
}
|
|
64
80
|
export interface GroupResult {
|
|
65
81
|
groupId: string;
|
|
66
82
|
appliedRevision: number;
|
|
@@ -86,6 +102,8 @@ export interface ImageResourceRequest extends Identity {
|
|
|
86
102
|
documentId: string;
|
|
87
103
|
path: string[];
|
|
88
104
|
value: string;
|
|
105
|
+
resourceSequence?: number;
|
|
106
|
+
expectedContentHash?: string;
|
|
89
107
|
}
|
|
90
108
|
export type ImageResourceData = {
|
|
91
109
|
mimeType: string;
|
|
@@ -124,6 +142,7 @@ export declare function sameIdentity(a: Identity, b: unknown): b is Identity;
|
|
|
124
142
|
export declare function assertBinding(expected: SessionBinding, actual: Hello): void;
|
|
125
143
|
export declare function assertDocuments(value: unknown): asserts value is RawDocuments;
|
|
126
144
|
export declare function assertImageResourceRequest(value: unknown): asserts value is ImageResourceRequest;
|
|
145
|
+
export declare function assertImageRefresh(value: unknown): asserts value is ImageRefreshRequest;
|
|
127
146
|
export declare function assertImageResourceData(value: unknown): asserts value is ImageResourceData;
|
|
128
147
|
export declare function assertSnapshot(value: unknown): asserts value is SnapshotRequest;
|
|
129
148
|
export declare function assertPatch(value: unknown): asserts value is PatchRequest;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { RuntimeOptions } from './adapters.js';
|
|
2
|
-
import type { ApplicationResult, Capabilities, Receipt } from './protocol.js';
|
|
2
|
+
import type { ApplicationResult, Capabilities, ImageRefreshReceipt, Receipt } from './protocol.js';
|
|
3
3
|
export declare function createParameterRuntime<C>(options: RuntimeOptions<C>): {
|
|
4
4
|
hello(value: unknown): Capabilities;
|
|
5
5
|
snapshot(value: unknown): Receipt;
|
|
6
6
|
patch(value: unknown): Receipt;
|
|
7
|
+
refreshImages(value: unknown): ImageRefreshReceipt;
|
|
7
8
|
restart(value: unknown): Receipt;
|
|
8
9
|
status(value: unknown): ApplicationResult;
|
|
9
10
|
subscribe(fn: (result: ApplicationResult) => void): () => boolean;
|