@patterkit/runtime 0.23.0 → 0.24.0
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/CHANGELOG.md +12 -0
- package/README.md +4 -0
- package/dist/index.cjs +57 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +57 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,18 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
33
33
|
|
|
34
34
|
## [Unreleased]
|
|
35
35
|
|
|
36
|
+
## [0.24.0] - 2026-10-11
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Flow capture and restore**: `engine.captureFlow(id)` takes one flow out as a `FlowState`, plain data in the
|
|
41
|
+
save's own spelling (its snapshot, a `version`, and its own values as a version 2 save's per-flow `scopes`
|
|
42
|
+
and `sceneBags`, without its id), and `engine.restoreFlow(id, state)` opens it again under any id, as
|
|
43
|
+
`loadGame` restores each flow. Shared state isn't in it, so a restore rewinds nothing else, and anything a
|
|
44
|
+
load left waiting under the id is dropped first, as when a flow starts. Refused over an open id, while a
|
|
45
|
+
checkpoint is open, and for a state with no cursor, changing nothing; capturing is allowed in a checkpoint.
|
|
46
|
+
The conformance corpus holds every runtime to it.
|
|
47
|
+
|
|
36
48
|
## [0.23.0] - 2026-10-10
|
|
37
49
|
|
|
38
50
|
### Added
|
package/README.md
CHANGED
|
@@ -110,6 +110,10 @@ value); `engine.loadGame(blob)` restores it. Saves from before property values m
|
|
|
110
110
|
registry (version 2) still load. [@patterkit/play-helpers](../play-helpers) wraps these as
|
|
111
111
|
`serializeState` / `deserializeState` with a tagged envelope.
|
|
112
112
|
|
|
113
|
+
To set one flow aside instead, `engine.captureFlow(id)` returns its part of a save as plain data (its
|
|
114
|
+
position, PRNG, visits, selectors, and its own property values, without its id); close it after capturing,
|
|
115
|
+
and `engine.restoreFlow(newId, state)` opens it again later, under any id, without rewinding shared state.
|
|
116
|
+
|
|
113
117
|
## One registry per game
|
|
114
118
|
|
|
115
119
|
A game has one `ScopeRegistry` holding every property from every engine, except values the game
|
package/dist/index.cjs
CHANGED
|
@@ -139,6 +139,7 @@ function tagsOf(host, id) {
|
|
|
139
139
|
}
|
|
140
140
|
var OWNER = "Patter";
|
|
141
141
|
var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
142
|
+
var unesc = (key) => key.replace(/%2F/g, "/").replace(/%25/g, "%");
|
|
142
143
|
var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
|
|
143
144
|
var keys = {
|
|
144
145
|
/** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
|
|
@@ -767,6 +768,45 @@ var Engine = class _Engine {
|
|
|
767
768
|
refuseInCheckpoint(what) {
|
|
768
769
|
if (this.host.journal) throw new Error(`${what} can't be undone, so it isn't allowed while a checkpoint is open`);
|
|
769
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Take flow `id` out of the engine as plain data, to be put back later with {@link restoreFlow}: its snapshot as
|
|
773
|
+
* `saveGame` writes it, and its own property values without its id (so it can come back under another). Shared
|
|
774
|
+
* state is never in it. Doesn't close the flow; close it with {@link closeFlow} AFTER capturing, since closing
|
|
775
|
+
* drops its values. Only reads, so it is allowed while a checkpoint is open.
|
|
776
|
+
*/
|
|
777
|
+
captureFlow(id) {
|
|
778
|
+
const flow = this.flowsById.get(id);
|
|
779
|
+
if (!flow) throw new Error(`no open flow '${id}' to capture`);
|
|
780
|
+
return { version: import_model3.SAVE_VERSION, ...flow.snapshot(), ...flowValuesFrom(id, this.host.registry.save()) };
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Open flow `id` from a {@link captureFlow} state, the per-flow half of `loadGame`: its values go into the
|
|
784
|
+
* registry as a load's do, and the flow is restored as a load restores each flow. `id` needn't be the id it was
|
|
785
|
+
* captured from. Against an edited bundle it re-finds its place by id, as a load does. Refused if a flow of
|
|
786
|
+
* that id is open, or while a checkpoint is.
|
|
787
|
+
*/
|
|
788
|
+
restoreFlow(id, state) {
|
|
789
|
+
this.refuseInCheckpoint("restoreFlow");
|
|
790
|
+
const version = state.version;
|
|
791
|
+
if (version !== 2 && version !== import_model3.SAVE_VERSION) throw new Error(`unsupported flow state version: ${String(version)}`);
|
|
792
|
+
const cursor = state.cursor;
|
|
793
|
+
if (typeof cursor !== "object" || cursor === null) throw new Error("malformed flow state: no cursor");
|
|
794
|
+
if (this.flowsById.has(id)) throw new Error(`a flow '${id}' is already open; close it before restoring another under that id`);
|
|
795
|
+
this.assertExternalScopes();
|
|
796
|
+
const parked = keys.flow(id);
|
|
797
|
+
this.host.registry.discardParked(parked);
|
|
798
|
+
this.host.registry.load(flowSections(id, state), { keepParked: true });
|
|
799
|
+
const flow = new Flow(id, this.host, this.defaultSeed);
|
|
800
|
+
try {
|
|
801
|
+
flow.restore(state);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
flow.close();
|
|
804
|
+
this.host.registry.discardParked(parked);
|
|
805
|
+
throw e;
|
|
806
|
+
}
|
|
807
|
+
this.flowsById.set(id, flow);
|
|
808
|
+
return flow;
|
|
809
|
+
}
|
|
770
810
|
/** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
|
|
771
811
|
* holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
|
|
772
812
|
closeFlow(id) {
|
|
@@ -2170,9 +2210,23 @@ function sectionsFromV2(save) {
|
|
|
2170
2210
|
const out = {};
|
|
2171
2211
|
if (save.shared?.patter) out["patter"] = save.shared.patter;
|
|
2172
2212
|
for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
|
|
2173
|
-
for (const [id, f] of Object.entries(save.flows ?? {}))
|
|
2174
|
-
|
|
2175
|
-
|
|
2213
|
+
for (const [id, f] of Object.entries(save.flows ?? {})) Object.assign(out, flowSections(id, f));
|
|
2214
|
+
return out;
|
|
2215
|
+
}
|
|
2216
|
+
function flowSections(id, f) {
|
|
2217
|
+
const out = {};
|
|
2218
|
+
if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
|
|
2219
|
+
for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
|
|
2220
|
+
return out;
|
|
2221
|
+
}
|
|
2222
|
+
function flowValuesFrom(id, saved) {
|
|
2223
|
+
const out = { scopes: {}, sceneBags: {} };
|
|
2224
|
+
const prefix = keys.flow(id);
|
|
2225
|
+
for (const [key, values] of Object.entries(saved)) {
|
|
2226
|
+
if (!key.startsWith(prefix)) continue;
|
|
2227
|
+
const rest = key.slice(prefix.length);
|
|
2228
|
+
if (rest === "patter") out.scopes.patter = values;
|
|
2229
|
+
else if (rest.startsWith("scene/")) out.sceneBags[unesc(rest.slice("scene/".length))] = values;
|
|
2176
2230
|
}
|
|
2177
2231
|
return out;
|
|
2178
2232
|
}
|