@fzgame/kit 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Fanzeal
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,183 @@
1
- # Temporary Holding Version
1
+ # @fzgame/kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The client kit a FZ GameHub game loads: it resolves which game instance is being played, creates or
4
+ resumes a play session, keeps a local snapshot of the session, and posts lifecycle events back to the
5
+ server. The editor and the sandbox talk to a game through the same message protocol.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @fzgame/kit
11
+ ```
12
+
13
+ The kit is ESM-first and also ships a CommonJS build. In the GameHub hosts a game loads the bundle as
14
+ a dev-time URL or as `game/vendor/kit.js` from a published zip, so a game does not bundle the kit
15
+ itself; the CLI's `fzgame build` vendors it. Items below assume a bundler when you import it directly.
16
+
17
+ ## Quick start
18
+
19
+ ```js
20
+ import { createKit } from '@fzgame/kit'
21
+
22
+ // Reads apiUrl, gameInstanceId, and playerToken from the query string.
23
+ const kit = createKit()
24
+
25
+ await kit.load() // the published game instance
26
+ await kit.connect() // a play session
27
+ await kit.emit('ready')
28
+
29
+ await kit.emit('start')
30
+ kit.setState({ clicks: 1 })
31
+ await kit.emit('score', { score: 1 })
32
+ await kit.emit('finish', { score: 1, result: { clicks: 1 } })
33
+
34
+ kit.snapshot // { status, score, result, state }
35
+ kit.config() // validated form values for this instance
36
+ ```
37
+
38
+ Pass the values explicitly instead of reading the query string:
39
+
40
+ ```js
41
+ const kit = createKit({ apiUrl, gameInstanceId, playerToken })
42
+ ```
43
+
44
+ `createKit()` reads anything you leave out from the query string (`apiUrl`, `gameInstanceId`,
45
+ `playerToken`). A value that is still missing throws at construction.
46
+
47
+ ## Lifecycle
48
+
49
+ ```
50
+ CREATED → READY → PLAYING ⇄ PAUSED → FINISHED
51
+ ↘ ABORTED
52
+ ```
53
+
54
+ `emit(type, payload?)` sends the event and adopts the session the server returns, so `kit.snapshot`
55
+ tracks the server rather than your local guess. The events the server acts on are `ready`, `start`,
56
+ `pause`, `finish`, `abort`, `score`, and `snapshot` (also exported as `LIFECYCLE_EVENTS`). Any other
57
+ string is stored as the session's last event and leaves the status alone.
58
+
59
+ | Event | Effect |
60
+ | --- | --- |
61
+ | `ready` | Status `READY`. |
62
+ | `start` | Status `PLAYING`. |
63
+ | `pause` | Status `PAUSED`. |
64
+ | `finish` | Status `FINISHED`; `payload.score` and `payload.result` are stored. |
65
+ | `abort` | Status `ABORTED`. |
66
+ | `score` | Stores `payload.score`. |
67
+ | `snapshot` | Stores `payload.state` as the session snapshot. |
68
+
69
+ `emit()` and `setState()` require a session and throw if you call them before `connect()`.
70
+
71
+ ## Snapshot
72
+
73
+ `kit.snapshot` is `{ status, score, result, state }`, refreshed after every call that returns a
74
+ session. `setState(state)` also stores the value in the session's metadata, so a resumed session
75
+ restores where the player stopped.
76
+
77
+ ## Config
78
+
79
+ `kit.config()` merges, in increasing priority: defaults from the game instance's config schema, the
80
+ instance overrides an operator set, and the config passed to `createKit` (or read from the query
81
+ string). `readFormSchema(schema)` exposes the schema itself when you render the settings form.
82
+
83
+ ## Preview
84
+
85
+ `?preview=true` (also `1` / `yes`) skips every server call. `load`, `connect`, `emit`, and `setState`
86
+ stay in memory so the game is free to play, and credentials become optional. The sandbox and editor
87
+ put unsaved settings in `?gameSetting={"goal":10}`; it wins over the older `?config=`, so Play reloads
88
+ with your draft before you submit.
89
+
90
+ ```
91
+ /game/index.html?preview=true&gameSetting=%7B%22goal%22%3A20%7D&kit=/@fzgame/kit.js
92
+ ```
93
+
94
+ ## Editor protocol
95
+
96
+ A game running inside the editor can send its draft config out and receive the editor's init message.
97
+ Both directions post to `window.parent`; the editor and sandbox listen on the `fzgame-editor` and
98
+ `fzgame-host` sources exported as `EDITOR_SOURCE` and `HOST_SOURCE`.
99
+
100
+ ```js
101
+ import { postEditorSave, onHostInit } from '@fzgame/kit'
102
+
103
+ const stop = onHostInit(({ config, configSchema, gameInstanceId }) => { /* apply */ })
104
+ postEditorSave({ goal: 20 })
105
+ ```
106
+
107
+ `onHostInit` returns an unsubscribe function. Messages are posted with a `'*'` target origin, because
108
+ a game running in a cross-origin frame cannot read the parent's origin.
109
+
110
+ ## Errors
111
+
112
+ | Error | When |
113
+ | --- | --- |
114
+ | `Error` | A required option is missing, or `emit` / `setState` runs before `connect()`. |
115
+ | `KitApiError` | A request answered with a non-OK status. Carries `status`, `statusText`, `method`, `url`, the parsed `body`, and `code` when the body has one. |
116
+ | `KitTimeoutError` | A request outlived `timeoutMs`. Carries `timeoutMs`, `method`, and `url`. |
117
+
118
+ ```js
119
+ import { KitApiError } from '@fzgame/kit'
120
+
121
+ try {
122
+ await kit.connect()
123
+ } catch (error) {
124
+ if (error instanceof KitApiError && error.status === 404) {
125
+ // the instance is gone
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Timeouts
131
+
132
+ Every request is aborted after `timeoutMs`, 30 seconds by default, so a game's loading screen never
133
+ hangs on a dead socket. Set `timeoutMs: 0` to wait without a deadline.
134
+
135
+ ```js
136
+ const kit = createKit({ timeoutMs: 5_000 })
137
+ ```
138
+
139
+ The kit uses `AbortController` rather than `AbortSignal.timeout`, so it runs in older browsers.
140
+
141
+ ## Debug channel
142
+
143
+ When the game runs in a frame, the kit posts its progress to the parent as
144
+ `{ source: 'fzgame-kit', type: 'debug', event, payload, at }`. The sandbox shows these in its log
145
+ panel. Credential-looking keys (`token`, `secret`, `password`, `authorization`, `apiKey`) and
146
+ reference cycles are replaced before posting.
147
+
148
+ ## API
149
+
150
+ | Export | Purpose |
151
+ | --- | --- |
152
+ | `createKit(options?)` | Build a kit from options plus the query string. |
153
+ | `FzGameKit` | The class, if you would rather construct it directly. |
154
+ | `DEFAULT_TIMEOUT_MS` | 30000. |
155
+ | `KIT_SOURCE`, `EDITOR_SOURCE`, `HOST_SOURCE` | Message `source` values. |
156
+ | `postEditorSave`, `onHostInit` | Outgoing save and incoming init for the editor. |
157
+ | `readQueryConfig`, `readFormSchema`, `isPreviewFlag` | Query and schema helpers. |
158
+ | `LIFECYCLE_EVENTS`, `PLAY_SESSION_STATUSES`, `emptySnapshot` | Lifecycle constants. |
159
+ | `KitApiError`, `KitTimeoutError` | Typed failures. |
160
+ | `KitOptions`, `EmitType`, `Snapshot`, `LifecycleEvent`, `GameInstancePayload`, `PlaySessionPayload` | Types. |
161
+
162
+ ## What it does not do
163
+
164
+ - It renders nothing. No DOM, no canvas, no styles: your game draws and the kit keeps score.
165
+ - It stores nothing locally. No `localStorage`, no cookies. State reaches the server through
166
+ `setState` and `emit`, so a player who reloads outside a resumed session starts over.
167
+ - It does not retry. A request that fails or times out rejects; nothing is queued, so a `finish`
168
+ event during a network blip is lost unless the game sends it again.
169
+ - It does not validate the config it returns. `kit.config()` merges default values, instance
170
+ overrides, and your own in that order and hands the result over as-is.
171
+
172
+ ## Development
173
+
174
+ ```sh
175
+ pnpm build # tsup, ESM + CJS + declarations
176
+ pnpm check-types
177
+ pnpm test:run
178
+ pnpm smoke # pack the tarball and consume it from a CJS and an ESM consumer
179
+ ```
180
+
181
+ ## License
182
+
183
+ ISC
package/dist/index.cjs ADDED
@@ -0,0 +1,435 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // src/index.ts
23
+ var src_exports = {};
24
+ __export(src_exports, {
25
+ DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
26
+ EDITOR_SOURCE: () => EDITOR_SOURCE,
27
+ FzGameKit: () => FzGameKit,
28
+ HOST_SOURCE: () => HOST_SOURCE,
29
+ KIT_SOURCE: () => KIT_SOURCE,
30
+ KitApiError: () => KitApiError,
31
+ KitTimeoutError: () => KitTimeoutError,
32
+ LIFECYCLE_EVENTS: () => LIFECYCLE_EVENTS,
33
+ PLAY_SESSION_STATUSES: () => PLAY_SESSION_STATUSES,
34
+ createKit: () => createKit,
35
+ emptySnapshot: () => emptySnapshot,
36
+ isPreviewFlag: () => isPreviewFlag,
37
+ onHostInit: () => onHostInit,
38
+ postEditorSave: () => postEditorSave,
39
+ readFormSchema: () => readFormSchema,
40
+ readQueryConfig: () => readQueryConfig
41
+ });
42
+ module.exports = __toCommonJS(src_exports);
43
+
44
+ // src/lifecycle.ts
45
+ var PLAY_SESSION_STATUSES = [
46
+ "CREATED",
47
+ "READY",
48
+ "PLAYING",
49
+ "PAUSED",
50
+ "FINISHED",
51
+ "ABORTED",
52
+ "EXPIRED"
53
+ ];
54
+ var LIFECYCLE_EVENTS = [
55
+ "ready",
56
+ "start",
57
+ "pause",
58
+ "finish",
59
+ "abort",
60
+ "score",
61
+ "snapshot"
62
+ ];
63
+ function emptySnapshot() {
64
+ return { status: "CREATED", score: null, result: null, state: null };
65
+ }
66
+
67
+ // src/kit.ts
68
+ var DEFAULT_TIMEOUT_MS = 3e4;
69
+ var KIT_SOURCE = "fzgame-kit";
70
+ var SECRET_KEY = /(token|secret|password|authorization|api[-_]?key)/i;
71
+ function redact(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
72
+ if (depth > 8 || value === null || typeof value !== "object") return value;
73
+ if (seen.has(value)) return "[circular]";
74
+ seen.add(value);
75
+ const out = Array.isArray(value) ? value.map((item) => redact(item, seen, depth + 1)) : Object.fromEntries(
76
+ Object.entries(value).map(([key, item]) => [
77
+ key,
78
+ SECRET_KEY.test(key) ? "[redacted]" : redact(item, seen, depth + 1)
79
+ ])
80
+ );
81
+ seen.delete(value);
82
+ return out;
83
+ }
84
+ var KitApiError = class extends Error {
85
+ constructor(response, body, method, url) {
86
+ super(`${method} ${url} failed with ${response.status} ${response.statusText}`);
87
+ __publicField(this, "status");
88
+ __publicField(this, "statusText");
89
+ __publicField(this, "method");
90
+ __publicField(this, "url");
91
+ /** Parsed response body, or the raw text when it is not JSON. */
92
+ __publicField(this, "body");
93
+ /** `body.code` when the server sends one. */
94
+ __publicField(this, "code");
95
+ this.name = "KitApiError";
96
+ this.status = response.status;
97
+ this.statusText = response.statusText;
98
+ this.method = method;
99
+ this.url = url;
100
+ this.body = body;
101
+ const code = body && typeof body === "object" ? body.code : void 0;
102
+ if (typeof code === "string") this.code = code;
103
+ }
104
+ };
105
+ var KitTimeoutError = class extends Error {
106
+ constructor(timeoutMs, method, url) {
107
+ super(`${method} ${url} did not answer within ${timeoutMs}ms`);
108
+ __publicField(this, "timeoutMs");
109
+ __publicField(this, "method");
110
+ __publicField(this, "url");
111
+ this.name = "KitTimeoutError";
112
+ this.timeoutMs = timeoutMs;
113
+ this.method = method;
114
+ this.url = url;
115
+ }
116
+ };
117
+ var FzGameKit = class {
118
+ constructor(options) {
119
+ __publicField(this, "options");
120
+ __publicField(this, "instance", null);
121
+ __publicField(this, "session", null);
122
+ __publicField(this, "snapshot", emptySnapshot());
123
+ __publicField(this, "injectedConfig");
124
+ __publicField(this, "connecting", null);
125
+ const preview = Boolean(options.preview);
126
+ this.options = {
127
+ apiUrl: (options.apiUrl || "").replace(/\/$/, ""),
128
+ gameInstanceId: options.gameInstanceId || (preview ? "preview" : ""),
129
+ playerToken: options.playerToken || (preview ? "preview" : ""),
130
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
131
+ config: options.config,
132
+ preview
133
+ };
134
+ if (!preview && (!this.options.apiUrl || !this.options.gameInstanceId || !this.options.playerToken)) {
135
+ throw new Error("FzGameKit requires apiUrl, gameInstanceId, and playerToken");
136
+ }
137
+ this.injectedConfig = options.config ?? readQueryConfig() ?? {};
138
+ }
139
+ get preview() {
140
+ return this.options.preview;
141
+ }
142
+ async load() {
143
+ try {
144
+ if (this.options.preview) {
145
+ this.instance = {
146
+ id: this.options.gameInstanceId,
147
+ name: "preview",
148
+ overrides: this.injectedConfig
149
+ };
150
+ this.debug("load", { instance: this.instance, preview: true });
151
+ return this.instance;
152
+ }
153
+ this.instance = await this.request(
154
+ `/pub/game-instances/${this.options.gameInstanceId}`
155
+ );
156
+ this.debug("load", { instance: this.instance });
157
+ return this.instance;
158
+ } catch (error) {
159
+ this.debug("load", { error: errorMessage(error) });
160
+ throw error;
161
+ }
162
+ }
163
+ async connect() {
164
+ this.connecting ?? (this.connecting = this.startSession().finally(() => {
165
+ this.connecting = null;
166
+ }));
167
+ return this.connecting;
168
+ }
169
+ async startSession() {
170
+ try {
171
+ if (this.options.preview) {
172
+ this.session = previewSession();
173
+ this.hydrate();
174
+ this.debug("connect", { session: this.session, preview: true });
175
+ return this.session;
176
+ }
177
+ this.session = await this.request(
178
+ "/games/runtime/sessions",
179
+ {
180
+ method: "POST",
181
+ body: JSON.stringify({
182
+ gameInstanceId: this.options.gameInstanceId,
183
+ playerToken: this.options.playerToken
184
+ })
185
+ }
186
+ );
187
+ this.hydrate();
188
+ this.debug("connect", { session: this.session });
189
+ return this.session;
190
+ } catch (error) {
191
+ this.debug("connect", { error: errorMessage(error) });
192
+ throw error;
193
+ }
194
+ }
195
+ async emit(type, payload) {
196
+ try {
197
+ if (this.options.preview) {
198
+ this.session = this.session ?? previewSession();
199
+ applyLocalEmit(this.session, type, payload);
200
+ this.hydrate();
201
+ this.debug("emit", { type, payload: payload ?? null, session: this.session, preview: true });
202
+ return this.session;
203
+ }
204
+ const token = this.requireToken();
205
+ this.session = await this.request(
206
+ `/games/runtime/sessions/${token}/emit`,
207
+ {
208
+ method: "POST",
209
+ body: JSON.stringify({ type, payload })
210
+ }
211
+ );
212
+ this.hydrate();
213
+ this.debug("emit", { type, payload: payload ?? null, session: this.session });
214
+ return this.session;
215
+ } catch (error) {
216
+ this.debug("emit", { type, payload: payload ?? null, error: errorMessage(error) });
217
+ throw error;
218
+ }
219
+ }
220
+ async setState(state) {
221
+ try {
222
+ this.snapshot = { ...this.snapshot, state };
223
+ if (this.options.preview) {
224
+ this.session = this.session ?? previewSession();
225
+ this.session.metadata = { snapshot: state };
226
+ this.hydrate();
227
+ this.debug("snapshot", { state, session: this.session, preview: true });
228
+ return this.snapshot;
229
+ }
230
+ const token = this.requireToken();
231
+ this.session = await this.request(
232
+ `/games/runtime/sessions/${token}/snapshot`,
233
+ {
234
+ method: "PUT",
235
+ body: JSON.stringify({ state })
236
+ }
237
+ );
238
+ this.hydrate();
239
+ this.debug("snapshot", { state, session: this.session });
240
+ return this.snapshot;
241
+ } catch (error) {
242
+ this.debug("snapshot", { state, error: errorMessage(error) });
243
+ throw error;
244
+ }
245
+ }
246
+ config() {
247
+ const schema = readFormSchema(this.instance?.game?.configSchema);
248
+ const defaults = readDefaults(schema);
249
+ return { ...defaults, ...this.instance?.overrides ?? {}, ...this.injectedConfig };
250
+ }
251
+ hydrate() {
252
+ if (!this.session) return;
253
+ const meta = this.session.metadata;
254
+ this.snapshot = {
255
+ status: this.session.status,
256
+ score: this.session.score ?? null,
257
+ result: this.session.result ?? null,
258
+ state: meta && typeof meta === "object" ? meta.snapshot ?? this.snapshot.state : this.snapshot.state
259
+ };
260
+ }
261
+ requireToken() {
262
+ if (!this.session?.token) {
263
+ throw new Error("Call connect() before emit or setState");
264
+ }
265
+ return this.session.token;
266
+ }
267
+ debug(event, payload) {
268
+ const w = typeof globalThis !== "undefined" ? globalThis.window : void 0;
269
+ if (!w?.parent?.postMessage) return;
270
+ const message = {
271
+ source: KIT_SOURCE,
272
+ type: "debug",
273
+ event,
274
+ payload: redact(payload),
275
+ at: Date.now()
276
+ };
277
+ try {
278
+ w.parent.postMessage(message, "*");
279
+ } catch {
280
+ }
281
+ }
282
+ async request(path, init = {}) {
283
+ const url = `${this.options.apiUrl}${path}`;
284
+ const method = init.method ?? "GET";
285
+ const headers = new Headers(init.headers);
286
+ if (init.body && !headers.has("content-type")) {
287
+ headers.set("content-type", "application/json");
288
+ }
289
+ const res = await this.fetchWithTimeout(url, { ...init, headers }, method);
290
+ if (!res.ok) {
291
+ throw new KitApiError(res, await readBody(res), method, url);
292
+ }
293
+ return await res.json();
294
+ }
295
+ async fetchWithTimeout(url, init, method) {
296
+ const { timeoutMs } = this.options;
297
+ if (!timeoutMs || timeoutMs <= 0) return fetch(url, init);
298
+ const controller = new AbortController();
299
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
300
+ try {
301
+ return await fetch(url, { ...init, signal: controller.signal });
302
+ } catch (error) {
303
+ if (controller.signal.aborted) throw new KitTimeoutError(timeoutMs, method, url);
304
+ throw error;
305
+ } finally {
306
+ clearTimeout(timer);
307
+ }
308
+ }
309
+ };
310
+ async function readBody(res) {
311
+ const text = await res.text();
312
+ if (!text) return void 0;
313
+ try {
314
+ return JSON.parse(text);
315
+ } catch {
316
+ return text;
317
+ }
318
+ }
319
+ function createKit(options = {}) {
320
+ const params = typeof location !== "undefined" ? new URLSearchParams(location.search) : new URLSearchParams();
321
+ const preview = options.preview ?? isPreviewFlag(params.get("preview"));
322
+ const apiUrl = options.apiUrl || params.get("apiUrl") || "";
323
+ const gameInstanceId = options.gameInstanceId || params.get("gameInstanceId") || "";
324
+ const playerToken = options.playerToken || params.get("playerToken") || "";
325
+ const config = options.config ?? readQueryConfig();
326
+ return new FzGameKit({
327
+ apiUrl,
328
+ gameInstanceId,
329
+ playerToken,
330
+ config,
331
+ preview,
332
+ timeoutMs: options.timeoutMs
333
+ });
334
+ }
335
+ function readQueryConfig() {
336
+ if (typeof location === "undefined") return void 0;
337
+ const params = new URLSearchParams(location.search);
338
+ return parseConfigParam(params.get("gameSetting")) ?? parseConfigParam(params.get("config"));
339
+ }
340
+ function isPreviewFlag(raw) {
341
+ if (!raw) return false;
342
+ const value = raw.trim().toLowerCase();
343
+ return value === "true" || value === "1" || value === "yes";
344
+ }
345
+ function parseConfigParam(raw) {
346
+ if (!raw) return void 0;
347
+ try {
348
+ const parsed = JSON.parse(raw);
349
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
350
+ return parsed;
351
+ }
352
+ } catch {
353
+ }
354
+ return void 0;
355
+ }
356
+ function previewSession() {
357
+ return {
358
+ id: "preview",
359
+ token: "preview",
360
+ status: "CREATED",
361
+ score: null,
362
+ result: null,
363
+ metadata: { snapshot: {} }
364
+ };
365
+ }
366
+ function applyLocalEmit(session, type, payload) {
367
+ if (type === "ready") session.status = "READY";
368
+ if (type === "start") session.status = "PLAYING";
369
+ if (type === "pause") session.status = "PAUSED";
370
+ if (type === "finish") {
371
+ session.status = "FINISHED";
372
+ if (typeof payload?.score === "number") session.score = payload.score;
373
+ if (payload && "result" in payload) session.result = payload.result;
374
+ }
375
+ if (type === "abort") session.status = "ABORTED";
376
+ if (type === "score" && typeof payload?.score === "number") session.score = payload.score;
377
+ if (type === "snapshot") {
378
+ session.metadata = { snapshot: payload?.state ?? payload ?? {} };
379
+ }
380
+ }
381
+ function errorMessage(error) {
382
+ return error instanceof Error ? error.message : String(error);
383
+ }
384
+ function readFormSchema(schema) {
385
+ if (!schema) return {};
386
+ const form = schema.formSchema;
387
+ if (form && typeof form === "object" && !Array.isArray(form)) {
388
+ return form;
389
+ }
390
+ return schema;
391
+ }
392
+ function readDefaults(schema) {
393
+ const properties = schema.properties;
394
+ if (!properties || typeof properties !== "object") return {};
395
+ const out = {};
396
+ for (const [key, value] of Object.entries(properties)) {
397
+ if (value && "default" in value) out[key] = value.default;
398
+ }
399
+ return out;
400
+ }
401
+ var EDITOR_SOURCE = "fzgame-editor";
402
+ var HOST_SOURCE = "fzgame-host";
403
+ function postEditorSave(config, target = window.parent) {
404
+ const message = { source: EDITOR_SOURCE, type: "save", config };
405
+ target.postMessage(message, "*");
406
+ }
407
+ function onHostInit(handler) {
408
+ const listener = (event) => {
409
+ const data = event.data;
410
+ if (!data || data.source !== HOST_SOURCE || data.type !== "init") return;
411
+ handler(data);
412
+ };
413
+ window.addEventListener("message", listener);
414
+ return () => window.removeEventListener("message", listener);
415
+ }
416
+ // Annotate the CommonJS export names for ESM import in node:
417
+ 0 && (module.exports = {
418
+ DEFAULT_TIMEOUT_MS,
419
+ EDITOR_SOURCE,
420
+ FzGameKit,
421
+ HOST_SOURCE,
422
+ KIT_SOURCE,
423
+ KitApiError,
424
+ KitTimeoutError,
425
+ LIFECYCLE_EVENTS,
426
+ PLAY_SESSION_STATUSES,
427
+ createKit,
428
+ emptySnapshot,
429
+ isPreviewFlag,
430
+ onHostInit,
431
+ postEditorSave,
432
+ readFormSchema,
433
+ readQueryConfig
434
+ });
435
+ //# sourceMappingURL=index.cjs.map