Store
Back to the reference

Store

State that outlives a scene, with optional persistence.

25 symbols

applyStoreDocsfunctionnacatamalon

applyStoreDocs(docs: typeOperator) => void

Installs what a project's store files say: over the stores that exist, and as stores of their own for the ones nothing has declared.

Both halves matter and they are not the same. Over an existing store it is a layer: the code declared the floor and the file lands on top of it key by key, so a field the file does not mention keeps what the code gave it. For a key no code has declared it builds the store outright, which is what lets a tool add state to a project with no file to write and no module to import.

A store built that way is held as waiting for its code: if the file that declares it turns up later, createGameStore takes over that very object instead of making a second one. That is the ordinary order inside an editor, where the stores of a project are read when it opens and the code that declares one only runs when a scene reaches for it.

It can be called at any point in a start-up, which is the property to keep: a packaged game imports its stores while the modules evaluate, long before this runs, and an editor does it the other way round.

Parameters

docstypeOperator

The files, already read with parseStoreDoc.

View source · game_store/document/apply_store_docs.ts:59

createGameStorefunctionnacatamalon

createGameStore(config: TGameStoreConfig<S, A>) => TGameStore<S, A>

Makes a store: data of the game that outlives a scene (lives, coins, options, a pet), with a way to be told when it changes and, optionally, to be saved and loaded.

A signal says that something happened; a store holds what is still true. Make it once, in a file of its own, and import it wherever it is needed: like a signal, it belongs to the page, not to a scene or a game.

  • Read: store.state.hunger, anywhere.
  • Change: an action, or store.set((s) => { s.hunger -= 1; }). The state is changed in place.
  • React: useStore(store, selector, handler) in a scene, store.subscribe(...) anywhere else. Everyone listening is told straight away, inside the set that changed it.
  • Keep it: persist with localStorageAdapter(), indexedDbAdapter(name) or an adapter of your own, then load() when the game starts.

Parameters

configTGameStoreConfig<S, A>

Its key, its starting state, its actions, and how it saves itself (persist).

Example

declare const pet: TSprite;
const GREEN = getColor('#40c040');
const RED = getColor('#e04040');

// stores/pet.ts
export const petStore = createGameStore({
    key: 'pet',
    state: { hunger: 70, timesFed: 0 },
    actions: (set) => ({
        feed: () => set((s) => { s.hunger = Math.min(100, s.hunger + 15); s.timesFed++; }),
    }),
    persist: { adapter: localStorageAdapter() },
});

// when the game starts
await petStore.load();

// in a scene
useStore(petStore, (s) => s.hunger > 30, (content) => { pet.tint = content ? GREEN : RED; });
View source · game_store/create_game_store.ts:50

getGameStorefunctionnacatamalon

getGameStore(key: string) => TGameStore<Record<string, unknown>, unknown> | null

One store by its key, or null.

This is what an object's store link resolves through, and the whole reason it can: a behaviour reaches state it did not define without an import naming a file it cannot know the path of.

Parameters

keystring

The store's name, as createGameStore was given it.

View source · game_store/store_registry.ts:171

indexedDbAdapterfunctionnacatamalon

indexedDbAdapter(dbName: string, storeName: string) => TStorePersistence<S>

Saves a store in the browser's IndexedDB: for bigger saves than localStorage holds, or several save slots.

Every store using the same dbName shares one table (storeName), each under its own key. The state is stored as it is, without turning it into text first. The database is opened on each operation; the browser keeps the connection, so the adapter itself holds nothing.

Unlike localStorageAdapter, a failure here rejects, and the store warns about it and carries on.

The state's type defaults to any on purpose: written in place (adapter: localStorageAdapter()) the adapter cannot know the store's state yet, and unknown would not fit it. The store's own state is what decides the type.

Parameters

dbNamestring

The database, shared by every store that names it.

storeNamestring

The table inside it. Default 'stores'.

View source · game_store/persistence/indexed_db_adapter.ts:25

