@storylet-studio/play-helpers 0.4.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/README.md +55 -0
- package/dist/index.cjs +1043 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +251 -0
- package/dist/index.d.ts +251 -0
- package/dist/index.js +999 -0
- package/dist/index.js.map +1 -0
- package/dist/storyletengine.min.js +33 -0
- package/dist/storyletengine.min.js.map +1 -0
- package/package.json +39 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Engine, Flow, LogEntry, EngineLogEntry, BundleDescription, PropertySummary, PropertyScopeSummary, TraceEvent, EngineOptions } from '@storylet-studio/runtime';
|
|
2
|
+
import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag as PropertyBag$1 } from '@wildwinter/scoperegistry';
|
|
3
|
+
export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot, createStateLogger as createKernelStateLogger, diffState } from '@wildwinter/scoperegistry';
|
|
4
|
+
import { PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
|
|
5
|
+
|
|
6
|
+
/** The full flattened snapshot of ONE FLOW's view - the shared partitions
|
|
7
|
+
* plus that flow's own - straight off the save envelope, so "what the
|
|
8
|
+
* snapshot sees" is by construction "what a save persists". @world is not
|
|
9
|
+
* here for the same reason it is not in the envelope: the host owns that
|
|
10
|
+
* container and mounts/saves it itself (createWorldContainer). */
|
|
11
|
+
declare function snapshotState(engine: Engine, flow: Flow): StateSnapshot;
|
|
12
|
+
/** The storylets state logger: the kernel core mounted on the SHARED bags
|
|
13
|
+
* (engine.listBags()) and one flow's own (flow.listBags()) - the same
|
|
14
|
+
* prefixes, one path space, names disjoint - plus the flow's turns /
|
|
15
|
+
* cooldowns / board adapter. A host that wants @world lines mounts its
|
|
16
|
+
* world container's bag through createKernelStateLogger itself. */
|
|
17
|
+
declare function createStateLogger(engine: Engine, flow: Flow, opts?: StateLoggerOptions): StateLogger;
|
|
18
|
+
|
|
19
|
+
/** The current engine state (and the host's @world values, if given) as
|
|
20
|
+
* pretty-printed .storyletsave JSON. */
|
|
21
|
+
declare function serializeState(engine: Engine, world?: PropertyBag): string;
|
|
22
|
+
/**
|
|
23
|
+
* Capture the whole engine (and the host's @world values, if it keeps any) as
|
|
24
|
+
* the tagged save-file OBJECT.
|
|
25
|
+
*
|
|
26
|
+
* Four verbs, in Patterplay's pairing (`patter` play-helpers `save.ts`, and
|
|
27
|
+
* the same in all four of its runtimes): saveState / loadState work on the
|
|
28
|
+
* PARSED object, serializeState / deserializeState work on TEXT.
|
|
29
|
+
*
|
|
30
|
+
* This reference had a different shape until 2026-08-29 - `deserializeState`
|
|
31
|
+
* parsed and did not restore, `loadState` took text - so one name meant two
|
|
32
|
+
* things across the four Storylets runtimes, and neither matched the family.
|
|
33
|
+
* Godot and Unreal already had Patter's shape; these two were brought to it.
|
|
34
|
+
*/
|
|
35
|
+
declare function saveState(engine: Engine, world?: PropertyBag): SaveFile;
|
|
36
|
+
/** Restore a {@link saveState} file into an engine. EVERY FLOW IS REBUILT, so
|
|
37
|
+
* the Flow handles you held before are inert: re-take them with
|
|
38
|
+
* `engine.getFlow(id)`, NOT `engine.openFlow(id)`. `openFlow` on an existing
|
|
39
|
+
* id REPLACES it, which here throws away the hand the file just restored, and
|
|
40
|
+
* the failure lands later, as `play()` refusing a card as "not dealt". (The
|
|
41
|
+
* engine's `onReplacedFlow` hook reports exactly this.) Throws on a foreign or malformed
|
|
42
|
+
* file, and the runtime's own project check still applies. Returns the file's
|
|
43
|
+
* @world values, if any - the HOST applies them to its container; the engine
|
|
44
|
+
* never touches them. */
|
|
45
|
+
declare function loadState(engine: Engine, file: SaveFile): PropertyBag | undefined;
|
|
46
|
+
/** Parse + restore a {@link serializeState} string: the TEXT twin of
|
|
47
|
+
* loadState, as Patterplay pairs them. Throws on malformed JSON, a foreign
|
|
48
|
+
* file or a project mismatch. Returns the file's @world values for the host. */
|
|
49
|
+
declare function deserializeState(engine: Engine, json: string): PropertyBag | undefined;
|
|
50
|
+
|
|
51
|
+
interface PropertyInspectorOptions {
|
|
52
|
+
/** Mount point; defaults to document.body. */
|
|
53
|
+
container?: HTMLElement;
|
|
54
|
+
title?: string;
|
|
55
|
+
/** Value-refresh poll; 0 disables polling. */
|
|
56
|
+
pollMs?: number;
|
|
57
|
+
}
|
|
58
|
+
interface PropertyInspector {
|
|
59
|
+
el: HTMLElement;
|
|
60
|
+
refresh(): void;
|
|
61
|
+
destroy(): void;
|
|
62
|
+
}
|
|
63
|
+
/** Inject the shared panel stylesheet once. Exported so the bundle inspector
|
|
64
|
+
* (bundle-inspector.ts) renders in the same CSS grammar. */
|
|
65
|
+
declare function ensureInspectorStyle(): void;
|
|
66
|
+
/** One line per entry, `[turn]`-stamped where the event has a box context
|
|
67
|
+
* (write lines share the state logger's `path: from -> to` reading). */
|
|
68
|
+
declare function formatLogEntry(e: LogEntry | EngineLogEntry): string;
|
|
69
|
+
declare function createPropertyInspector(engine: Engine, flow: Flow, opts?: PropertyInspectorOptions): PropertyInspector;
|
|
70
|
+
|
|
71
|
+
interface BundleInspectorOptions {
|
|
72
|
+
/** Mount point; defaults to document.body. */
|
|
73
|
+
container?: HTMLElement;
|
|
74
|
+
title?: string;
|
|
75
|
+
/** Start the collapsible sections open (default true). */
|
|
76
|
+
open?: boolean;
|
|
77
|
+
}
|
|
78
|
+
interface BundleInspector {
|
|
79
|
+
el: HTMLElement;
|
|
80
|
+
/** The description this panel rendered (the API is the parity member). */
|
|
81
|
+
description: BundleDescription;
|
|
82
|
+
destroy(): void;
|
|
83
|
+
}
|
|
84
|
+
/** "name: type = default", plus enum/flags options where declared. */
|
|
85
|
+
declare function formatPropertySummary(p: PropertySummary): string;
|
|
86
|
+
/** The scope label a declaration block files under ("world", "box box",
|
|
87
|
+
* "tag docks (zone)"). */
|
|
88
|
+
declare function formatScopeLabel(scope: PropertyScopeSummary): string;
|
|
89
|
+
/** Render a read-only summary of a compiled bundle: what an integrator may
|
|
90
|
+
* call, with no session and no game running. */
|
|
91
|
+
declare function createBundleInspector(bundle: Bundle, opts?: BundleInspectorOptions): BundleInspector;
|
|
92
|
+
|
|
93
|
+
/** A minimal structural type for a WebSocket implementation (browsers and
|
|
94
|
+
* Node 22+ have a global one). */
|
|
95
|
+
interface LiveSocketLike {
|
|
96
|
+
readyState: number;
|
|
97
|
+
send(data: string): void;
|
|
98
|
+
close(): void;
|
|
99
|
+
addEventListener(type: "open" | "close" | "error", listener: () => void): void;
|
|
100
|
+
/** Incoming editor messages (the pushed bundle). Optional so a bare
|
|
101
|
+
* send-only socket still fits. */
|
|
102
|
+
addEventListener(type: "message", listener: (ev: {
|
|
103
|
+
data: unknown;
|
|
104
|
+
}) => void): void;
|
|
105
|
+
}
|
|
106
|
+
type LiveSocketCtor = new (url: string) => LiveSocketLike;
|
|
107
|
+
interface LiveLinkOptions {
|
|
108
|
+
/** The running bundle's build identity: pass `bundle.content.hash`. The
|
|
109
|
+
* editor compares it with its own compiled hash (in sync / stale). */
|
|
110
|
+
build: string;
|
|
111
|
+
/** Optional project name, shown in the editor's connect-chip tooltip. */
|
|
112
|
+
project?: string;
|
|
113
|
+
/** Editor WebSocket URL. Default `ws://127.0.0.1:4472`. */
|
|
114
|
+
url?: string;
|
|
115
|
+
/** A WebSocket constructor to use instead of the global one (tests with a
|
|
116
|
+
* fake socket, or a host without a global WebSocket). */
|
|
117
|
+
WebSocket?: LiveSocketCtor;
|
|
118
|
+
/** Live refresh: the editor pushed a freshly compiled bundle. `data` is
|
|
119
|
+
* the .storyletsc JSON; hand it (with your current Engine) to
|
|
120
|
+
* `applyLiveBundle`, `attach` the engine it returns, then call
|
|
121
|
+
* `link.setBuild(build)`. Never called with a malformed frame. */
|
|
122
|
+
onBundle?: (msg: {
|
|
123
|
+
build: string;
|
|
124
|
+
data: string;
|
|
125
|
+
}) => void;
|
|
126
|
+
}
|
|
127
|
+
interface LiveLink {
|
|
128
|
+
/** Start forwarding this ENGINE's trace: every flow's events, each frame
|
|
129
|
+
* naming the flow it came from, so the editor can follow one participant
|
|
130
|
+
* and switch. An earlier engine is detached first. Sends a board snapshot
|
|
131
|
+
* per open flow straight away, queued behind the hello if the socket is
|
|
132
|
+
* not open yet.
|
|
133
|
+
*
|
|
134
|
+
* Flows are discovered rather than declared: the link diffs `engine.flows()`
|
|
135
|
+
* whenever anything happens and emits `flowOpen` / `flowClose` itself. That
|
|
136
|
+
* is a deliberate departure from Patterplay, whose host calls `FlowOpened`
|
|
137
|
+
* by hand - it has no engine-level trace tap to hang the diff on and we do,
|
|
138
|
+
* so the host has nothing to remember and cannot get the editor's flow list
|
|
139
|
+
* wrong. The one cost: a flow that opens and then does nothing at all is not
|
|
140
|
+
* announced until the next event anywhere in the run. */
|
|
141
|
+
attach(engine: Engine): void;
|
|
142
|
+
/** Stop forwarding. A refresh replaces the engine, so attach the new one
|
|
143
|
+
* afterwards. */
|
|
144
|
+
detach(): void;
|
|
145
|
+
/** After applying a pushed bundle: report the build now running (re-hellos
|
|
146
|
+
* with the new build and a fresh board snapshot, so the editor's chip goes
|
|
147
|
+
* back to in sync and it stops re-pushing the same bundle). */
|
|
148
|
+
setBuild(build: string): void;
|
|
149
|
+
/** Close the link; every later call is a no-op. */
|
|
150
|
+
close(): void;
|
|
151
|
+
}
|
|
152
|
+
/** One game-to-editor frame, as the client serialises it. Exported for the
|
|
153
|
+
* fixture test; hosts never build these by hand. */
|
|
154
|
+
type LiveFrame = {
|
|
155
|
+
t: "hello";
|
|
156
|
+
v: 2;
|
|
157
|
+
build: string;
|
|
158
|
+
project?: string;
|
|
159
|
+
boxes?: string[];
|
|
160
|
+
flows: string[];
|
|
161
|
+
} | {
|
|
162
|
+
t: "flowOpen";
|
|
163
|
+
flow: string;
|
|
164
|
+
} | {
|
|
165
|
+
t: "flowClose";
|
|
166
|
+
flow: string;
|
|
167
|
+
} | {
|
|
168
|
+
t: "trace";
|
|
169
|
+
flow: string;
|
|
170
|
+
event: TraceEvent;
|
|
171
|
+
} | {
|
|
172
|
+
t: "board";
|
|
173
|
+
flow: string;
|
|
174
|
+
hands: Record<string, string[]>;
|
|
175
|
+
turns: Record<string, number>;
|
|
176
|
+
};
|
|
177
|
+
/** The cheap snapshot: hands by gameId holding card gameIds in dealt order,
|
|
178
|
+
* and every box's clock by gameId. */
|
|
179
|
+
declare function boardFrame(flow: Flow): Extract<LiveFrame, {
|
|
180
|
+
t: "board";
|
|
181
|
+
}>;
|
|
182
|
+
/**
|
|
183
|
+
* Open a Live Link to Storyletter. Returns a handle whose calls are no-ops once
|
|
184
|
+
* the editor disconnects or if it was never listening: safe to leave wired into
|
|
185
|
+
* a shipping build behind a flag.
|
|
186
|
+
*/
|
|
187
|
+
declare function createLiveLink(opts: LiveLinkOptions): LiveLink;
|
|
188
|
+
|
|
189
|
+
type LiveBundleResult =
|
|
190
|
+
/** The new engine, carrying the old one's run (all flows), and the bundle
|
|
191
|
+
* it runs. */
|
|
192
|
+
{
|
|
193
|
+
ok: true;
|
|
194
|
+
engine: Engine;
|
|
195
|
+
bundle: Bundle;
|
|
196
|
+
}
|
|
197
|
+
/** Nothing changed: keep the engine you have. */
|
|
198
|
+
| {
|
|
199
|
+
ok: false;
|
|
200
|
+
error: string;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Apply a bundle the editor pushed over the Live Link: `new Engine(parsed,
|
|
204
|
+
* opts)` then `loadGame(engine.saveGame())`, returning the new engine. Never
|
|
205
|
+
* throws; a failure (unparseable JSON, a bundle the runtime rejects, a
|
|
206
|
+
* different project) comes back as `{ ok: false, error }` and the old engine
|
|
207
|
+
* is untouched.
|
|
208
|
+
*
|
|
209
|
+
* `opts` are the options the old engine was created with. An engine does
|
|
210
|
+
* not expose its seed, and it does not matter here: the save envelope carries
|
|
211
|
+
* each flow's PRNG state, so `loadGame` resumes the draw sequences exactly
|
|
212
|
+
* where they were and `seed` only shapes fresh flows. `log` does matter (the
|
|
213
|
+
* retained log is per flow), and so does `world` (the host's binding does not
|
|
214
|
+
* ride the envelope), so pass them if you had them.
|
|
215
|
+
*/
|
|
216
|
+
declare function applyLiveBundle(engine: Engine, bundleJson: string, opts?: EngineOptions): LiveBundleResult;
|
|
217
|
+
|
|
218
|
+
type ScalarValue = boolean | number | string | string[];
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A scope backed by a host resolver rather than a static bag - the basis for
|
|
222
|
+
* *foreign* scopes (e.g. `@game` / `@world`) whose values live in a host or
|
|
223
|
+
* another engine and are read (and optionally written) at runtime. The
|
|
224
|
+
* evaluator treats a missing property the same as a bag does (the scope's
|
|
225
|
+
* missing-policy decides false-vs-throw). A scope entirely absent from the
|
|
226
|
+
* EvalContext still resolves to false regardless.
|
|
227
|
+
*/
|
|
228
|
+
interface ScopeResolver {
|
|
229
|
+
/** Read a property's value, or undefined if the scope does not have it. */
|
|
230
|
+
get(name: string): ScalarValue | undefined;
|
|
231
|
+
/** Write a property (omit for a read-only scope). The core never calls this;
|
|
232
|
+
* it is for host/runtime effect application (e.g. a state container's `set`). */
|
|
233
|
+
set?(name: string, value: ScalarValue): void;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
interface WorldContainer {
|
|
237
|
+
/** Pass as `new Engine(bundle, { world: container.resolver })`. */
|
|
238
|
+
resolver: ScopeResolver;
|
|
239
|
+
/** The kernel bag itself (subscribe, audit, rows live there) - mount it
|
|
240
|
+
* into a state logger or examiner beside the engine's own bags. */
|
|
241
|
+
bag: PropertyBag$1;
|
|
242
|
+
/** The current values, for saving beside the engine's envelope. */
|
|
243
|
+
values(): PropertyBag;
|
|
244
|
+
/** Restore saved values over fresh defaults: orphaned keys drop, new
|
|
245
|
+
* declarations keep their defaults - the same drift rule as loadGame. */
|
|
246
|
+
load(values: PropertyBag): void;
|
|
247
|
+
}
|
|
248
|
+
/** A world container seeded from the bundle's @world declarations. */
|
|
249
|
+
declare function createWorldContainer(bundle: Bundle): WorldContainer;
|
|
250
|
+
|
|
251
|
+
export { type BundleInspector, type BundleInspectorOptions, type LiveBundleResult, type LiveFrame, type LiveLink, type LiveLinkOptions, type LiveSocketLike, type PropertyInspector, type PropertyInspectorOptions, type WorldContainer, applyLiveBundle, boardFrame, createBundleInspector, createLiveLink, createPropertyInspector, createStateLogger, createWorldContainer, deserializeState, ensureInspectorStyle, formatLogEntry, formatPropertySummary, formatScopeLabel, loadState, saveState, serializeState, snapshotState };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Engine, Flow, LogEntry, EngineLogEntry, BundleDescription, PropertySummary, PropertyScopeSummary, TraceEvent, EngineOptions } from '@storylet-studio/runtime';
|
|
2
|
+
import { StateLoggerOptions, StateLogger, StateSnapshot, PropertyBag as PropertyBag$1 } from '@wildwinter/scoperegistry';
|
|
3
|
+
export { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot, createStateLogger as createKernelStateLogger, diffState } from '@wildwinter/scoperegistry';
|
|
4
|
+
import { PropertyBag, SaveFile, Bundle } from '@storylet-studio/model';
|
|
5
|
+
|
|
6
|
+
/** The full flattened snapshot of ONE FLOW's view - the shared partitions
|
|
7
|
+
* plus that flow's own - straight off the save envelope, so "what the
|
|
8
|
+
* snapshot sees" is by construction "what a save persists". @world is not
|
|
9
|
+
* here for the same reason it is not in the envelope: the host owns that
|
|
10
|
+
* container and mounts/saves it itself (createWorldContainer). */
|
|
11
|
+
declare function snapshotState(engine: Engine, flow: Flow): StateSnapshot;
|
|
12
|
+
/** The storylets state logger: the kernel core mounted on the SHARED bags
|
|
13
|
+
* (engine.listBags()) and one flow's own (flow.listBags()) - the same
|
|
14
|
+
* prefixes, one path space, names disjoint - plus the flow's turns /
|
|
15
|
+
* cooldowns / board adapter. A host that wants @world lines mounts its
|
|
16
|
+
* world container's bag through createKernelStateLogger itself. */
|
|
17
|
+
declare function createStateLogger(engine: Engine, flow: Flow, opts?: StateLoggerOptions): StateLogger;
|
|
18
|
+
|
|
19
|
+
/** The current engine state (and the host's @world values, if given) as
|
|
20
|
+
* pretty-printed .storyletsave JSON. */
|
|
21
|
+
declare function serializeState(engine: Engine, world?: PropertyBag): string;
|
|
22
|
+
/**
|
|
23
|
+
* Capture the whole engine (and the host's @world values, if it keeps any) as
|
|
24
|
+
* the tagged save-file OBJECT.
|
|
25
|
+
*
|
|
26
|
+
* Four verbs, in Patterplay's pairing (`patter` play-helpers `save.ts`, and
|
|
27
|
+
* the same in all four of its runtimes): saveState / loadState work on the
|
|
28
|
+
* PARSED object, serializeState / deserializeState work on TEXT.
|
|
29
|
+
*
|
|
30
|
+
* This reference had a different shape until 2026-08-29 - `deserializeState`
|
|
31
|
+
* parsed and did not restore, `loadState` took text - so one name meant two
|
|
32
|
+
* things across the four Storylets runtimes, and neither matched the family.
|
|
33
|
+
* Godot and Unreal already had Patter's shape; these two were brought to it.
|
|
34
|
+
*/
|
|
35
|
+
declare function saveState(engine: Engine, world?: PropertyBag): SaveFile;
|
|
36
|
+
/** Restore a {@link saveState} file into an engine. EVERY FLOW IS REBUILT, so
|
|
37
|
+
* the Flow handles you held before are inert: re-take them with
|
|
38
|
+
* `engine.getFlow(id)`, NOT `engine.openFlow(id)`. `openFlow` on an existing
|
|
39
|
+
* id REPLACES it, which here throws away the hand the file just restored, and
|
|
40
|
+
* the failure lands later, as `play()` refusing a card as "not dealt". (The
|
|
41
|
+
* engine's `onReplacedFlow` hook reports exactly this.) Throws on a foreign or malformed
|
|
42
|
+
* file, and the runtime's own project check still applies. Returns the file's
|
|
43
|
+
* @world values, if any - the HOST applies them to its container; the engine
|
|
44
|
+
* never touches them. */
|
|
45
|
+
declare function loadState(engine: Engine, file: SaveFile): PropertyBag | undefined;
|
|
46
|
+
/** Parse + restore a {@link serializeState} string: the TEXT twin of
|
|
47
|
+
* loadState, as Patterplay pairs them. Throws on malformed JSON, a foreign
|
|
48
|
+
* file or a project mismatch. Returns the file's @world values for the host. */
|
|
49
|
+
declare function deserializeState(engine: Engine, json: string): PropertyBag | undefined;
|
|
50
|
+
|
|
51
|
+
interface PropertyInspectorOptions {
|
|
52
|
+
/** Mount point; defaults to document.body. */
|
|
53
|
+
container?: HTMLElement;
|
|
54
|
+
title?: string;
|
|
55
|
+
/** Value-refresh poll; 0 disables polling. */
|
|
56
|
+
pollMs?: number;
|
|
57
|
+
}
|
|
58
|
+
interface PropertyInspector {
|
|
59
|
+
el: HTMLElement;
|
|
60
|
+
refresh(): void;
|
|
61
|
+
destroy(): void;
|
|
62
|
+
}
|
|
63
|
+
/** Inject the shared panel stylesheet once. Exported so the bundle inspector
|
|
64
|
+
* (bundle-inspector.ts) renders in the same CSS grammar. */
|
|
65
|
+
declare function ensureInspectorStyle(): void;
|
|
66
|
+
/** One line per entry, `[turn]`-stamped where the event has a box context
|
|
67
|
+
* (write lines share the state logger's `path: from -> to` reading). */
|
|
68
|
+
declare function formatLogEntry(e: LogEntry | EngineLogEntry): string;
|
|
69
|
+
declare function createPropertyInspector(engine: Engine, flow: Flow, opts?: PropertyInspectorOptions): PropertyInspector;
|
|
70
|
+
|
|
71
|
+
interface BundleInspectorOptions {
|
|
72
|
+
/** Mount point; defaults to document.body. */
|
|
73
|
+
container?: HTMLElement;
|
|
74
|
+
title?: string;
|
|
75
|
+
/** Start the collapsible sections open (default true). */
|
|
76
|
+
open?: boolean;
|
|
77
|
+
}
|
|
78
|
+
interface BundleInspector {
|
|
79
|
+
el: HTMLElement;
|
|
80
|
+
/** The description this panel rendered (the API is the parity member). */
|
|
81
|
+
description: BundleDescription;
|
|
82
|
+
destroy(): void;
|
|
83
|
+
}
|
|
84
|
+
/** "name: type = default", plus enum/flags options where declared. */
|
|
85
|
+
declare function formatPropertySummary(p: PropertySummary): string;
|
|
86
|
+
/** The scope label a declaration block files under ("world", "box box",
|
|
87
|
+
* "tag docks (zone)"). */
|
|
88
|
+
declare function formatScopeLabel(scope: PropertyScopeSummary): string;
|
|
89
|
+
/** Render a read-only summary of a compiled bundle: what an integrator may
|
|
90
|
+
* call, with no session and no game running. */
|
|
91
|
+
declare function createBundleInspector(bundle: Bundle, opts?: BundleInspectorOptions): BundleInspector;
|
|
92
|
+
|
|
93
|
+
/** A minimal structural type for a WebSocket implementation (browsers and
|
|
94
|
+
* Node 22+ have a global one). */
|
|
95
|
+
interface LiveSocketLike {
|
|
96
|
+
readyState: number;
|
|
97
|
+
send(data: string): void;
|
|
98
|
+
close(): void;
|
|
99
|
+
addEventListener(type: "open" | "close" | "error", listener: () => void): void;
|
|
100
|
+
/** Incoming editor messages (the pushed bundle). Optional so a bare
|
|
101
|
+
* send-only socket still fits. */
|
|
102
|
+
addEventListener(type: "message", listener: (ev: {
|
|
103
|
+
data: unknown;
|
|
104
|
+
}) => void): void;
|
|
105
|
+
}
|
|
106
|
+
type LiveSocketCtor = new (url: string) => LiveSocketLike;
|
|
107
|
+
interface LiveLinkOptions {
|
|
108
|
+
/** The running bundle's build identity: pass `bundle.content.hash`. The
|
|
109
|
+
* editor compares it with its own compiled hash (in sync / stale). */
|
|
110
|
+
build: string;
|
|
111
|
+
/** Optional project name, shown in the editor's connect-chip tooltip. */
|
|
112
|
+
project?: string;
|
|
113
|
+
/** Editor WebSocket URL. Default `ws://127.0.0.1:4472`. */
|
|
114
|
+
url?: string;
|
|
115
|
+
/** A WebSocket constructor to use instead of the global one (tests with a
|
|
116
|
+
* fake socket, or a host without a global WebSocket). */
|
|
117
|
+
WebSocket?: LiveSocketCtor;
|
|
118
|
+
/** Live refresh: the editor pushed a freshly compiled bundle. `data` is
|
|
119
|
+
* the .storyletsc JSON; hand it (with your current Engine) to
|
|
120
|
+
* `applyLiveBundle`, `attach` the engine it returns, then call
|
|
121
|
+
* `link.setBuild(build)`. Never called with a malformed frame. */
|
|
122
|
+
onBundle?: (msg: {
|
|
123
|
+
build: string;
|
|
124
|
+
data: string;
|
|
125
|
+
}) => void;
|
|
126
|
+
}
|
|
127
|
+
interface LiveLink {
|
|
128
|
+
/** Start forwarding this ENGINE's trace: every flow's events, each frame
|
|
129
|
+
* naming the flow it came from, so the editor can follow one participant
|
|
130
|
+
* and switch. An earlier engine is detached first. Sends a board snapshot
|
|
131
|
+
* per open flow straight away, queued behind the hello if the socket is
|
|
132
|
+
* not open yet.
|
|
133
|
+
*
|
|
134
|
+
* Flows are discovered rather than declared: the link diffs `engine.flows()`
|
|
135
|
+
* whenever anything happens and emits `flowOpen` / `flowClose` itself. That
|
|
136
|
+
* is a deliberate departure from Patterplay, whose host calls `FlowOpened`
|
|
137
|
+
* by hand - it has no engine-level trace tap to hang the diff on and we do,
|
|
138
|
+
* so the host has nothing to remember and cannot get the editor's flow list
|
|
139
|
+
* wrong. The one cost: a flow that opens and then does nothing at all is not
|
|
140
|
+
* announced until the next event anywhere in the run. */
|
|
141
|
+
attach(engine: Engine): void;
|
|
142
|
+
/** Stop forwarding. A refresh replaces the engine, so attach the new one
|
|
143
|
+
* afterwards. */
|
|
144
|
+
detach(): void;
|
|
145
|
+
/** After applying a pushed bundle: report the build now running (re-hellos
|
|
146
|
+
* with the new build and a fresh board snapshot, so the editor's chip goes
|
|
147
|
+
* back to in sync and it stops re-pushing the same bundle). */
|
|
148
|
+
setBuild(build: string): void;
|
|
149
|
+
/** Close the link; every later call is a no-op. */
|
|
150
|
+
close(): void;
|
|
151
|
+
}
|
|
152
|
+
/** One game-to-editor frame, as the client serialises it. Exported for the
|
|
153
|
+
* fixture test; hosts never build these by hand. */
|
|
154
|
+
type LiveFrame = {
|
|
155
|
+
t: "hello";
|
|
156
|
+
v: 2;
|
|
157
|
+
build: string;
|
|
158
|
+
project?: string;
|
|
159
|
+
boxes?: string[];
|
|
160
|
+
flows: string[];
|
|
161
|
+
} | {
|
|
162
|
+
t: "flowOpen";
|
|
163
|
+
flow: string;
|
|
164
|
+
} | {
|
|
165
|
+
t: "flowClose";
|
|
166
|
+
flow: string;
|
|
167
|
+
} | {
|
|
168
|
+
t: "trace";
|
|
169
|
+
flow: string;
|
|
170
|
+
event: TraceEvent;
|
|
171
|
+
} | {
|
|
172
|
+
t: "board";
|
|
173
|
+
flow: string;
|
|
174
|
+
hands: Record<string, string[]>;
|
|
175
|
+
turns: Record<string, number>;
|
|
176
|
+
};
|
|
177
|
+
/** The cheap snapshot: hands by gameId holding card gameIds in dealt order,
|
|
178
|
+
* and every box's clock by gameId. */
|
|
179
|
+
declare function boardFrame(flow: Flow): Extract<LiveFrame, {
|
|
180
|
+
t: "board";
|
|
181
|
+
}>;
|
|
182
|
+
/**
|
|
183
|
+
* Open a Live Link to Storyletter. Returns a handle whose calls are no-ops once
|
|
184
|
+
* the editor disconnects or if it was never listening: safe to leave wired into
|
|
185
|
+
* a shipping build behind a flag.
|
|
186
|
+
*/
|
|
187
|
+
declare function createLiveLink(opts: LiveLinkOptions): LiveLink;
|
|
188
|
+
|
|
189
|
+
type LiveBundleResult =
|
|
190
|
+
/** The new engine, carrying the old one's run (all flows), and the bundle
|
|
191
|
+
* it runs. */
|
|
192
|
+
{
|
|
193
|
+
ok: true;
|
|
194
|
+
engine: Engine;
|
|
195
|
+
bundle: Bundle;
|
|
196
|
+
}
|
|
197
|
+
/** Nothing changed: keep the engine you have. */
|
|
198
|
+
| {
|
|
199
|
+
ok: false;
|
|
200
|
+
error: string;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Apply a bundle the editor pushed over the Live Link: `new Engine(parsed,
|
|
204
|
+
* opts)` then `loadGame(engine.saveGame())`, returning the new engine. Never
|
|
205
|
+
* throws; a failure (unparseable JSON, a bundle the runtime rejects, a
|
|
206
|
+
* different project) comes back as `{ ok: false, error }` and the old engine
|
|
207
|
+
* is untouched.
|
|
208
|
+
*
|
|
209
|
+
* `opts` are the options the old engine was created with. An engine does
|
|
210
|
+
* not expose its seed, and it does not matter here: the save envelope carries
|
|
211
|
+
* each flow's PRNG state, so `loadGame` resumes the draw sequences exactly
|
|
212
|
+
* where they were and `seed` only shapes fresh flows. `log` does matter (the
|
|
213
|
+
* retained log is per flow), and so does `world` (the host's binding does not
|
|
214
|
+
* ride the envelope), so pass them if you had them.
|
|
215
|
+
*/
|
|
216
|
+
declare function applyLiveBundle(engine: Engine, bundleJson: string, opts?: EngineOptions): LiveBundleResult;
|
|
217
|
+
|
|
218
|
+
type ScalarValue = boolean | number | string | string[];
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A scope backed by a host resolver rather than a static bag - the basis for
|
|
222
|
+
* *foreign* scopes (e.g. `@game` / `@world`) whose values live in a host or
|
|
223
|
+
* another engine and are read (and optionally written) at runtime. The
|
|
224
|
+
* evaluator treats a missing property the same as a bag does (the scope's
|
|
225
|
+
* missing-policy decides false-vs-throw). A scope entirely absent from the
|
|
226
|
+
* EvalContext still resolves to false regardless.
|
|
227
|
+
*/
|
|
228
|
+
interface ScopeResolver {
|
|
229
|
+
/** Read a property's value, or undefined if the scope does not have it. */
|
|
230
|
+
get(name: string): ScalarValue | undefined;
|
|
231
|
+
/** Write a property (omit for a read-only scope). The core never calls this;
|
|
232
|
+
* it is for host/runtime effect application (e.g. a state container's `set`). */
|
|
233
|
+
set?(name: string, value: ScalarValue): void;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
interface WorldContainer {
|
|
237
|
+
/** Pass as `new Engine(bundle, { world: container.resolver })`. */
|
|
238
|
+
resolver: ScopeResolver;
|
|
239
|
+
/** The kernel bag itself (subscribe, audit, rows live there) - mount it
|
|
240
|
+
* into a state logger or examiner beside the engine's own bags. */
|
|
241
|
+
bag: PropertyBag$1;
|
|
242
|
+
/** The current values, for saving beside the engine's envelope. */
|
|
243
|
+
values(): PropertyBag;
|
|
244
|
+
/** Restore saved values over fresh defaults: orphaned keys drop, new
|
|
245
|
+
* declarations keep their defaults - the same drift rule as loadGame. */
|
|
246
|
+
load(values: PropertyBag): void;
|
|
247
|
+
}
|
|
248
|
+
/** A world container seeded from the bundle's @world declarations. */
|
|
249
|
+
declare function createWorldContainer(bundle: Bundle): WorldContainer;
|
|
250
|
+
|
|
251
|
+
export { type BundleInspector, type BundleInspectorOptions, type LiveBundleResult, type LiveFrame, type LiveLink, type LiveLinkOptions, type LiveSocketLike, type PropertyInspector, type PropertyInspectorOptions, type WorldContainer, applyLiveBundle, boardFrame, createBundleInspector, createLiveLink, createPropertyInspector, createStateLogger, createWorldContainer, deserializeState, ensureInspectorStyle, formatLogEntry, formatPropertySummary, formatScopeLabel, loadState, saveState, serializeState, snapshotState };
|