This repository was archived by the owner on Sep 10, 2026. It is now read-only.
Repository navigation
Release: version packages - #52
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@eigeninteractive/server@0.3.0
Minor Changes
#51
d0b28f4Thanks @seenu-k! - The socket now carries one message, a complete per-seat session snapshot, and every accepted command answers with the same value.roster,syncandframeas separate message kinds are gone.This is a breaking wire change, and it fixes a class of bug rather than an instance. The old shape asked a client to assemble one session out of four sources, each carrying a different slice of the truth under a different versioning scheme: an HTTP summary that carried status, unversioned
rostermessages that stopped at the lobby, asyncon mid-game open that carried only a version, and versionedframemessages that carried neither. Read that list for "how does a client learn the game becameactive" and the answer is that it cannot. Nothing on the live channel said so. A creator sitting in a full waiting room never saw a Start button, and every seat stayed in the waiting room after the game began, until the screen was disposed and re-entered.{ "type": "session", "seq": 7, // monotonic per game, incremented by EVERY commit "gameId": "…", "shortCode": "ABC123", "access": "private", "schemaVersion": 1, "config": { … }, "turnSeconds": null, "budgetSeconds": null, "incrementSeconds": null, "rated": false, "ratingPool": null, "minPlayers": 2, "maxPlayers": 2, "createdBy": "…", "status": "active", // what moves "players": [ … ], "version": 3, // null in the lobby "frame": { … } // THIS seat's observation; null in the lobby }It is sent on socket open at every status, and after every committed change, lobby or state. Being complete makes it idempotent: a client that applies the newest one it has seen is correct however many it missed, so there is nothing to reconstruct and no second channel to correlate against. It carries the immutable header as well as the moving parts because a game screen must not need a second source; the D1 summary stays what it always was, the index behind lists and discovery.
Hidden information is safe by construction rather than by vigilance. The envelope is projected per seat before it is sent,
frameis only ever the receiving principal's own seat's view resolved against the roster at send time, and a socket holding no seat gets the envelope withframe: null, which is how a viewer learns the game started at all.seqis the new ordering key, becauseversioncannot order a lobby change that has none. Apply a snapshot whenseqexceeds the one you hold; that single comparison resolves a command response racing its own socket push, a duplicate delivery, and a reconnect that missed nothing. One clause is added, and it is a property of the state machine rather than an exception: afinishedorabortedsnapshot applies whatever itsseq, because those statuses are absorbing and the abort teardown drops the storage the counter lives in.Gap recovery is unchanged and still animates.
GET /games/{id}/frames?from=&to=still fills a version jump, and the missing span is played through under the previous envelope, so only the real snapshot may movestatus,playersandseq. A client that missed a finish therefore animates the moves and then shows the outcome, instead of displaying a finished game while mid-game moves play.New and changed surface:
GET /games/{id}/sessionreturns the snapshot over HTTP, for the paths that have no socket.LobbyAccepted,JoinedandRosterare gone.CommandAcceptedandSoloStartedare now{ session }.RosterSnapshotandSyncMessageare removed from the exported protocol types;SessionSnapshotreplaces them.FrameMessageremains, as the payload inside a snapshot and the element type of the range fetch.GameStubgainssession(gameId, userId).metatable gainsseq,short_codeandoutcomes. Pre-1.0 and with no games depending on the engine, these are edited into the init migration in place rather than added as a second one, so local.wranglerstate predating this must be deleted. Outcomes are retained rather than drained by the finish compaction, because they are kernel output that no transition row holds, so a cold open of a finished game could not otherwise be answered from the DO alone.One incidental fix rides along:
framewas typed non-nullable in the generated Dart client, because a nullable$reflost its null branch on the way into the OpenAPI document. The schema now spells it as a union, so it emitsanyOf: [$ref, {type: null}]and generates asFrame?. A lobby session would have crashed the old typing on arrival.Patch Changes
create-eigen-game@0.9.3
Patch Changes
#51
e140da5Thanks @seenu-k! - A scaffolded game now ships the local inspector wired up:pnpm inspect games,pnpm inspect game <id|code>,pnpm inspect do.One line makes it work. The
inspectscript points ateigen-inspect, which the testkit already installs as adevDependency, and which reads the local.wranglerdatabases through Node's built-innode:sqlite, so there is nothing to install and no native build to approve.#51
8c7d54eThanks @seenu-k! -test:watchnow reruns when you edit only a fixture. It did not before, and the failure was silent: you saved a case, nothing happened, and the suite you were watching kept reporting the last result.The cause is that
twinFixtureTestsreads fixtures withreadFileSyncat collect time, so they are not in Vite's module graph. Vitest looks a changed path up in that graph, finds nothing, checks whether the file is itself a test file, and reruns nothing. Fixture JSON was never even watched, since Vitest only adds paths outside the graph to its watcher when a trigger glob names them.A scaffold therefore now ships
server/vitest.config.mtscarrying one option:This is what
forceRerunTriggersis for. Vitest's own defaults use it forpackage.jsonand the config files, which are read outside the graph for the same reason, and it appendssetupFilesandsnapshotSerializersto the same list.Two things worth keeping if you edit the file. Spread the defaults rather than replacing them, because config resolution is a shallow merge and a bare array quietly stops manifest and config edits from triggering a rerun. And leave the trigger off any
@cloudflare/vitest-pool-workersproject you add later: a match reruns the whole suite, which costs nothing for pure-Node tests and is not free for workerd, where a fixture cannot change the result anyway.@eigeninteractive/kernel@0.3.0
Patch Changes
@eigeninteractive/testkit@0.3.0
Patch Changes
#51
e140da5Thanks @seenu-k! - Neweigen-inspectcommand, for reading a Worker's local.wranglerstate from the terminal while you develop a game.A game's truth is split across two stores on purpose. D1 is the index: discovery, history, ratings, identity. The game's own Durable Object is the session: the authoritative status, the roster, and the append-only transition log. That split is what makes the engine's read paths cheap, and it is also why a question like "the lobby filled, so why is nothing happening" cannot be answered by looking at one table. The D1
gamesrow is a fire-and-forget display mirror, so it can lag; the DO is the only authority.eigen-inspect game <id|code|prefix>joins both, decodes every JSON column, prints the transition log as a timeline, and ends with the sentence you actually wanted:When the two stores disagree it says so as
Mirror drift, naming the DO as authoritative, so a lost mirror write reads as a lost mirror write rather than as a mystery. A finished game with survivingoutboxrows is called out the same way, since that means the D1 finish apply has not landed and ratings are not published.The other commands are
games(the index),do(every local Durable Object mapped back to its game id, which the hashed filename cannot tell you),players,tables, andsqlfor anything unmodelled.--game <ref>pointstablesandsqlat one game's Durable Object instead of D1;--jsonemits everything for a script or an agent.This complements rather than replaces Wrangler's own local browser, which is
einwrangler dev(or/cdn-cgi/explorer) and is the better tool for generic table browsing.eigen-inspectis the one that knows what a game is, and it needs no dev server, browser, or account.The reader is also exported as a library at
@eigeninteractive/testkit/local-state, so a test or a script can assert against local state directly:Every database is opened read-only, so all of this is safe to run against a live
wrangler dev. It reads them through Node's built-innode:sqlite, so it adds no dependency, needs no native build, and nothing new reaches your Worker's runtime dependencies. It needs Node 24, which the engine already requires.Updated dependencies []:
@eigeninteractive/rules@0.3.0