@eigeninteractive/testkit 0.2.5 → 0.3.1

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/inspect-cli.js";
@@ -1,5 +1,5 @@
1
1
  // src/twin-fixtures.ts
2
- import { readdirSync, readFileSync } from "fs";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
3
  import { IllegalMoveError } from "@eigeninteractive/rules";
4
4
  import Rand from "rand-seed";
5
5
  import { it } from "vitest";
@@ -296,8 +296,8 @@ function deepEquals(a, b) {
296
296
  }
297
297
 
298
298
  // src/game-contract.ts
299
- import { existsSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
300
- import { basename, relative, resolve } from "path";
299
+ import { existsSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
300
+ import { basename, relative, resolve } from "node:path";
301
301
  var GAME_CONTRACT_FORMAT_VERSION = 1;
302
302
  function canonical(value) {
303
303
  if (Array.isArray(value)) return value.map(canonical);
@@ -396,9 +396,9 @@ function checkGameContract(options) {
396
396
  var gameContractFilename = (game) => `${basename(game).toLowerCase().replaceAll(/[^a-z0-9]+/g, "-")}-game-contract.json`;
397
397
 
398
398
  // src/contract-command.ts
399
- import { readFileSync as readFileSync3 } from "fs";
400
- import { resolve as resolve2 } from "path";
401
- import { pathToFileURL } from "url";
399
+ import { readFileSync as readFileSync3 } from "node:fs";
400
+ import { resolve as resolve2 } from "node:path";
401
+ import { pathToFileURL } from "node:url";
402
402
  import { tsImport } from "tsx/esm/api";
403
403
  function configuredString(value, fallback, field) {
404
404
  if (value === void 0) return fallback;
@@ -450,4 +450,4 @@ export {
450
450
  emitConfiguredGameContract,
451
451
  checkConfiguredGameContract
452
452
  };
453
- //# sourceMappingURL=chunk-SVC3562K.js.map
453
+ //# sourceMappingURL=chunk-JJW4WLTM.js.map
@@ -0,0 +1,391 @@
1
+ // src/local-state.ts
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ var STATE_SUFFIX = join(".wrangler", "state", "v3");
6
+ var METADATA_FILE = "metadata.sqlite";
7
+ var DO_NAME_TABLE = "__miniflare_do_name";
8
+ function findWranglerState(from = process.cwd()) {
9
+ let dir = resolve(from);
10
+ for (; ; ) {
11
+ const candidate = join(dir, STATE_SUFFIX);
12
+ if (existsSync(candidate)) return candidate;
13
+ const parent = dirname(dir);
14
+ if (parent === dir) return null;
15
+ dir = parent;
16
+ }
17
+ }
18
+ function openSqlite(file) {
19
+ if (!existsSync(file)) throw new Error(`no SQLite database at ${file}`);
20
+ try {
21
+ return new DatabaseSync(file, { readOnly: true });
22
+ } catch {
23
+ return new DatabaseSync(file);
24
+ }
25
+ }
26
+ function sqliteFilesIn(dir) {
27
+ if (!existsSync(dir)) return [];
28
+ return readdirSync(dir).filter((entry) => entry.endsWith(".sqlite") && entry !== METADATA_FILE).map((entry) => join(dir, entry));
29
+ }
30
+ function tableExists(db, table) {
31
+ const row = db.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(table);
32
+ return row !== void 0;
33
+ }
34
+ function json(value) {
35
+ if (typeof value !== "string") return value ?? null;
36
+ try {
37
+ return JSON.parse(value);
38
+ } catch {
39
+ return value;
40
+ }
41
+ }
42
+ function numbers(value) {
43
+ const parsed = json(value);
44
+ return Array.isArray(parsed) ? parsed.filter((n) => typeof n === "number") : [];
45
+ }
46
+ function bool(value) {
47
+ return value === 1 || value === true;
48
+ }
49
+ var LocalStore = class _LocalStore {
50
+ /** Absolute path to `.wrangler/state/v3`. */
51
+ root;
52
+ #d1;
53
+ #open = /* @__PURE__ */ new Map();
54
+ constructor(root) {
55
+ this.root = root;
56
+ }
57
+ /**
58
+ * Locate and open the state under `from`, or throw with the one thing the
59
+ * caller can act on: that no Worker has been run here yet.
60
+ */
61
+ static open(from) {
62
+ const root = findWranglerState(from);
63
+ if (root === null) {
64
+ throw new Error(`no .wrangler/state/v3 found at or above ${resolve(from ?? process.cwd())}. Run the Worker once (\`pnpm dev\`) so it has local state to read.`);
65
+ }
66
+ return new _LocalStore(root);
67
+ }
68
+ #handle(file) {
69
+ const existing = this.#open.get(file);
70
+ if (existing !== void 0) return existing;
71
+ const db = openSqlite(file);
72
+ this.#open.set(file, db);
73
+ return db;
74
+ }
75
+ /**
76
+ * The engine's D1 database, identified by its schema rather than its name.
77
+ *
78
+ * Miniflare names the file after a hash of the database, so there is no
79
+ * mapping back to the binding. Recognising the engine's own `games` table
80
+ * instead is both simpler and more honest about what the caller wants: the
81
+ * database that has games in it, whatever the binding was called.
82
+ */
83
+ d1() {
84
+ if (this.#d1 !== void 0) return this.#d1;
85
+ for (const file of sqliteFilesIn(join(this.root, "d1", "miniflare-D1DatabaseObject"))) {
86
+ const db = this.#handle(file);
87
+ if (tableExists(db, "games")) {
88
+ this.#d1 = { file, db };
89
+ return this.#d1;
90
+ }
91
+ }
92
+ this.#d1 = null;
93
+ return null;
94
+ }
95
+ /**
96
+ * Every local Durable Object database, newest-modified first.
97
+ *
98
+ * The directory is `<worker>-<ClassName>`, and a worker name may itself
99
+ * contain a dash, so the class is everything after the LAST one. Ordering by
100
+ * mtime puts the game you just touched at the top, which is nearly always the
101
+ * one being debugged.
102
+ */
103
+ durableObjects() {
104
+ const base = join(this.root, "do");
105
+ if (!existsSync(base)) return [];
106
+ const found = [];
107
+ for (const namespace of readdirSync(base)) {
108
+ const dir = join(base, namespace);
109
+ if (!statSync(dir).isDirectory()) continue;
110
+ const className = namespace.slice(namespace.lastIndexOf("-") + 1);
111
+ for (const file of sqliteFilesIn(dir)) {
112
+ const db = this.#handle(file);
113
+ const name = tableExists(db, DO_NAME_TABLE) ? db.prepare(`select name from ${DO_NAME_TABLE} limit 1`).get()?.name ?? null : null;
114
+ found.push({ className, id: file.slice(file.lastIndexOf("/") + 1, -".sqlite".length), name, file });
115
+ }
116
+ }
117
+ return found.sort((a, b) => statSync(b.file).mtimeMs - statSync(a.file).mtimeMs);
118
+ }
119
+ /** The armed alarm for a Durable Object, from its namespace's metadata
120
+ * database. Matched on either column because the id is what Miniflare keys
121
+ * by and the name is what a human recognises. */
122
+ alarm(target) {
123
+ const file = join(dirname(target.file), METADATA_FILE);
124
+ if (!existsSync(file)) return null;
125
+ const db = this.#handle(file);
126
+ if (!tableExists(db, "_cf_ALARM")) return null;
127
+ const row = db.prepare("select actor_id, actor_name, scheduled_time from _cf_ALARM where actor_id = ? or actor_name = ?").get(target.id, target.name ?? target.id);
128
+ return row === void 0 ? null : { actorId: row.actor_id, actorName: row.actor_name, scheduledTime: row.scheduled_time };
129
+ }
130
+ /** The D1 index rows, newest-updated first. */
131
+ games(options = {}) {
132
+ const d1 = this.d1();
133
+ if (d1 === null) return [];
134
+ const where = options.status === void 0 ? "" : "where status = ?";
135
+ const params = options.status === void 0 ? [] : [options.status];
136
+ const rows = d1.db.prepare(`select * from games ${where} order by updated_at desc limit ?`).all(...params, options.limit ?? 50);
137
+ return rows.map(toGameRow);
138
+ }
139
+ /**
140
+ * Resolve a game reference the way a developer will type it: a full id, a
141
+ * six-character short code, or an id prefix long enough to be unique.
142
+ *
143
+ * D1 is tried first because it can match a short code, then the DO names, so
144
+ * a game whose index row was wiped is still reachable by id.
145
+ */
146
+ resolveGameId(ref) {
147
+ const d1 = this.d1();
148
+ if (d1 !== null) {
149
+ const exact = d1.db.prepare("select id from games where id = ? or short_code = ?").get(ref, ref.toUpperCase());
150
+ if (exact !== void 0) return exact.id;
151
+ const prefixed = d1.db.prepare("select id from games where id like ? limit 2").all(`${ref}%`);
152
+ if (prefixed.length === 1) return prefixed[0].id;
153
+ if (prefixed.length > 1) throw new Error(`"${ref}" matches more than one game; use a longer prefix`);
154
+ }
155
+ const named = this.durableObjects().filter((o) => o.name !== null && (o.name === ref || o.name.startsWith(ref)));
156
+ if (named.length === 1) return named[0].name;
157
+ if (named.length > 1) throw new Error(`"${ref}" matches more than one game; use a longer prefix`);
158
+ return null;
159
+ }
160
+ /** The Durable Object holding a game's session, or null if none exists yet. */
161
+ gameDurableObject(gameId) {
162
+ return this.durableObjects().find((o) => o.name === gameId) ?? null;
163
+ }
164
+ /** Identity for a set of user and bot ids, for rendering seats as names. */
165
+ #identities(userIds, botIds) {
166
+ const out = /* @__PURE__ */ new Map();
167
+ const d1 = this.d1();
168
+ if (d1 === null) return out;
169
+ for (const [table, ids] of [
170
+ ["users", userIds],
171
+ ["bots", botIds]
172
+ ]) {
173
+ if (ids.length === 0) continue;
174
+ const placeholders = ids.map(() => "?").join(",");
175
+ const rows = d1.db.prepare(`select id, username, display_name from ${table} where id in (${placeholders})`).all(...ids);
176
+ for (const row of rows) out.set(row.id, { username: row.username, displayName: row.display_name });
177
+ }
178
+ return out;
179
+ }
180
+ #seats(rows) {
181
+ const identities = this.#identities(
182
+ rows.map((r) => r.user_id).filter((id) => id !== null),
183
+ rows.map((r) => r.bot_id).filter((id) => id !== null)
184
+ );
185
+ return rows.map((row) => {
186
+ const identity = identities.get(row.user_id ?? row.bot_id ?? "");
187
+ return { playerIndex: row.player_index, userId: row.user_id, botId: row.bot_id, type: row.type, username: identity?.username ?? null, displayName: identity?.displayName ?? null };
188
+ });
189
+ }
190
+ /**
191
+ * Join both stores for one game: the index row, the session, the transition
192
+ * log, and the derived diagnosis.
193
+ *
194
+ * Returns null only when neither store has heard of the game.
195
+ */
196
+ game(ref) {
197
+ const gameId = this.resolveGameId(ref);
198
+ if (gameId === null) return null;
199
+ const d1 = this.d1();
200
+ const indexRow = d1 === null ? void 0 : d1.db.prepare("select * from games where id = ?").get(gameId);
201
+ const index = indexRow === void 0 ? null : toGameRow(indexRow);
202
+ const indexSeats = d1 === null ? [] : this.#seats(
203
+ d1.db.prepare("select player_index, user_id, bot_id, type from participants where game_id = ? order by player_index").all(gameId)
204
+ );
205
+ const durableObject = this.gameDurableObject(gameId);
206
+ let meta = null;
207
+ let roster = [];
208
+ let transitions = [];
209
+ let commands = [];
210
+ let outbox = [];
211
+ if (durableObject !== null) {
212
+ const db = this.#handle(durableObject.file);
213
+ if (tableExists(db, "meta")) {
214
+ const row = db.prepare("select * from meta where id = 1").get();
215
+ if (row !== void 0) meta = toMeta(row);
216
+ }
217
+ if (tableExists(db, "roster")) {
218
+ roster = this.#seats(db.prepare("select player_index, user_id, bot_id, type from roster order by player_index").all());
219
+ }
220
+ if (tableExists(db, "transitions")) {
221
+ const frameSeats = /* @__PURE__ */ new Map();
222
+ if (tableExists(db, "frames")) {
223
+ for (const row of db.prepare("select version, player_index from frames order by version, player_index").all()) {
224
+ frameSeats.set(row.version, [...frameSeats.get(row.version) ?? [], row.player_index]);
225
+ }
226
+ }
227
+ transitions = db.prepare("select * from transitions order by version").all().map((row) => toTransition(row, frameSeats));
228
+ }
229
+ if (tableExists(db, "commands")) {
230
+ commands = db.prepare("select command_id, response, created_at from commands order by created_at desc").all().map((row) => ({
231
+ commandId: row.command_id,
232
+ createdAt: row.created_at,
233
+ result: json(row.response)
234
+ }));
235
+ }
236
+ if (tableExists(db, "outbox")) {
237
+ outbox = db.prepare("select finish_id, outcomes, created_at from outbox order by created_at").all().map((row) => ({
238
+ finishId: row.finish_id,
239
+ createdAt: row.created_at,
240
+ outcomes: json(row.outcomes)
241
+ }));
242
+ }
243
+ }
244
+ const alarm = durableObject === null ? null : this.alarm(durableObject);
245
+ const view = { gameId, index, indexSeats, durableObject, meta, roster, transitions, commands, outbox, alarm };
246
+ return { ...view, diagnosis: diagnose(view) };
247
+ }
248
+ /** Run a read-only statement against D1 or one game's Durable Object: the
249
+ * escape hatch for a question this reader does not model. */
250
+ query(sql, options = {}) {
251
+ const db = options.game === void 0 ? this.d1()?.db : this.#gameDb(options.game);
252
+ if (db === void 0 || db === null) {
253
+ throw new Error(options.game === void 0 ? "no local D1 database found in this Worker's state" : `no local Durable Object found for game ${options.game}`);
254
+ }
255
+ return db.prepare(sql).all(...options.params ?? []);
256
+ }
257
+ /** Table names and row counts, for D1 or one game's Durable Object. */
258
+ tables(options = {}) {
259
+ const db = options.game === void 0 ? this.d1()?.db : this.#gameDb(options.game);
260
+ if (db === void 0 || db === null) return [];
261
+ const names = db.prepare("select name from sqlite_master where type = 'table' order by name").all().map((r) => r.name);
262
+ return names.map((name) => ({ name, rows: db.prepare(`select count(*) as n from "${name}"`).get().n }));
263
+ }
264
+ #gameDb(ref) {
265
+ const gameId = this.resolveGameId(ref);
266
+ const target = gameId === null ? null : this.gameDurableObject(gameId);
267
+ return target === null ? null : this.#handle(target.file);
268
+ }
269
+ /** Release every open handle. */
270
+ close() {
271
+ for (const db of this.#open.values()) db.close();
272
+ this.#open.clear();
273
+ this.#d1 = void 0;
274
+ }
275
+ };
276
+ function toGameRow(row) {
277
+ return {
278
+ id: row.id,
279
+ createdBy: row.created_by ?? null,
280
+ status: row.status,
281
+ access: row.access,
282
+ schemaVersion: row.schema_version,
283
+ config: json(row.config),
284
+ turnSeconds: row.turn_seconds ?? null,
285
+ budgetSeconds: row.budget_seconds ?? null,
286
+ incrementSeconds: row.increment_seconds ?? null,
287
+ rated: bool(row.rated),
288
+ ratingPool: row.rating_pool ?? null,
289
+ minPlayers: row.min_players,
290
+ maxPlayers: row.max_players,
291
+ shortCode: row.short_code,
292
+ pendingPlayers: row.pending_players === null || row.pending_players === void 0 ? null : numbers(row.pending_players),
293
+ turnDeadline: row.turn_deadline ?? null,
294
+ outcomes: json(row.outcomes),
295
+ finishId: row.finish_id ?? null,
296
+ finishedAt: row.finished_at ?? null,
297
+ archivedAt: row.archived_at ?? null,
298
+ createdAt: row.created_at,
299
+ updatedAt: row.updated_at
300
+ };
301
+ }
302
+ function toMeta(row) {
303
+ return {
304
+ gameId: row.game_id,
305
+ status: row.status,
306
+ access: row.access,
307
+ schemaVersion: row.schema_version,
308
+ config: json(row.config),
309
+ turnSeconds: row.turn_seconds ?? null,
310
+ budgetSeconds: row.budget_seconds ?? null,
311
+ incrementSeconds: row.increment_seconds ?? null,
312
+ rated: bool(row.rated),
313
+ ratingPool: row.rating_pool ?? null,
314
+ minPlayers: row.min_players,
315
+ maxPlayers: row.max_players,
316
+ createdBy: row.created_by ?? null,
317
+ rngSeed: row.rng_seed ?? null
318
+ };
319
+ }
320
+ function toTransition(row, frameSeats) {
321
+ const version = row.version;
322
+ const action = json(row.action);
323
+ return {
324
+ version,
325
+ state: json(row.state),
326
+ action: action ?? null,
327
+ pending: numbers(row.pending),
328
+ deadline: row.deadline ?? null,
329
+ playerTimes: row.player_times === null || row.player_times === void 0 ? null : numbers(row.player_times),
330
+ turnStartedAt: row.turn_started_at ?? null,
331
+ frameSeats: frameSeats.get(version) ?? []
332
+ };
333
+ }
334
+ function diagnose(view) {
335
+ const status = view.meta?.status ?? view.index?.status ?? "unknown";
336
+ const seated = (view.meta === null ? view.indexSeats : view.roster).length;
337
+ const min = view.meta?.minPlayers ?? view.index?.minPlayers ?? 0;
338
+ const max = view.meta?.maxPlayers ?? view.index?.maxPlayers ?? 0;
339
+ const latest = view.transitions.at(-1) ?? null;
340
+ const mirrorDrift = [];
341
+ if (view.index !== null && view.meta !== null) {
342
+ if (view.index.status !== view.meta.status) {
343
+ mirrorDrift.push(`D1 says status=${view.index.status}, the Durable Object says ${view.meta.status}. The DO is authoritative; a mirror write was lost.`);
344
+ }
345
+ if (view.indexSeats.length !== view.roster.length) {
346
+ mirrorDrift.push(`D1 mirrors ${view.indexSeats.length} seat(s), the Durable Object holds ${view.roster.length}.`);
347
+ }
348
+ const mirroredPending = view.index.pendingPlayers ?? null;
349
+ const actualPending = latest?.pending ?? null;
350
+ if (status === "active" && JSON.stringify(mirroredPending) !== JSON.stringify(actualPending)) {
351
+ mirrorDrift.push(`D1 mirrors pendingPlayers=${JSON.stringify(mirroredPending)}, the newest transition has ${JSON.stringify(actualPending)}.`);
352
+ }
353
+ }
354
+ if (view.index === null && view.meta !== null) {
355
+ mirrorDrift.push("no D1 index row for a game whose Durable Object exists; discovery and history lists cannot show it.");
356
+ }
357
+ let summary;
358
+ switch (status) {
359
+ case "waiting":
360
+ summary = `waiting for players: ${seated}/${min} needed to become ready (${max} max). The next event is a join, or add-bot from the creator.`;
361
+ break;
362
+ case "ready":
363
+ summary = `ready and not started: ${seated}/${max} seats filled. Nothing happens until the CREATOR calls POST /api/engine/games/${view.gameId}/start. A start is explicit and creator-only; filling the lobby does not start a game.`;
364
+ break;
365
+ case "active":
366
+ if (latest === null) {
367
+ summary = "status is active but no transition is committed, which should be impossible: a start commits v0 in the same transaction that sets the status.";
368
+ } else {
369
+ const waitingOn = latest.pending.length === 0 ? "nobody, which means the game should have finished at this version" : `seat(s) ${latest.pending.join(", ")}`;
370
+ const timer = latest.deadline === null ? "untimed, so no alarm is armed and only a move or a forfeit moves it" : `deadline ${new Date(latest.deadline).toISOString()}`;
371
+ summary = `at v${latest.version}, waiting on ${waitingOn}. ${timer}.`;
372
+ }
373
+ break;
374
+ case "finished":
375
+ summary = view.outbox.length > 0 ? `finished, but ${view.outbox.length} outbox row(s) survive: the D1 finish apply has not succeeded, so ratings and history are not published yet.` : `finished at v${latest?.version ?? "?"}; the outbox is clear, so D1 has the outcomes.`;
376
+ break;
377
+ case "aborted":
378
+ summary = "aborted: cancelled by its creator, reaped as abandoned, or torn down by an account purge. Nothing moves an aborted game.";
379
+ break;
380
+ default:
381
+ summary = view.durableObject === null ? "created in D1 but its Durable Object does not exist yet. That is normal for a lobby nobody has touched: the DO is created lazily by the first command or socket." : `unrecognised status ${status}.`;
382
+ }
383
+ return { summary, mirrorDrift };
384
+ }
385
+
386
+ export {
387
+ findWranglerState,
388
+ openSqlite,
389
+ LocalStore
390
+ };
391
+ //# sourceMappingURL=chunk-VXWO4LVS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/local-state.ts"],"sourcesContent":["/**\n * Read a Worker's LOCAL `.wrangler` state directly: the D1 index and the\n * per-game Durable Object databases, opened as plain SQLite files.\n *\n * This is a development and debugging surface, never a runtime one. Nothing here\n * runs on Workers: it is Node reading files that `wrangler dev` left on disk,\n * which is why it lives in the testkit (a `devDependency`) behind its own\n * subpath, and why it reads them through Node's built-in `node:sqlite` rather\n * than a driver package. `better-sqlite3` would work and was tried first, but\n * `drizzle-orm` declares it as an OPTIONAL PEER: installing it anywhere in the\n * workspace re-resolves drizzle's peer set for every importer, which changed the\n * types `packages/server` compiles against and broke its `dts` build. A\n * debugging tool is not worth perturbing the engine's own dependency graph, and\n * the built-in module costs nothing: no native build, no `allowBuilds` opt-in,\n * and no dependency at all.\n *\n * Two stores, two different questions, and the reason a game-shaped reader\n * exists at all:\n *\n * - **D1 is the index.** Discovery, history, ratings, identity. Its `games` row\n * is a display read-model written by the DO as a fire-and-forget mirror.\n * - **The game's DO is the session.** One SQLite database per game, holding the\n * authoritative status, roster and the append-only transition log.\n *\n * A question like \"why is this game not starting\" therefore cannot be answered\n * from either store alone, and answering it from a generic table browser means\n * knowing which of the two to distrust. {@link LocalStore.game} joins them and\n * reports the disagreement as a value, so a stale mirror reads as a stale\n * mirror rather than as a mystery.\n *\n * Every open is read-only, so this is safe to run against a live `wrangler dev`.\n *\n * @module @eigeninteractive/testkit/local-state\n */\n\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\n\n/** Where `wrangler dev` persists local resources, relative to a Worker root. */\nconst STATE_SUFFIX = join(\".wrangler\", \"state\", \"v3\");\n\n/** Miniflare's own bookkeeping database, present in every resource directory\n * alongside the real ones. It holds `_cf_ALARM`, not user data. */\nconst METADATA_FILE = \"metadata.sqlite\";\n\n/** The table Miniflare writes into every Durable Object database, holding the\n * `idFromName` name the object was addressed by. It is what makes the hashed\n * filename reversible: the engine names a game's DO by its `gameId`, and the\n * hash is one-way, so this row is the only local file-to-game map there is. */\nconst DO_NAME_TABLE = \"__miniflare_do_name\";\n\n/** A local Durable Object database: one game's whole session. */\nexport interface LocalDurableObject {\n /** The Durable Object class, parsed from the directory Miniflare names\n * `<worker>-<ClassName>`. */\n className: string;\n /** The hex `DurableObjectId`, which is the filename: `idFromName(name)`\n * hashed. Not the game id, and not reversible without {@link name}. */\n id: string;\n /** The name the object was addressed by, which for a game DO is its\n * `gameId`. Null only if the object was created by unique id rather than by\n * name, which the engine never does. */\n name: string | null;\n /** Absolute path to the SQLite file. */\n file: string;\n}\n\n/** What a bound parameter may be: SQLite's own value domain, spelled out here\n * rather than re-exported from `node:sqlite` so the public signature does not\n * depend on a built-in module's type names. */\nexport type LocalQueryParam = string | number | bigint | null | Uint8Array;\n\n/** A row of Miniflare's alarm table: the DO's armed alarm, which for a game is\n * its turn deadline plus grace. An absent row means no timer is armed, which is\n * correct for an untimed game and a bug for a timed one mid-turn. */\nexport interface LocalAlarm {\n actorId: string;\n actorName: string | null;\n scheduledTime: number | null;\n}\n\n/** The engine's `games` row: the D1 index entry, a display mirror of the DO. */\nexport interface LocalGameRow {\n id: string;\n createdBy: string | null;\n status: string;\n access: string;\n schemaVersion: number;\n config: unknown;\n turnSeconds: number | null;\n budgetSeconds: number | null;\n incrementSeconds: number | null;\n rated: boolean;\n ratingPool: string | null;\n minPlayers: number;\n maxPlayers: number;\n shortCode: string;\n pendingPlayers: number[] | null;\n turnDeadline: number | null;\n outcomes: unknown;\n finishId: string | null;\n finishedAt: number | null;\n archivedAt: number | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/** One seat, from either store. `username` is filled in from D1's `users` and\n * `bots` tables when the reader has an index to join against. */\nexport interface LocalSeat {\n playerIndex: number;\n userId: string | null;\n botId: string | null;\n type: string;\n username: string | null;\n displayName: string | null;\n}\n\n/** The DO's single `meta` row: the authoritative session header. */\nexport interface LocalGameMeta {\n gameId: string;\n status: string;\n access: string;\n schemaVersion: number;\n config: unknown;\n turnSeconds: number | null;\n budgetSeconds: number | null;\n incrementSeconds: number | null;\n rated: boolean;\n ratingPool: string | null;\n minPlayers: number;\n maxPlayers: number;\n createdBy: string | null;\n rngSeed: string | null;\n}\n\n/** One committed transition, decoded. `action` is null at v0 (the start). */\nexport interface LocalTransition {\n version: number;\n state: unknown;\n action: { type: string; kind: string; playerIndex: number | null; data: unknown } | null;\n pending: number[];\n deadline: number | null;\n playerTimes: number[] | null;\n turnStartedAt: number | null;\n /** Which seats hold a stored frame at this version. Empty after the finish\n * compaction, which empties `frames` and leaves replay to re-projection. */\n frameSeats: number[];\n}\n\n/** Why the game is where it is, and what would move it. Derived rather than\n * stored: this is the sentence a developer actually wants, and every input to\n * it is already on screen above it. */\nexport interface LocalGameDiagnosis {\n /** One line naming the next expected event, or the reason there is none. */\n summary: string;\n /** Disagreements between the D1 mirror and the DO's own truth. A non-empty\n * list means a mirror write was lost, since the DO is authoritative. */\n mirrorDrift: string[];\n}\n\n/** Everything both stores know about one game. */\nexport interface LocalGameView {\n gameId: string;\n /** The D1 index row, or null when D1 has no such game: either the id came\n * from a DO name and the index was wiped, or the create write never landed. */\n index: LocalGameRow | null;\n /** D1's `participants`, which mirror the DO's roster for display. */\n indexSeats: LocalSeat[];\n /** The DO file, or null when no Durable Object exists for this game yet.\n * That is the normal state of a created-but-never-touched lobby: the DO is\n * created lazily by the first command or socket. */\n durableObject: LocalDurableObject | null;\n meta: LocalGameMeta | null;\n roster: LocalSeat[];\n transitions: LocalTransition[];\n /** Recorded `commandId` dedupe entries, newest first. */\n commands: { commandId: string; createdAt: number; result: unknown }[];\n /** Unapplied finish rows: a surviving row means the D1 finish apply has not\n * succeeded and the admin re-poke is the recovery. */\n outbox: { finishId: string; createdAt: number; outcomes: unknown }[];\n alarm: LocalAlarm | null;\n diagnosis: LocalGameDiagnosis;\n}\n\n/**\n * Walk up from `from` to the nearest directory holding `.wrangler/state/v3`.\n *\n * Walking rather than requiring the Worker root means this works from anywhere\n * inside a game repository, including the app half of a two-package layout,\n * where the state lives a directory or two above the shell's cwd.\n */\nexport function findWranglerState(from: string = process.cwd()): string | null {\n let dir = resolve(from);\n for (;;) {\n const candidate = join(dir, STATE_SUFFIX);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/**\n * Open a SQLite file for reading only.\n *\n * Read-only is what makes this safe to point at a running `wrangler dev`: no\n * writer lock is taken and no journal is touched. The fallback matters because\n * these databases are in WAL mode, and a read-only connection cannot recover a\n * hot journal or create the `-shm` file it needs; when `wrangler dev` is not\n * running and left one behind, the first open fails and a writable handle is\n * the only way in. Nothing here issues anything but `SELECT`, so the widened\n * handle stays as harmless as the narrow one. The existence check comes first\n * because that fallback would otherwise CREATE a database rather than report a\n * missing one.\n */\nexport function openSqlite(file: string): DatabaseSync {\n if (!existsSync(file)) throw new Error(`no SQLite database at ${file}`);\n try {\n return new DatabaseSync(file, { readOnly: true });\n } catch {\n return new DatabaseSync(file);\n }\n}\n\nfunction sqliteFilesIn(dir: string): string[] {\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((entry) => entry.endsWith(\".sqlite\") && entry !== METADATA_FILE)\n .map((entry) => join(dir, entry));\n}\n\nfunction tableExists(db: DatabaseSync, table: string): boolean {\n const row = db.prepare(\"select 1 from sqlite_master where type = 'table' and name = ?\").get(table);\n return row !== undefined;\n}\n\n/** Parse a JSON text column, tolerating a value some other writer left\n * un-encoded. A debugging reader that throws on one malformed cell is useless\n * precisely when it is needed. */\nfunction json(value: unknown): unknown {\n if (typeof value !== \"string\") return value ?? null;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n}\n\nfunction numbers(value: unknown): number[] {\n const parsed = json(value);\n return Array.isArray(parsed) ? parsed.filter((n): n is number => typeof n === \"number\") : [];\n}\n\nfunction bool(value: unknown): boolean {\n return value === 1 || value === true;\n}\n\n/**\n * A Worker's local state, with its databases opened lazily and held open.\n *\n * Stateful because it owns file handles: construct one, ask it questions, and\n * {@link close} it. A single instance is the unit both the CLI and a test use,\n * so neither has to know which file backs which resource.\n */\nexport class LocalStore {\n /** Absolute path to `.wrangler/state/v3`. */\n readonly root: string;\n\n #d1: { file: string; db: DatabaseSync } | null | undefined;\n readonly #open = new Map<string, DatabaseSync>();\n\n private constructor(root: string) {\n this.root = root;\n }\n\n /**\n * Locate and open the state under `from`, or throw with the one thing the\n * caller can act on: that no Worker has been run here yet.\n */\n static open(from?: string): LocalStore {\n const root = findWranglerState(from);\n if (root === null) {\n throw new Error(`no .wrangler/state/v3 found at or above ${resolve(from ?? process.cwd())}. Run the Worker once (\\`pnpm dev\\`) so it has local state to read.`);\n }\n return new LocalStore(root);\n }\n\n #handle(file: string): DatabaseSync {\n const existing = this.#open.get(file);\n if (existing !== undefined) return existing;\n const db = openSqlite(file);\n this.#open.set(file, db);\n return db;\n }\n\n /**\n * The engine's D1 database, identified by its schema rather than its name.\n *\n * Miniflare names the file after a hash of the database, so there is no\n * mapping back to the binding. Recognising the engine's own `games` table\n * instead is both simpler and more honest about what the caller wants: the\n * database that has games in it, whatever the binding was called.\n */\n d1(): { file: string; db: DatabaseSync } | null {\n if (this.#d1 !== undefined) return this.#d1;\n for (const file of sqliteFilesIn(join(this.root, \"d1\", \"miniflare-D1DatabaseObject\"))) {\n const db = this.#handle(file);\n if (tableExists(db, \"games\")) {\n this.#d1 = { file, db };\n return this.#d1;\n }\n }\n this.#d1 = null;\n return null;\n }\n\n /**\n * Every local Durable Object database, newest-modified first.\n *\n * The directory is `<worker>-<ClassName>`, and a worker name may itself\n * contain a dash, so the class is everything after the LAST one. Ordering by\n * mtime puts the game you just touched at the top, which is nearly always the\n * one being debugged.\n */\n durableObjects(): LocalDurableObject[] {\n const base = join(this.root, \"do\");\n if (!existsSync(base)) return [];\n const found: LocalDurableObject[] = [];\n for (const namespace of readdirSync(base)) {\n const dir = join(base, namespace);\n if (!statSync(dir).isDirectory()) continue;\n const className = namespace.slice(namespace.lastIndexOf(\"-\") + 1);\n for (const file of sqliteFilesIn(dir)) {\n const db = this.#handle(file);\n const name = tableExists(db, DO_NAME_TABLE) ? ((db.prepare(`select name from ${DO_NAME_TABLE} limit 1`).get() as { name?: string } | undefined)?.name ?? null) : null;\n found.push({ className, id: file.slice(file.lastIndexOf(\"/\") + 1, -\".sqlite\".length), name, file });\n }\n }\n return found.sort((a, b) => statSync(b.file).mtimeMs - statSync(a.file).mtimeMs);\n }\n\n /** The armed alarm for a Durable Object, from its namespace's metadata\n * database. Matched on either column because the id is what Miniflare keys\n * by and the name is what a human recognises. */\n alarm(target: LocalDurableObject): LocalAlarm | null {\n const file = join(dirname(target.file), METADATA_FILE);\n if (!existsSync(file)) return null;\n const db = this.#handle(file);\n if (!tableExists(db, \"_cf_ALARM\")) return null;\n const row = db.prepare(\"select actor_id, actor_name, scheduled_time from _cf_ALARM where actor_id = ? or actor_name = ?\").get(target.id, target.name ?? target.id) as { actor_id: string; actor_name: string | null; scheduled_time: number | null } | undefined;\n return row === undefined ? null : { actorId: row.actor_id, actorName: row.actor_name, scheduledTime: row.scheduled_time };\n }\n\n /** The D1 index rows, newest-updated first. */\n games(options: { status?: string; limit?: number } = {}): LocalGameRow[] {\n const d1 = this.d1();\n if (d1 === null) return [];\n const where = options.status === undefined ? \"\" : \"where status = ?\";\n const params = options.status === undefined ? [] : [options.status];\n const rows = d1.db.prepare(`select * from games ${where} order by updated_at desc limit ?`).all(...params, options.limit ?? 50) as Record<string, unknown>[];\n return rows.map(toGameRow);\n }\n\n /**\n * Resolve a game reference the way a developer will type it: a full id, a\n * six-character short code, or an id prefix long enough to be unique.\n *\n * D1 is tried first because it can match a short code, then the DO names, so\n * a game whose index row was wiped is still reachable by id.\n */\n resolveGameId(ref: string): string | null {\n const d1 = this.d1();\n if (d1 !== null) {\n const exact = d1.db.prepare(\"select id from games where id = ? or short_code = ?\").get(ref, ref.toUpperCase()) as { id: string } | undefined;\n if (exact !== undefined) return exact.id;\n const prefixed = d1.db.prepare(\"select id from games where id like ? limit 2\").all(`${ref}%`) as { id: string }[];\n if (prefixed.length === 1) return prefixed[0].id;\n if (prefixed.length > 1) throw new Error(`\"${ref}\" matches more than one game; use a longer prefix`);\n }\n const named = this.durableObjects().filter((o) => o.name !== null && (o.name === ref || o.name.startsWith(ref)));\n if (named.length === 1) return named[0].name;\n if (named.length > 1) throw new Error(`\"${ref}\" matches more than one game; use a longer prefix`);\n return null;\n }\n\n /** The Durable Object holding a game's session, or null if none exists yet. */\n gameDurableObject(gameId: string): LocalDurableObject | null {\n return this.durableObjects().find((o) => o.name === gameId) ?? null;\n }\n\n /** Identity for a set of user and bot ids, for rendering seats as names. */\n #identities(userIds: string[], botIds: string[]): Map<string, { username: string; displayName: string }> {\n const out = new Map<string, { username: string; displayName: string }>();\n const d1 = this.d1();\n if (d1 === null) return out;\n for (const [table, ids] of [\n [\"users\", userIds],\n [\"bots\", botIds],\n ] as const) {\n if (ids.length === 0) continue;\n const placeholders = ids.map(() => \"?\").join(\",\");\n const rows = d1.db.prepare(`select id, username, display_name from ${table} where id in (${placeholders})`).all(...ids) as { id: string; username: string; display_name: string }[];\n for (const row of rows) out.set(row.id, { username: row.username, displayName: row.display_name });\n }\n return out;\n }\n\n #seats(rows: { player_index: number; user_id: string | null; bot_id: string | null; type: string }[]): LocalSeat[] {\n const identities = this.#identities(\n rows.map((r) => r.user_id).filter((id): id is string => id !== null),\n rows.map((r) => r.bot_id).filter((id): id is string => id !== null),\n );\n return rows.map((row) => {\n const identity = identities.get(row.user_id ?? row.bot_id ?? \"\");\n return { playerIndex: row.player_index, userId: row.user_id, botId: row.bot_id, type: row.type, username: identity?.username ?? null, displayName: identity?.displayName ?? null };\n });\n }\n\n /**\n * Join both stores for one game: the index row, the session, the transition\n * log, and the derived diagnosis.\n *\n * Returns null only when neither store has heard of the game.\n */\n game(ref: string): LocalGameView | null {\n const gameId = this.resolveGameId(ref);\n if (gameId === null) return null;\n\n const d1 = this.d1();\n const indexRow = d1 === null ? undefined : (d1.db.prepare(\"select * from games where id = ?\").get(gameId) as Record<string, unknown> | undefined);\n const index = indexRow === undefined ? null : toGameRow(indexRow);\n const indexSeats =\n d1 === null\n ? []\n : this.#seats(\n d1.db.prepare(\"select player_index, user_id, bot_id, type from participants where game_id = ? order by player_index\").all(gameId) as {\n player_index: number;\n user_id: string | null;\n bot_id: string | null;\n type: string;\n }[],\n );\n\n const durableObject = this.gameDurableObject(gameId);\n let meta: LocalGameMeta | null = null;\n let roster: LocalSeat[] = [];\n let transitions: LocalTransition[] = [];\n let commands: LocalGameView[\"commands\"] = [];\n let outbox: LocalGameView[\"outbox\"] = [];\n\n if (durableObject !== null) {\n const db = this.#handle(durableObject.file);\n if (tableExists(db, \"meta\")) {\n const row = db.prepare(\"select * from meta where id = 1\").get() as Record<string, unknown> | undefined;\n if (row !== undefined) meta = toMeta(row);\n }\n if (tableExists(db, \"roster\")) {\n roster = this.#seats(db.prepare(\"select player_index, user_id, bot_id, type from roster order by player_index\").all() as { player_index: number; user_id: string | null; bot_id: string | null; type: string }[]);\n }\n if (tableExists(db, \"transitions\")) {\n const frameSeats = new Map<number, number[]>();\n if (tableExists(db, \"frames\")) {\n for (const row of db.prepare(\"select version, player_index from frames order by version, player_index\").all() as { version: number; player_index: number }[]) {\n frameSeats.set(row.version, [...(frameSeats.get(row.version) ?? []), row.player_index]);\n }\n }\n transitions = (db.prepare(\"select * from transitions order by version\").all() as Record<string, unknown>[]).map((row) => toTransition(row, frameSeats));\n }\n if (tableExists(db, \"commands\")) {\n commands = (db.prepare(\"select command_id, response, created_at from commands order by created_at desc\").all() as { command_id: string; response: unknown; created_at: number }[]).map((row) => ({\n commandId: row.command_id,\n createdAt: row.created_at,\n result: json(row.response),\n }));\n }\n if (tableExists(db, \"outbox\")) {\n outbox = (db.prepare(\"select finish_id, outcomes, created_at from outbox order by created_at\").all() as { finish_id: string; outcomes: unknown; created_at: number }[]).map((row) => ({\n finishId: row.finish_id,\n createdAt: row.created_at,\n outcomes: json(row.outcomes),\n }));\n }\n }\n\n const alarm = durableObject === null ? null : this.alarm(durableObject);\n const view: Omit<LocalGameView, \"diagnosis\"> = { gameId, index, indexSeats, durableObject, meta, roster, transitions, commands, outbox, alarm };\n return { ...view, diagnosis: diagnose(view) };\n }\n\n /** Run a read-only statement against D1 or one game's Durable Object: the\n * escape hatch for a question this reader does not model. */\n query(sql: string, options: { game?: string; params?: LocalQueryParam[] } = {}): Record<string, unknown>[] {\n const db = options.game === undefined ? this.d1()?.db : this.#gameDb(options.game);\n if (db === undefined || db === null) {\n throw new Error(options.game === undefined ? \"no local D1 database found in this Worker's state\" : `no local Durable Object found for game ${options.game}`);\n }\n return db.prepare(sql).all(...(options.params ?? [])) as Record<string, unknown>[];\n }\n\n /** Table names and row counts, for D1 or one game's Durable Object. */\n tables(options: { game?: string } = {}): { name: string; rows: number }[] {\n const db = options.game === undefined ? this.d1()?.db : this.#gameDb(options.game);\n if (db === undefined || db === null) return [];\n const names = (db.prepare(\"select name from sqlite_master where type = 'table' order by name\").all() as { name: string }[]).map((r) => r.name);\n return names.map((name) => ({ name, rows: (db.prepare(`select count(*) as n from \"${name}\"`).get() as { n: number }).n }));\n }\n\n #gameDb(ref: string): DatabaseSync | null {\n const gameId = this.resolveGameId(ref);\n const target = gameId === null ? null : this.gameDurableObject(gameId);\n return target === null ? null : this.#handle(target.file);\n }\n\n /** Release every open handle. */\n close(): void {\n for (const db of this.#open.values()) db.close();\n this.#open.clear();\n this.#d1 = undefined;\n }\n}\n\nfunction toGameRow(row: Record<string, unknown>): LocalGameRow {\n return {\n id: row.id as string,\n createdBy: (row.created_by as string | null) ?? null,\n status: row.status as string,\n access: row.access as string,\n schemaVersion: row.schema_version as number,\n config: json(row.config),\n turnSeconds: (row.turn_seconds as number | null) ?? null,\n budgetSeconds: (row.budget_seconds as number | null) ?? null,\n incrementSeconds: (row.increment_seconds as number | null) ?? null,\n rated: bool(row.rated),\n ratingPool: (row.rating_pool as string | null) ?? null,\n minPlayers: row.min_players as number,\n maxPlayers: row.max_players as number,\n shortCode: row.short_code as string,\n pendingPlayers: row.pending_players === null || row.pending_players === undefined ? null : numbers(row.pending_players),\n turnDeadline: (row.turn_deadline as number | null) ?? null,\n outcomes: json(row.outcomes),\n finishId: (row.finish_id as string | null) ?? null,\n finishedAt: (row.finished_at as number | null) ?? null,\n archivedAt: (row.archived_at as number | null) ?? null,\n createdAt: row.created_at as number,\n updatedAt: row.updated_at as number,\n };\n}\n\nfunction toMeta(row: Record<string, unknown>): LocalGameMeta {\n return {\n gameId: row.game_id as string,\n status: row.status as string,\n access: row.access as string,\n schemaVersion: row.schema_version as number,\n config: json(row.config),\n turnSeconds: (row.turn_seconds as number | null) ?? null,\n budgetSeconds: (row.budget_seconds as number | null) ?? null,\n incrementSeconds: (row.increment_seconds as number | null) ?? null,\n rated: bool(row.rated),\n ratingPool: (row.rating_pool as string | null) ?? null,\n minPlayers: row.min_players as number,\n maxPlayers: row.max_players as number,\n createdBy: (row.created_by as string | null) ?? null,\n rngSeed: (row.rng_seed as string | null) ?? null,\n };\n}\n\nfunction toTransition(row: Record<string, unknown>, frameSeats: Map<number, number[]>): LocalTransition {\n const version = row.version as number;\n const action = json(row.action) as LocalTransition[\"action\"];\n return {\n version,\n state: json(row.state),\n action: action ?? null,\n pending: numbers(row.pending),\n deadline: (row.deadline as number | null) ?? null,\n playerTimes: row.player_times === null || row.player_times === undefined ? null : numbers(row.player_times),\n turnStartedAt: (row.turn_started_at as number | null) ?? null,\n frameSeats: frameSeats.get(version) ?? [],\n };\n}\n\n/**\n * Say what the game is waiting for, and whether the two stores agree.\n *\n * The engine's statuses each have exactly one thing that moves them, so this is\n * a total function over `status` rather than a heuristic. The cases developers\n * actually hit are the first two: a lobby that will not fill, and a full lobby\n * whose creator has not pressed start.\n */\nfunction diagnose(view: Omit<LocalGameView, \"diagnosis\">): LocalGameDiagnosis {\n const status = view.meta?.status ?? view.index?.status ?? \"unknown\";\n const seated = (view.meta === null ? view.indexSeats : view.roster).length;\n const min = view.meta?.minPlayers ?? view.index?.minPlayers ?? 0;\n const max = view.meta?.maxPlayers ?? view.index?.maxPlayers ?? 0;\n const latest = view.transitions.at(-1) ?? null;\n\n const mirrorDrift: string[] = [];\n if (view.index !== null && view.meta !== null) {\n if (view.index.status !== view.meta.status) {\n mirrorDrift.push(`D1 says status=${view.index.status}, the Durable Object says ${view.meta.status}. The DO is authoritative; a mirror write was lost.`);\n }\n if (view.indexSeats.length !== view.roster.length) {\n mirrorDrift.push(`D1 mirrors ${view.indexSeats.length} seat(s), the Durable Object holds ${view.roster.length}.`);\n }\n const mirroredPending = view.index.pendingPlayers ?? null;\n const actualPending = latest?.pending ?? null;\n if (status === \"active\" && JSON.stringify(mirroredPending) !== JSON.stringify(actualPending)) {\n mirrorDrift.push(`D1 mirrors pendingPlayers=${JSON.stringify(mirroredPending)}, the newest transition has ${JSON.stringify(actualPending)}.`);\n }\n }\n if (view.index === null && view.meta !== null) {\n mirrorDrift.push(\"no D1 index row for a game whose Durable Object exists; discovery and history lists cannot show it.\");\n }\n\n let summary: string;\n switch (status) {\n case \"waiting\":\n summary = `waiting for players: ${seated}/${min} needed to become ready (${max} max). The next event is a join, or add-bot from the creator.`;\n break;\n case \"ready\":\n summary = `ready and not started: ${seated}/${max} seats filled. Nothing happens until the CREATOR calls POST /api/engine/games/${view.gameId}/start. A start is explicit and creator-only; filling the lobby does not start a game.`;\n break;\n case \"active\":\n if (latest === null) {\n summary = \"status is active but no transition is committed, which should be impossible: a start commits v0 in the same transaction that sets the status.\";\n } else {\n const waitingOn = latest.pending.length === 0 ? \"nobody, which means the game should have finished at this version\" : `seat(s) ${latest.pending.join(\", \")}`;\n const timer = latest.deadline === null ? \"untimed, so no alarm is armed and only a move or a forfeit moves it\" : `deadline ${new Date(latest.deadline).toISOString()}`;\n summary = `at v${latest.version}, waiting on ${waitingOn}. ${timer}.`;\n }\n break;\n case \"finished\":\n summary = view.outbox.length > 0 ? `finished, but ${view.outbox.length} outbox row(s) survive: the D1 finish apply has not succeeded, so ratings and history are not published yet.` : `finished at v${latest?.version ?? \"?\"}; the outbox is clear, so D1 has the outcomes.`;\n break;\n case \"aborted\":\n summary = \"aborted: cancelled by its creator, reaped as abandoned, or torn down by an account purge. Nothing moves an aborted game.\";\n break;\n default:\n summary = view.durableObject === null ? \"created in D1 but its Durable Object does not exist yet. That is normal for a lobby nobody has touched: the DO is created lazily by the first command or socket.\" : `unrecognised status ${status}.`;\n }\n\n return { summary, mirrorDrift };\n}\n"],"mappings":"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","names":[]}
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  checkConfiguredGameContract,
4
4
  emitConfiguredGameContract
5
- } from "./chunk-SVC3562K.js";
5
+ } from "./chunk-JJW4WLTM.js";
6
6
 