localStorageAdapterfunctionnacatamalon

localStorageAdapter() => TStorePersistence<S>

Saves a store in the browser's localStorage, as JSON under the store's key.

Right for small state that is always on: options, a score table, a pet. localStorage holds a few megabytes per site and every write is synchronous; for bigger saves use indexedDbAdapter.

A failure (storage full, disabled by the browser, a save that is not valid JSON) is warned about and never thrown: the game carries on, as if there were no save.

The state's type defaults to any on purpose: written in place (adapter: localStorageAdapter()) the adapter cannot know the store's state yet, and unknown would not fit it. The store's own state is what decides the type.

View source · game_store/persistence/local_storage_adapter.ts:22

parseStoreDocfunctionnacatamalon

parseStoreDoc(raw: unknown, src: string) => TStoreDoc

Reads a store file, whatever state it is in.

Total and it never throws, the same as the scene reader and for the same reason: a project that refused to open because one row of one file was malformed is a project nobody can repair. A row it cannot make sense of is left out and said out loud; everything else opens.

Parameters

rawunknown

The parsed JSON.

srcstring

Where it came from, named in anything it has to report.

View source · game_store/document/parse_store_doc.ts:100

storeOffunctionnacatamalon

storeOf(self: TBox, key?: string) => TGameStore<S, A> | null

The store an object was given: how a behaviour reaches state it did not define.

This is what the link on an object is for. A behaviour attached through a tool cannot import the file that declares a store, because it does not know where that file is, or even that it exists; so the object names the store and the behaviour asks the object. It is also what makes state reachable from a scene put together entirely by hand in an editor, where there is no code to write an import in.

Pass a key when the object was given more than one. With no key it hands back the only one it has, and says so when there are several, because picking one of them would be a guess that works until somebody adds the second.

It is a plain function and not a hook on purpose: it registers nothing, cleans up nothing and is told which object to look at, so it can be called anywhere, including from inside an update.

Hands back null rather than a stand-in that does nothing, because the shape of a store is the game's and there is no neutral state to invent. The warning says which of the two went wrong: the object has no link at all, or it has one to a store this project does not have.

Parameters

selfTBox

The object, as a behaviour is handed it.

keyoptionalstring

Which of its stores, for an object that was given more than one.

Example

declare let hungry: boolean;

registerScript('feed', (self) => {
    const pet = storeOf<{ hunger: number }>(self);
    useUpdate(() => {
        if (pet !== null && hungry) pet.set((s) => { s.hunger += 10; });
    });
});
View source · game_store/store_of.ts:45

useStorefunctionhooknacatamalon

useStore(store: TGameStore<S, A>, selector: TStoreSelector<S, T>, handler: TStoreListener<T>, equals?: (a: T, b: T) => boolean) => void

Reacts from a scene when one value of a store changes, and stops by itself when that part of the scene goes away. Nothing to disconnect by hand.

The selector picks the value; the handler runs only when it changes, straight away, inside the change that caused it, with the new value and the one before. A value that decays every frame but is selected as "is it low?" only calls the handler when the answer flips.

Pick values, not objects: the store's state is changed in place, so (s) => s.inventory is the same object every time and never looks changed. (s) => s.inventory.length does.

The handler is not called when the scene starts: the scene can read store.state right there. It keeps listening while the scene is paused, like useSignal.

Parameters

storeTGameStore<S, A>

The store, made with createGameStore.

selectorTStoreSelector<S, T>

Picks the value to watch.

handlerTStoreListener<T>

What to do when it changes.

equalsoptional(a: T, b: T) => boolean

When two values count as the same. Default Object.is.

Example

declare const petStore: TGameStore<{ hunger: number }>;
const GREEN = getColor('#40c040');
const RED = getColor('#e04040');

export const Pet: TSceneFn = () => {
    const pet = createSprite({ tint: GREEN, width: 64, height: 64 });
    useStore(petStore, (s) => s.hunger > 30, (content) => {
        pet.tint = content ? GREEN : RED;
    });
    return createScene();
};
View source · hooks/store/use_store.ts:42
useStoreLink(key: string) => void

