Repository navigation
feat(storage): add persistent state and storage backends - #722
Merged
Merged
Conversation
Adds the @forge-game-engine/forge/storage module: the StorageBackend interface with localStorage and memory backends, StorageError and its subclasses, and createPersistentState, a typed record of flat values with defaults, validation, an ordered write queue and an onChange event. Adds a Storage guide and a Persistent State docs demo, and removes the implemented design document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXmwbd7e594EYzG7UrGdf8
stormmuller
commented
Oct 6, 2026
stormmuller
commented
Oct 6, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXmwbd7e594EYzG7UrGdf8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXmwbd7e594EYzG7UrGdf8
stormmuller
enabled auto-merge (squash)
October 6, 2026 21:42
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
1 of 4 tasks
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Implements
design/persistent-preferences.md(all of Phase 1) and deletes the design doc.New module
@forge-game-engine/forge/storage(src/storage, exported fromsrc/index.tsandpackage.json):StorageBackend: an asynchronousget/set/removeinterface over strings. Its JSDoc and the guide state the contract: every promise settles,removeis idempotent, and creating a backend touches no storage.createLocalStorageBackend(): does its work inside the call and returns an already-settled promise, so a write is stored beforesetreturns. A missinglocalStoragebecomesStorageUnavailableError, aSecurityErrorbecomesStorageBlockedError, andQuotaExceededError(or Firefox's legacyNS_ERROR_DOM_QUOTA_REACHED) becomesStorageFullError, with the original error ascause. Any other error is rejected as it is. Errors are recognized byname, becauseDOMExceptionisn't anErrorinstance in every environment.createMemoryStorageBackend(): a per-instanceMap.createPersistentState(name, defaults, { validators, storage }):set, and keeps stored fields this version doesn't know.valuesis frozen and replaced on every change.onChangeis raised after everyset/reset.reset()removes the entry.PersistentStateFormatErrorwhen the entry isn't a JSON object, andPersistentStateValueErrorcarryingfield,valueandstateName.Docs and demo:
documentation-site/docs/docs/storage/covers the index, persistent state (including writing a record into a component throughonChange) and storage backends (including implementing your own).persistent-stateis in theuicategory, since there's no storage category. It has a size slider, a spin toggle and a reset button, built from engine UI. The record is loaded beforecreateGame, written into a demo component, and that component is read by a demo system.npm run buildand checked in Chromium against the built docs site. The settings were stored ({"spin":false,"size":1.73…}), survived a reload, andResetremoved the entry and restored the defaults.Changes from the design
design/audio-mixer.md's PR.createPersistentStaterejects withPersistentStateValueErrorif a default fails its validator or is a non-finite number. This is a programming error, caught at creation time.setchecks keys withObject.hasOwn(defaults, key), so a key with no default (including prototype names likeconstructor) throwsPersistentStateValueErrornaming the field.name, and thePersistentState*errors also carrystateName.Pre-existing defect found (not fixed here)
createToggleshows the checkmark only fromonValueChanged. Its JSDoc saysisOncan be set directly, but a directtoggle.isOn = …write doesn't raiseonValueChanged, so the checkmark doesn't update. The demo works around it by setting the checkmark sprite'senabledwhen the settings change, with a comment. The right fix is in/src/ui, and I left it out to keep this PR's scope.Decisions taken from the design's open questions
PersistentStateValueError. The demo handles that error by removing the entry and creating the record again.addComponentfromvalues, thenObject.assignononChange).Solution reviewer verdict
REVISE, with these points. I acted on all of them except the first:
audio-mixer.mdandwebgl-context-loss.mdwere reworded.demo-findings.mdis left to the coordinator.value/isOnwrites don't raiseonValueChanged, so nothing loops back.Object.hasOwnfor keys. Done.nameon every error class. Done.resetduring an in-flight write, a failed write followed by a successful one, alocalStoragewrite landing beforesetreturns, and importing the module withoutwindow(a node-environment test).Related issue(s)
Part of #567 (save/load epic). It defines the
StorageBackendthat #567's save system will share.Verification checklist
npm run check-typespasses with 0 errorsnpm testpasses (1980 tests)npm run lintpasses with 0 errors (2 existing TODO warnings inmaterial.ts)npm run cspellpasses with 0 errorsnpm run check-exportspassesindex.ts(and
/src/index.ts/package.jsonexportsif it's a new module)/documentation-site/docs/docsis updated if thischange affects documented behavior
/documentation-site/src/pages/demos, the demo has been updated andverified (see AGENTS.md's "Documentation Site Demos" section). Root
npm run build, docsnpm run typecheckandnpm run buildall ran, and the page was loaded in Chromium.No e2e test: the module has no rendering, input or game-loop behaviour.
Changelog
## [Unreleased]inCHANGELOG.md(required unless this PR's Conventional Commits type is
chore,style,refactor,test,ci,docs, orbuild— seeAGENTS.md's "Changelog" section)
🤖 Generated with Claude Code
https://claude.ai/code/session_01YXmwbd7e594EYzG7UrGdf8
Generated by Claude Code