7
7
  // src/contract-cli.ts
8
- import { parseArgs } from "util";
8
+ import { parseArgs } from "node:util";
9
9
  try {
10
10
  const { values } = parseArgs({
11
11
  strict: true,
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  gameContractJson,
12
12
  parseTwinFixtureFile,
13
13
  twinFixtureTests
14
- } from "./chunk-SVC3562K.js";
14
+ } from "./chunk-JJW4WLTM.js";
15
15
 
16
16
  // src/kernel-scenarios.ts
17
17
  import { assertHookPayload } from "@eigeninteractive/kernel";
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ LocalStore
4
+ } from "./chunk-VXWO4LVS.js";
5
+
6
+ // src/inspect-cli.ts
7
+ import { parseArgs } from "node:util";
8
+ var HELP = `Usage: eigen-inspect <command> [options]
9
+
10
+ Read the local .wrangler state of an EigenInteractive Worker: the D1 index and
11
+ each game's own Durable Object database.
12
+
13
+ Commands:
14
+ games The D1 index, newest first
15
+ game <id|code|prefix> One game across both stores, with its timeline
16
+ do Every local Durable Object, mapped back to its game
17
+ players Local users and bots, by id
18
+ tables [--game <ref>] Table names and row counts
19
+ sql <query> [--game <ref>]
20
+ Run a read-only statement against D1 or one game's DO
21
+
22
+ Options:
23
+ --dir <path> Start the search for .wrangler/state here (default: cwd)
24
+ --status <s> games: filter by status
25
+ --limit <n> games: row cap (default 20)
26
+ --game <ref> tables/sql: target one game's Durable Object instead of D1
27
+ --json Emit JSON instead of a table
28
+ -h, --help Show this help
29
+
30
+ Every database is opened read-only, so this is safe to run while the Worker is
31
+ running. Wrangler's own browser for the same files is \`e\` in \`wrangler dev\`.`;
32
+ function relative(ms) {
33
+ if (ms === null) return "never";
34
+ const delta = Date.now() - ms;
35
+ const future = delta < 0;
36
+ const seconds = Math.round(Math.abs(delta) / 1e3);
37
+ const [value, unit] = seconds < 60 ? [seconds, "s"] : seconds < 3600 ? [Math.round(seconds / 60), "m"] : seconds < 86400 ? [Math.round(seconds / 3600), "h"] : [Math.round(seconds / 86400), "d"];
38
+ return future ? `in ${value}${unit}` : `${value}${unit} ago`;
39
+ }
40
+ function table(rows) {
41
+ if (rows.length === 0) return "(no rows)";
42
+ const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
43
+ const cell = (value) => value === null || value === void 0 ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
44
+ const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));
45
+ const line = (values) => values.map((value, i) => value.padEnd(widths[i])).join(" ").trimEnd();
46
+ return [line(columns), line(widths.map((w) => "-".repeat(w))), ...rows.map((row) => line(columns.map((column, i) => cell(row[column]).padEnd(widths[i]))))].join("\n");
47
+ }
48
+ function short(id, length = 8) {
49
+ return id === null ? "" : id.length <= length ? id : `${id.slice(0, length)}\u2026`;
50
+ }
51
+ function seatLabel(seat) {
52
+ const identity = seat.username ?? short(seat.userId ?? seat.botId);
53
+ return `${seat.playerIndex}:${identity || "(empty)"}${seat.type === "bot" ? " (bot)" : ""}`;
54
+ }
55
+ function timingLabel(row) {
56
+ if (row.turnSeconds !== null) return `${row.turnSeconds}s per turn`;
57
+ if (row.budgetSeconds !== null) return `${row.budgetSeconds}s budget${row.incrementSeconds === null ? "" : ` +${row.incrementSeconds}s`}`;
58
+ return "untimed";
59
+ }
60
+ function gamesTable(rows) {
61
+ return rows.map((row) => ({
62
+ id: short(row.id, 8),
63
+ code: row.shortCode,
64
+ status: row.status,
65
+ access: row.access,
66
+ seats: `${row.minPlayers}-${row.maxPlayers}`,
67
+ rated: row.rated ? "yes" : "no",
68
+ timing: timingLabel(row),
69
+ pending: row.pendingPlayers === null ? "" : JSON.stringify(row.pendingPlayers),
70
+ updated: relative(row.updatedAt)
71
+ }));
72
+ }
73
+ function renderGame(view) {
74
+ const out = [];
75
+ const index = view.index;
76
+ out.push(`Game ${view.gameId}${index === null ? "" : ` ${index.shortCode}`}`);
77
+ if (index === null) {
78
+ out.push("", "D1 index (no games row)");
79
+ } else {
80
+ out.push(
81
+ "",
82
+ `D1 index status=${index.status} access=${index.access} rated=${index.rated ? "yes" : "no"} schema=v${index.schemaVersion}`,
83
+ ` seats ${index.minPlayers}-${index.maxPlayers} ${timingLabel(index)} created ${relative(index.createdAt)} updated ${relative(index.updatedAt)}`,
84
+ `config ${JSON.stringify(index.config)}`,
85
+ `participants ${view.indexSeats.length === 0 ? "(none)" : view.indexSeats.map(seatLabel).join(" ")}`
86
+ );
87
+ if (index.finishedAt !== null) out.push(`finished ${relative(index.finishedAt)} outcomes ${JSON.stringify(index.outcomes)}`);
88
+ }
89
+ out.push("");
90
+ if (view.durableObject === null) {
91
+ out.push("Durable Object (none: no command or socket has touched this game yet)");
92
+ } else {
93
+ out.push(`Durable Object ${short(view.durableObject.id, 12)} ${view.durableObject.file}`);
94
+ const meta = view.meta;
95
+ if (meta === null) {
96
+ out.push("DO meta (no meta row: the object exists but never initialised)");
97
+ } else {
98
+ out.push(`DO meta status=${meta.status} seats ${meta.minPlayers}-${meta.maxPlayers} createdBy=${short(meta.createdBy)} rngSeed=${meta.rngSeed === null ? "(unset, so no start has committed)" : short(meta.rngSeed, 12)}`);
99
+ }
100
+ out.push(`roster ${view.roster.length === 0 ? "(empty)" : view.roster.map(seatLabel).join(" ")}`);
101
+ out.push(`alarm ${view.alarm === null || view.alarm.scheduledTime === null ? "none armed" : `${new Date(view.alarm.scheduledTime).toISOString()} (${relative(view.alarm.scheduledTime)})`}`);
102
+ out.push(`commands ${view.commands.length} recorded`);
103
+ if (view.outbox.length > 0) out.push(`outbox ${view.outbox.length} UNAPPLIED finish row(s): ${view.outbox.map((row) => short(row.finishId)).join(", ")}`);
104
+ }
105
+ out.push("", "Timeline");
106
+ if (view.transitions.length === 0) {
107
+ out.push(" (no transitions: the game has not started, so there is no v0)");
108
+ } else {
109
+ for (const transition of view.transitions) {
110
+ const action = transition.action;
111
+ const cause = action === null ? "start" : `${action.kind}/${action.type}${action.playerIndex === null ? "" : ` seat ${action.playerIndex}`}`;
112
+ const payload = action === null || action.data === null || action.data === void 0 ? "" : ` ${JSON.stringify(action.data)}`;
113
+ out.push(` v${String(transition.version).padEnd(3)} ${cause}${payload}`);
114
+ out.push(` state ${JSON.stringify(transition.state)}`);
115
+ out.push(` pending ${JSON.stringify(transition.pending)} deadline ${transition.deadline === null ? "none" : relative(transition.deadline)} frames ${transition.frameSeats.length === 0 ? "(compacted)" : JSON.stringify(transition.frameSeats)}`);
116
+ }
117
+ }
118
+ out.push("", `Diagnosis ${view.diagnosis.summary}`);
119
+ for (const drift of view.diagnosis.mirrorDrift) out.push(`Mirror drift ${drift}`);
120
+ return out.join("\n");
121
+ }
122
+ function main() {
123
+ const { values, positionals } = parseArgs({
124
+ allowPositionals: true,
125
+ options: {
126
+ dir: { type: "string" },
127
+ status: { type: "string" },
128
+ limit: { type: "string" },
129
+ game: { type: "string" },
130
+ json: { type: "boolean" },
131
+ help: { type: "boolean", short: "h" }
132
+ }
133
+ });
134
+ const [command, ...rest] = positionals;
135
+ if (values.help === true || command === void 0) {
136
+ console.log(HELP);
137
+ return;
138
+ }
139
+ const emit = (value, text) => {
140
+ console.log(values.json === true ? JSON.stringify(value, null, 2) : text());
141
+ };
142
+ const store = LocalStore.open(values.dir);
143
+ try {
144
+ switch (command) {
145
+ case "games": {
146
+ const rows = store.games({ status: values.status, limit: values.limit === void 0 ? 20 : Number(values.limit) });
147
+ emit(rows, () => rows.length === 0 ? "no games in the local D1 index" : table(gamesTable(rows)));
148
+ return;
149
+ }
150
+ case "game": {
151
+ const ref = rest[0];
152
+ if (ref === void 0) throw new Error("game needs an id, short code, or id prefix");
153
+ const view = store.game(ref);
154
+ if (view === null) throw new Error(`no game matching "${ref}" in the local D1 index or in any local Durable Object`);
155
+ emit(view, () => renderGame(view));
156
+ return;
157
+ }
158
+ case "do": {
159
+ const objects = store.durableObjects().map((object) => ({ class: object.className, game: object.name ?? "(unnamed)", doId: short(object.id, 12), file: object.file.startsWith(store.root) ? object.file.slice(store.root.length + 1) : object.file }));
160
+ emit(objects, () => objects.length === 0 ? "no local Durable Objects: nothing has created a game session yet" : table(objects));
161
+ return;
162
+ }
163
+ case "players": {
164
+ const users = store.query("select id, username, display_name, is_anonymous, created_at from users order by created_at desc");
165
+ const bots = store.query("select id, username, display_name, type, schema_version from bots order by username");
166
+ emit({ users, bots }, () => `Users
167
+ ${table(users)}
168
+
169
+ Bots
170
+ ${table(bots)}`);
171
+ return;
172
+ }
173
+ case "tables": {
174
+ const rows = store.tables({ game: values.game });
175
+ emit(rows, () => table(rows));
176
+ return;
177
+ }
178
+ case "sql": {
179
+ const sql = rest.join(" ");
180
+ if (sql === "") throw new Error("sql needs a statement");
181
+ const rows = store.query(sql, { game: values.game });
182
+ emit(rows, () => table(rows));
183
+ return;
184
+ }
185
+ default:
186
+ throw new Error(`unknown command "${command}". Run eigen-inspect --help`);
187
+ }
188
+ } finally {
189
+ store.close();
190
+ }
191
+ }
192
+ try {
193
+ main();
194
+ } catch (error) {
195
+ console.error(`eigen-inspect: ${error instanceof Error ? error.message : String(error)}`);
196
+ process.exitCode = 1;
197
+ }
198
+ //# sourceMappingURL=inspect-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/inspect-cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `eigen-inspect`: read a Worker's local `.wrangler` state from the terminal.\n *\n * Deliberately narrow. Wrangler already ships a first-party browser for local\n * resources (press `e` in `wrangler dev`, or open `/cdn-cgi/explorer`), and\n * generic table browsing is its job, not this tool's. What that view cannot do\n * is answer a question in the engine's own terms, because a game's truth is\n * split across two stores on purpose: the D1 index and the game's own Durable\n * Object. This reads both, decodes the JSON columns, prints the transition log\n * as a timeline, and says what the game is waiting for.\n *\n * It also runs with no dev server, no browser and no account, and emits\n * `--json` for a script or an agent.\n */\n\nimport { parseArgs } from \"node:util\";\nimport type { LocalGameRow, LocalGameView, LocalSeat } from \"./local-state.js\";\nimport { LocalStore } from \"./local-state.js\";\n\nconst HELP = `Usage: eigen-inspect <command> [options]\n\nRead the local .wrangler state of an EigenInteractive Worker: the D1 index and\neach game's own Durable Object database.\n\nCommands:\n games The D1 index, newest first\n game <id|code|prefix> One game across both stores, with its timeline\n do Every local Durable Object, mapped back to its game\n players Local users and bots, by id\n tables [--game <ref>] Table names and row counts\n sql <query> [--game <ref>]\n Run a read-only statement against D1 or one game's DO\n\nOptions:\n --dir <path> Start the search for .wrangler/state here (default: cwd)\n --status <s> games: filter by status\n --limit <n> games: row cap (default 20)\n --game <ref> tables/sql: target one game's Durable Object instead of D1\n --json Emit JSON instead of a table\n -h, --help Show this help\n\nEvery database is opened read-only, so this is safe to run while the Worker is\nrunning. Wrangler's own browser for the same files is \\`e\\` in \\`wrangler dev\\`.`;\n\nfunction relative(ms: number | null): string {\n if (ms === null) return \"never\";\n const delta = Date.now() - ms;\n const future = delta < 0;\n const seconds = Math.round(Math.abs(delta) / 1000);\n const [value, unit] = seconds < 60 ? [seconds, \"s\"] : seconds < 3600 ? [Math.round(seconds / 60), \"m\"] : seconds < 86400 ? [Math.round(seconds / 3600), \"h\"] : [Math.round(seconds / 86400), \"d\"];\n return future ? `in ${value}${unit}` : `${value}${unit} ago`;\n}\n\n/** Render rows as an aligned table. Values are stringified defensively because\n * SQLite hands back whatever the column holds, including blobs and nulls. */\nfunction table(rows: Record<string, unknown>[]): string {\n if (rows.length === 0) return \"(no rows)\";\n const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];\n const cell = (value: unknown): string => (value === null || value === undefined ? \"\" : typeof value === \"object\" ? JSON.stringify(value) : String(value));\n const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));\n const line = (values: string[]): string =>\n values\n .map((value, i) => value.padEnd(widths[i]))\n .join(\" \")\n .trimEnd();\n return [line(columns), line(widths.map((w) => \"-\".repeat(w))), ...rows.map((row) => line(columns.map((column, i) => cell(row[column]).padEnd(widths[i]))))].join(\"\\n\");\n}\n\nfunction short(id: string | null, length = 8): string {\n return id === null ? \"\" : id.length <= length ? id : `${id.slice(0, length)}…`;\n}\n\nfunction seatLabel(seat: LocalSeat): string {\n const identity = seat.username ?? short(seat.userId ?? seat.botId);\n return `${seat.playerIndex}:${identity || \"(empty)\"}${seat.type === \"bot\" ? \" (bot)\" : \"\"}`;\n}\n\nfunction timingLabel(row: { turnSeconds: number | null; budgetSeconds: number | null; incrementSeconds: number | null }): string {\n if (row.turnSeconds !== null) return `${row.turnSeconds}s per turn`;\n if (row.budgetSeconds !== null) return `${row.budgetSeconds}s budget${row.incrementSeconds === null ? \"\" : ` +${row.incrementSeconds}s`}`;\n return \"untimed\";\n}\n\nfunction gamesTable(rows: LocalGameRow[]): Record<string, unknown>[] {\n return rows.map((row) => ({\n id: short(row.id, 8),\n code: row.shortCode,\n status: row.status,\n access: row.access,\n seats: `${row.minPlayers}-${row.maxPlayers}`,\n rated: row.rated ? \"yes\" : \"no\",\n timing: timingLabel(row),\n pending: row.pendingPlayers === null ? \"\" : JSON.stringify(row.pendingPlayers),\n updated: relative(row.updatedAt),\n }));\n}\n\n/** The `game` report: the whole session in reading order, index first, then the\n * authoritative session, then the log, then the one sentence that explains it. */\nfunction renderGame(view: LocalGameView): string {\n const out: string[] = [];\n const index = view.index;\n out.push(`Game ${view.gameId}${index === null ? \"\" : ` ${index.shortCode}`}`);\n\n if (index === null) {\n out.push(\"\", \"D1 index (no games row)\");\n } else {\n out.push(\n \"\",\n `D1 index status=${index.status} access=${index.access} rated=${index.rated ? \"yes\" : \"no\"} schema=v${index.schemaVersion}`,\n ` seats ${index.minPlayers}-${index.maxPlayers} ${timingLabel(index)} created ${relative(index.createdAt)} updated ${relative(index.updatedAt)}`,\n `config ${JSON.stringify(index.config)}`,\n `participants ${view.indexSeats.length === 0 ? \"(none)\" : view.indexSeats.map(seatLabel).join(\" \")}`,\n );\n if (index.finishedAt !== null) out.push(`finished ${relative(index.finishedAt)} outcomes ${JSON.stringify(index.outcomes)}`);\n }\n\n out.push(\"\");\n if (view.durableObject === null) {\n out.push(\"Durable Object (none: no command or socket has touched this game yet)\");\n } else {\n out.push(`Durable Object ${short(view.durableObject.id, 12)} ${view.durableObject.file}`);\n const meta = view.meta;\n if (meta === null) {\n out.push(\"DO meta (no meta row: the object exists but never initialised)\");\n } else {\n out.push(`DO meta status=${meta.status} seats ${meta.minPlayers}-${meta.maxPlayers} createdBy=${short(meta.createdBy)} rngSeed=${meta.rngSeed === null ? \"(unset, so no start has committed)\" : short(meta.rngSeed, 12)}`);\n }\n out.push(`roster ${view.roster.length === 0 ? \"(empty)\" : view.roster.map(seatLabel).join(\" \")}`);\n out.push(`alarm ${view.alarm === null || view.alarm.scheduledTime === null ? \"none armed\" : `${new Date(view.alarm.scheduledTime).toISOString()} (${relative(view.alarm.scheduledTime)})`}`);\n out.push(`commands ${view.commands.length} recorded`);\n if (view.outbox.length > 0) out.push(`outbox ${view.outbox.length} UNAPPLIED finish row(s): ${view.outbox.map((row) => short(row.finishId)).join(\", \")}`);\n }\n\n out.push(\"\", \"Timeline\");\n if (view.transitions.length === 0) {\n out.push(\" (no transitions: the game has not started, so there is no v0)\");\n } else {\n for (const transition of view.transitions) {\n const action = transition.action;\n const cause = action === null ? \"start\" : `${action.kind}/${action.type}${action.playerIndex === null ? \"\" : ` seat ${action.playerIndex}`}`;\n const payload = action === null || action.data === null || action.data === undefined ? \"\" : ` ${JSON.stringify(action.data)}`;\n out.push(` v${String(transition.version).padEnd(3)} ${cause}${payload}`);\n out.push(` state ${JSON.stringify(transition.state)}`);\n out.push(` pending ${JSON.stringify(transition.pending)} deadline ${transition.deadline === null ? \"none\" : relative(transition.deadline)} frames ${transition.frameSeats.length === 0 ? \"(compacted)\" : JSON.stringify(transition.frameSeats)}`);\n }\n }\n\n out.push(\"\", `Diagnosis ${view.diagnosis.summary}`);\n for (const drift of view.diagnosis.mirrorDrift) out.push(`Mirror drift ${drift}`);\n return out.join(\"\\n\");\n}\n\nfunction main(): void {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n dir: { type: \"string\" },\n status: { type: \"string\" },\n limit: { type: \"string\" },\n game: { type: \"string\" },\n json: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n\n const [command, ...rest] = positionals;\n if (values.help === true || command === undefined) {\n console.log(HELP);\n return;\n }\n\n const emit = (value: unknown, text: () => string): void => {\n console.log(values.json === true ? JSON.stringify(value, null, 2) : text());\n };\n\n const store = LocalStore.open(values.dir);\n try {\n switch (command) {\n case \"games\": {\n const rows = store.games({ status: values.status, limit: values.limit === undefined ? 20 : Number(values.limit) });\n emit(rows, () => (rows.length === 0 ? \"no games in the local D1 index\" : table(gamesTable(rows))));\n return;\n }\n case \"game\": {\n const ref = rest[0];\n if (ref === undefined) throw new Error(\"game needs an id, short code, or id prefix\");\n const view = store.game(ref);\n if (view === null) throw new Error(`no game matching \"${ref}\" in the local D1 index or in any local Durable Object`);\n emit(view, () => renderGame(view));\n return;\n }\n case \"do\": {\n // Paths print relative to the state root: the absolute prefix is the\n // same on every row and pushes the useful columns off the terminal.\n const objects = store.durableObjects().map((object) => ({ class: object.className, game: object.name ?? \"(unnamed)\", doId: short(object.id, 12), file: object.file.startsWith(store.root) ? object.file.slice(store.root.length + 1) : object.file }));\n emit(objects, () => (objects.length === 0 ? \"no local Durable Objects: nothing has created a game session yet\" : table(objects)));\n return;\n }\n case \"players\": {\n const users = store.query(\"select id, username, display_name, is_anonymous, created_at from users order by created_at desc\");\n const bots = store.query(\"select id, username, display_name, type, schema_version from bots order by username\");\n emit({ users, bots }, () => `Users\\n${table(users)}\\n\\nBots\\n${table(bots)}`);\n return;\n }\n case \"tables\": {\n const rows = store.tables({ game: values.game });\n emit(rows, () => table(rows));\n return;\n }\n case \"sql\": {\n const sql = rest.join(\" \");\n if (sql === \"\") throw new Error(\"sql needs a statement\");\n const rows = store.query(sql, { game: values.game });\n emit(rows, () => table(rows));\n return;\n }\n default:\n throw new Error(`unknown command \"${command}\". Run eigen-inspect --help`);\n }\n } finally {\n store.close();\n }\n}\n\ntry {\n main();\n} catch (error) {\n console.error(`eigen-inspect: ${error instanceof Error ? error.message : String(error)}`);\n process.exitCode = 1;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,279 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+
3
+ /**
4
+ * Read a Worker's LOCAL `.wrangler` state directly: the D1 index and the
5
+ * per-game Durable Object databases, opened as plain SQLite files.
6
+ *
7
+ * This is a development and debugging surface, never a runtime one. Nothing here
8
+ * runs on Workers: it is Node reading files that `wrangler dev` left on disk,
9
+ * which is why it lives in the testkit (a `devDependency`) behind its own
10
+ * subpath, and why it reads them through Node's built-in `node:sqlite` rather
11
+ * than a driver package. `better-sqlite3` would work and was tried first, but
12
+ * `drizzle-orm` declares it as an OPTIONAL PEER: installing it anywhere in the
13
+ * workspace re-resolves drizzle's peer set for every importer, which changed the
14
+ * types `packages/server` compiles against and broke its `dts` build. A
15
+ * debugging tool is not worth perturbing the engine's own dependency graph, and
16
+ * the built-in module costs nothing: no native build, no `allowBuilds` opt-in,
17
+ * and no dependency at all.
18
+ *
19
+ * Two stores, two different questions, and the reason a game-shaped reader
20
+ * exists at all:
21
+ *
22
+ * - **D1 is the index.** Discovery, history, ratings, identity. Its `games` row
23
+ * is a display read-model written by the DO as a fire-and-forget mirror.
24
+ * - **The game's DO is the session.** One SQLite database per game, holding the
25
+ * authoritative status, roster and the append-only transition log.
26
+ *
27
+ * A question like "why is this game not starting" therefore cannot be answered
28
+ * from either store alone, and answering it from a generic table browser means
29
+ * knowing which of the two to distrust. {@link LocalStore.game} joins them and
30
+ * reports the disagreement as a value, so a stale mirror reads as a stale
31
+ * mirror rather than as a mystery.
32
+ *
33
+ * Every open is read-only, so this is safe to run against a live `wrangler dev`.
34
+ *
35
+ * @module @eigeninteractive/testkit/local-state
36
+ */
37
+
38
+ /** A local Durable Object database: one game's whole session. */
39
+ interface LocalDurableObject {
40
+ /** The Durable Object class, parsed from the directory Miniflare names
41
+ * `<worker>-<ClassName>`. */
42
+ className: string;
43
+ /** The hex `DurableObjectId`, which is the filename: `idFromName(name)`
44
+ * hashed. Not the game id, and not reversible without {@link name}. */
45
+ id: string;
46
+ /** The name the object was addressed by, which for a game DO is its
47
+ * `gameId`. Null only if the object was created by unique id rather than by
48
+ * name, which the engine never does. */
49
+ name: string | null;
50
+ /** Absolute path to the SQLite file. */
51
+ file: string;
52
+ }
53
+ /** What a bound parameter may be: SQLite's own value domain, spelled out here
54
+ * rather than re-exported from `node:sqlite` so the public signature does not
55
+ * depend on a built-in module's type names. */
56
+ type LocalQueryParam = string | number | bigint | null | Uint8Array;
57
+ /** A row of Miniflare's alarm table: the DO's armed alarm, which for a game is
58
+ * its turn deadline plus grace. An absent row means no timer is armed, which is
59
+ * correct for an untimed game and a bug for a timed one mid-turn. */
60
+ interface LocalAlarm {
61
+ actorId: string;
62
+ actorName: string | null;
63
+ scheduledTime: number | null;
64
+ }
65
+ /** The engine's `games` row: the D1 index entry, a display mirror of the DO. */
66
+ interface LocalGameRow {
67
+ id: string;
68
+ createdBy: string | null;
69
+ status: string;
70
+ access: string;
71
+ schemaVersion: number;
72
+ config: unknown;
73
+ turnSeconds: number | null;
74
+ budgetSeconds: number | null;
75
+ incrementSeconds: number | null;
76
+ rated: boolean;
77
+ ratingPool: string | null;
78
+ minPlayers: number;
79
+ maxPlayers: number;
80
+ shortCode: string;
81
+ pendingPlayers: number[] | null;
82
+ turnDeadline: number | null;
83
+ outcomes: unknown;
84
+ finishId: string | null;
85
+ finishedAt: number | null;
86
+ archivedAt: number | null;
87
+ createdAt: number;
88
+ updatedAt: number;
89
+ }
90
+ /** One seat, from either store. `username` is filled in from D1's `users` and
91
+ * `bots` tables when the reader has an index to join against. */
92
+ interface LocalSeat {
93
+ playerIndex: number;
94
+ userId: string | null;
95
+ botId: string | null;
96
+ type: string;
97
+ username: string | null;
98
+ displayName: string | null;
99
+ }
100
+ /** The DO's single `meta` row: the authoritative session header. */
101
+ interface LocalGameMeta {
102
+ gameId: string;
103
+ status: string;
104
+ access: string;
105
+ schemaVersion: number;
106
+ config: unknown;
107
+ turnSeconds: number | null;
108
+ budgetSeconds: number | null;
109
+ incrementSeconds: number | null;
110
+ rated: boolean;
111
+ ratingPool: string | null;
112
+ minPlayers: number;
113
+ maxPlayers: number;
114
+ createdBy: string | null;
115
+ rngSeed: string | null;
116
+ }
117
+ /** One committed transition, decoded. `action` is null at v0 (the start). */
118
+ interface LocalTransition {
119
+ version: number;
120
+ state: unknown;
121
+ action: {
122
+ type: string;
123
+ kind: string;
124
+ playerIndex: number | null;
125
+ data: unknown;
126
+ } | null;
127
+ pending: number[];
128
+ deadline: number | null;
129
+ playerTimes: number[] | null;
130
+ turnStartedAt: number | null;
131
+ /** Which seats hold a stored frame at this version. Empty after the finish
132
+ * compaction, which empties `frames` and leaves replay to re-projection. */
133
+ frameSeats: number[];
134
+ }
135
+ /** Why the game is where it is, and what would move it. Derived rather than
136
+ * stored: this is the sentence a developer actually wants, and every input to
137
+ * it is already on screen above it. */
138
+ interface LocalGameDiagnosis {
139
+ /** One line naming the next expected event, or the reason there is none. */
140
+ summary: string;
141
+ /** Disagreements between the D1 mirror and the DO's own truth. A non-empty
142
+ * list means a mirror write was lost, since the DO is authoritative. */
143
+ mirrorDrift: string[];
144
+ }
145
+ /** Everything both stores know about one game. */
146
+ interface LocalGameView {
147
+ gameId: string;
148
+ /** The D1 index row, or null when D1 has no such game: either the id came
149
+ * from a DO name and the index was wiped, or the create write never landed. */
150
+ index: LocalGameRow | null;
151
+ /** D1's `participants`, which mirror the DO's roster for display. */
152
+ indexSeats: LocalSeat[];
153
+ /** The DO file, or null when no Durable Object exists for this game yet.
154
+ * That is the normal state of a created-but-never-touched lobby: the DO is
155
+ * created lazily by the first command or socket. */
156
+ durableObject: LocalDurableObject | null;
157
+ meta: LocalGameMeta | null;
158
+ roster: LocalSeat[];
159
+ transitions: LocalTransition[];
160
+ /** Recorded `commandId` dedupe entries, newest first. */
161
+ commands: {
162
+ commandId: string;
163
+ createdAt: number;
164
+ result: unknown;
165
+ }[];
166
+ /** Unapplied finish rows: a surviving row means the D1 finish apply has not
167
+ * succeeded and the admin re-poke is the recovery. */
168
+ outbox: {
169
+ finishId: string;
170
+ createdAt: number;
171
+ outcomes: unknown;
172
+ }[];
173
+ alarm: LocalAlarm | null;
174
+ diagnosis: LocalGameDiagnosis;
175
+ }
176
+ /**
177
+ * Walk up from `from` to the nearest directory holding `.wrangler/state/v3`.
178
+ *
179
+ * Walking rather than requiring the Worker root means this works from anywhere
180
+ * inside a game repository, including the app half of a two-package layout,
181
+ * where the state lives a directory or two above the shell's cwd.
182
+ */
183
+ declare function findWranglerState(from?: string): string | null;
184
+ /**
185
+ * Open a SQLite file for reading only.
186
+ *
187
+ * Read-only is what makes this safe to point at a running `wrangler dev`: no
188
+ * writer lock is taken and no journal is touched. The fallback matters because
189
+ * these databases are in WAL mode, and a read-only connection cannot recover a
190
+ * hot journal or create the `-shm` file it needs; when `wrangler dev` is not
191
+ * running and left one behind, the first open fails and a writable handle is
192
+ * the only way in. Nothing here issues anything but `SELECT`, so the widened
193
+ * handle stays as harmless as the narrow one. The existence check comes first
194
+ * because that fallback would otherwise CREATE a database rather than report a
195
+ * missing one.
196
+ */
197
+ declare function openSqlite(file: string): DatabaseSync;
198
+ /**
199
+ * A Worker's local state, with its databases opened lazily and held open.
200
+ *
201
+ * Stateful because it owns file handles: construct one, ask it questions, and
202
+ * {@link close} it. A single instance is the unit both the CLI and a test use,
203
+ * so neither has to know which file backs which resource.
204
+ */
205
+ declare class LocalStore {
206
+ #private;
207
+ /** Absolute path to `.wrangler/state/v3`. */
208
+ readonly root: string;
209
+ private constructor();
210
+ /**
211
+ * Locate and open the state under `from`, or throw with the one thing the
212
+ * caller can act on: that no Worker has been run here yet.
213
+ */
214
+ static open(from?: string): LocalStore;
215
+ /**
216
+ * The engine's D1 database, identified by its schema rather than its name.
217
+ *
218
+ * Miniflare names the file after a hash of the database, so there is no
219
+ * mapping back to the binding. Recognising the engine's own `games` table
220
+ * instead is both simpler and more honest about what the caller wants: the
221
+ * database that has games in it, whatever the binding was called.
222
+ */
223
+ d1(): {
224
+ file: string;
225
+ db: DatabaseSync;
226
+ } | null;
227
+ /**
228
+ * Every local Durable Object database, newest-modified first.
229
+ *
230
+ * The directory is `<worker>-<ClassName>`, and a worker name may itself
231
+ * contain a dash, so the class is everything after the LAST one. Ordering by
232
+ * mtime puts the game you just touched at the top, which is nearly always the
233
+ * one being debugged.
234
+ */
235
+ durableObjects(): LocalDurableObject[];
236
+ /** The armed alarm for a Durable Object, from its namespace's metadata
237
+ * database. Matched on either column because the id is what Miniflare keys
238
+ * by and the name is what a human recognises. */
239
+ alarm(target: LocalDurableObject): LocalAlarm | null;
240
+ /** The D1 index rows, newest-updated first. */
241
+ games(options?: {
242
+ status?: string;
243
+ limit?: number;
244
+ }): LocalGameRow[];
245
+ /**
246
+ * Resolve a game reference the way a developer will type it: a full id, a
247
+ * six-character short code, or an id prefix long enough to be unique.
248
+ *
249
+ * D1 is tried first because it can match a short code, then the DO names, so
250
+ * a game whose index row was wiped is still reachable by id.
251
+ */
252
+ resolveGameId(ref: string): string | null;
253
+ /** The Durable Object holding a game's session, or null if none exists yet. */
254
+ gameDurableObject(gameId: string): LocalDurableObject | null;
255
+ /**
256
+ * Join both stores for one game: the index row, the session, the transition
257
+ * log, and the derived diagnosis.
258
+ *
259
+ * Returns null only when neither store has heard of the game.
260
+ */
261
+ game(ref: string): LocalGameView | null;
262
+ /** Run a read-only statement against D1 or one game's Durable Object: the
263
+ * escape hatch for a question this reader does not model. */
264
+ query(sql: string, options?: {
265
+ game?: string;
266
+ params?: LocalQueryParam[];
267
+ }): Record<string, unknown>[];
268
+ /** Table names and row counts, for D1 or one game's Durable Object. */
269
+ tables(options?: {
270
+ game?: string;
271
+ }): {
272
+ name: string;
273
+ rows: number;
274
+ }[];
275
+ /** Release every open handle. */
276
+ close(): void;
277
+ }
278
+
279
+ export { type LocalAlarm, type LocalDurableObject, type LocalGameDiagnosis, type LocalGameMeta, type LocalGameRow, type LocalGameView, type LocalQueryParam, type LocalSeat, LocalStore, type LocalTransition, findWranglerState, openSqlite };
@@ -0,0 +1,11 @@
1
+ import {
2
+ LocalStore,
3
+ findWranglerState,
4
+ openSqlite
5
+ } from "./chunk-VXWO4LVS.js";
6
+ export {
7
+ LocalStore,
8
+ findWranglerState,
9
+ openSqlite
10
+ };
11
+ //# sourceMappingURL=local-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@eigeninteractive/testkit",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "description": "Test helpers for games built on the EigenInteractive engine: drive rules through the real kernel and run shared twin fixtures, with no Worker, database or network.",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "bin": {
9
- "eigen-contract": "./bin/eigen-contract.js"
9
+ "eigen-contract": "./bin/eigen-contract.js",
10
+ "eigen-inspect": "./bin/eigen-inspect.js"
10
11
  },
11
12
  "exports": {
12
13
  ".": {
13
14
  "types": "./dist/index.d.ts",
14
15
  "import": "./dist/index.js"
16
+ },
17
+ "./local-state": {
18
+ "types": "./dist/local-state.d.ts",
19
+ "import": "./dist/local-state.js"
15
20
  }
16
21
  },
17
22
  "files": [
@@ -31,16 +36,16 @@
31
36
  "dependencies": {
32
37
  "rand-seed": "^3.0.0",
33
38
  "tsx": "^4.23.1",
34
- "@eigeninteractive/kernel": "0.2.5"
39
+ "@eigeninteractive/kernel": "0.3.1"
35
40
  },
36
41
  "devDependencies": {
37
42
  "@types/node": "^26.1.1",
38
43
  "vitest": "^4.1.10",
39
- "@eigeninteractive/rules": "0.2.5"
44
+ "@eigeninteractive/rules": "0.3.1"
40
45
  },
41
46
  "peerDependencies": {
42
47
  "vitest": "^4.1.0",
43
- "@eigeninteractive/rules": "^0.2.5"
48
+ "@eigeninteractive/rules": "^0.3.1"
44
49
  },
45
50
  "homepage": "https://eigeninteractive.com",
46
51
  "repository": {