Says that this object uses one of the game's stores.

The important word is uses. The store is not in the object and cannot be: it outlives every scene, and one living inside an object would die with its scene and be two the moment a second object named it. So what is kept here is the name, exactly as a behaviour is kept by name rather than as a function.

Two things come of it that an ordinary import cannot give. A behaviour attached in an editor can be handed the state through the scene itself (storeOf(self)), so a scene put together entirely by hand can wire behaviour to state with no file to write. And the tree becomes able to answer "what touches this state?", which nothing else could.

Called by the scene reader for a store component; call it yourself when you would rather say it in code than in the document.

Parameters

Example

const Pet = () => {
    useStoreLink('pet');
    // ...and now a behaviour on this object can reach it with storeOf(self).
};
View source · hooks/store/use_store_link.ts:33

TGameStoretypenacatamalon

Data of the game that outlives a scene: lives, coins, the options, a pet. Made with createGameStore, once, in a file of its own, and imported wherever it is needed.

state is plain JSON, which is what gets saved. Everything else (actions, set, subscribe...) is how the game reads and changes it.

Properties

keystring

The name it is saved under.

stateS

The live state. Read it anywhere; change it through an action or set, so whoever listens is told.

actionsA

The actions given to createGameStore.

setTStoreSet<S>

Changes the state in place and tells everyone listening, straight away.

getTStoreGet<S>

The live state, the same object as state.

subscribe(listener: () => void) => () => void

Starts listening. With only a listener, it is called after every change. With a selector, only when the selected value changes (by Object.is, or by equals), with the new and the old value. Returns the function that stops.

Inside a scene prefer useStore, which stops by itself when the scene goes away.

save
load
reset
View source · game_store/types/t_game_store.ts:106

TGameStoreConfigtypenacatamalon

What createGameStore is given.

Properties

keystring

The name it is saved under. Different stores need different keys.

stateS

The state a new game starts with, and what reset() goes back to. Plain JSON only.

actionsoptionalTStoreActionsFactory<S, A>

The store's actions. Optional: a store can also be changed with set directly.

persistoptionalTStorePersist<NoInfer<S>>

Saving and loading. Without it, the store lives only while the page is open.

NoInfer, so the state's type comes from state alone: an adapter written in place (adapter: localStorageAdapter()) is generic and knows nothing yet, and left to vote it would turn the whole state into object and every action into an error.

View source · game_store/types/t_game_store.ts:72

TStoreActionsFactorytypenacatamalon

TStoreActionsFactory: (set: TStoreSet<S>, get: TStoreGet<S>) => A

Builds a store's actions from its set and get.

A function and not an object, so the actions can use set and get, and so a big store can be put together from several files: each file returns some actions and the store spreads them into one.

actions: (set, get) => ({ ...careActions(set, get), ...lifeActions(set, get) }),
View source · game_store/types/t_game_store.ts:42

TStoreDoctypenacatamalon

A store as a file: the state a game starts with, written down.

This is the half of a store that a tool can make. createGameStore is code, and code is what a person writes; this is the same store said in data, so that an editor can add a field, change a starting value and show what a project's state even is, none of which is possible when the only definition is a module somebody has to open.

The two halves layer, they do not compete: what the code declares is the floor and what the file says lands on top of it, key by key. A field the file does not mention keeps the value the code gave it, which is what makes a store that gains a field cost no migration at all.

Properties

formatquery
versionquery
keystring

The store's name: the key createGameStore was given, and the key its saved game is written under. By habit it is the file's own name (stores/pet.store gives pet), so a project's stores can be listed without reading a byte of any of them. It is written down as well so that a file that has been moved still says what it is.

fieldsTStoreField[]

The starting state, one row per field.

persistoptionalTStorePersistDoc

How it saves itself, when this file is the only thing defining it.

View source · game_store/document/t_store_doc.ts:114

TStoreFieldtypenacatamalon

One authored entry of a store's starting state: the name it takes in the state, its type and its value.

A list rather than an object, for two reasons that only show up later: a tool can keep the rows in the order somebody put them in, and a type survives a value that happens to look like another one (nothing tells 0 from a number meant as a number, and { x, y } from a pair meant as a place, once the type is gone).

Properties

namestring
View source · game_store/document/t_store_doc.ts:68

TStoreFieldTypetypenacatamalon

TStoreFieldType: 'number' | 'string' | 'boolean' | 'vec2' | 'vec3' | 'color'

What a store field can hold.

A short, flat list, and deliberately so. That a store's state is plain JSON is what holds up the saving, the tools and the whole idea of writing a game down, and a free-form field in a form would be the first place where somebody puts a Map into it and finds out three features later. A typed row cannot express one.

It says 'boolean' where core's format says 'bool', because TScriptField in this engine already says 'boolean': being consistent inside one engine is worth more than matching a format that belongs to the other one, whose files are told apart by their own name anyway.

View source · game_store/document/t_store_doc.ts:38

TStorePersisttypenacatamalon

How a store saves itself.

  • mode: 'auto', the default: the store saves on its own after it changes, at most once every throttle milliseconds. A value changed on every frame still saves regularly.
  • mode: 'manual': the store only saves when save() is called. A save point: the game lives in memory and is only written when the player reaches the typewriter.

In both modes load() reads what was saved; call it once when the game starts.

Properties

adapterTStorePersistence<S>

Where to save: localStorageAdapter(), indexedDbAdapter(name) or one of your own.

modeoptional'auto' | 'manual'

Default 'auto'.

throttleoptionalnumber

In 'auto', the shortest time between two saves, in milliseconds. Default 250.

View source · game_store/persistence/types/t_store_persistence.ts:56

TStorePersistDoctypenacatamalon

How a store saves itself, as a file can say it: the adapter named rather than built, since JSON cannot hold a function.

Only read for a store the document is the only thing defining. A store its own code declared keeps its own saving, because saving is behaviour and behaviour lives in code.

Properties

adapter'localStorage' | 'indexedDb'
modeoptional'auto' | 'manual'
throttleoptionalnumber

How long the trailing throttle waits, in milliseconds.

dbNameoptionalstring

The database's name, for indexedDb. Ignored by the other one.

View source · game_store/document/t_store_doc.ts:85

TStorePersistencetypenacatamalon

Where a store is saved to and loaded from. Two functions, both asynchronous, both keyed by the store's key.

The engine ships localStorageAdapter and indexedDbAdapter, but any storage fits by implementing these two: a server, an external database, a file in a desktop wrapper. save receives the live state, which is plain JSON, so it can be sent as it is.

Properties

load
save

Example

A server of your own:
```ts
const serverAdapter = <S>(baseUrl: string): TStorePersistence<S> => ({
    load: async (key) => {
        const response = await fetch(`${baseUrl}/saves/${key}`);
        return response.ok ? ((await response.json()) as S) : null;
    },
    save: async (key, state) => {
        await fetch(`${baseUrl}/saves/${key}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(state),
        });
    },
});
```
View source · game_store/persistence/types/t_store_persistence.ts:31

TStoreSelectortypenacatamalon

TStoreSelector: (state: S) => T

Picks one value out of a store's state, to be told only when that value changes.

Pick values, not objects: the state is changed in place, so (s) => s.inventory returns the same object every time and never looks changed. (s) => s.inventory.length does.

View source · game_store/types/t_game_store.ts:54

TStoreSettypenacatamalon

TStoreSet: (change: (state: S) => void) => void

Changes a store: receives the live state and changes it in place.

set((s) => { s.hunger -= 1; });

The state is changed, not replaced, the same way a sprite is moved by changing its transform: no copy per change, even for a value that changes on every frame.

View source · game_store/types/t_game_store.ts:17