@k8ordo/state 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/llms.txt CHANGED
@@ -3,11 +3,18 @@
3
3
  > Declare state by where it lives: `definePageState` (typed URL search params
4
4
  > plus hidden history-entry state — the two faces of one entry, updated
5
5
  > atomically over the Navigation API), `defineLocalState` (localStorage,
6
- > cross-tab), `defineMemoryState` (a typed shared box, resets on reload).
7
- > One zod schema per place derives the server read, canonical links, salvage
8
- > of stale data, and a client subscription with exact per-key change
9
- > detection. Works through links and GET forms before JavaScript loads;
10
- > imperative url updates ride a Navigation-API-intercepting router.
6
+ > cross-tab), `defineSessionState` (sessionStorage — the same store over the
7
+ > per-tab area: survives a reload, gone with the tab), `defineCookieState` (a
8
+ > cookie the browser writes with the Cookie Store API and the server reads
9
+ > from the request, so preferences render without a flash of the default —
10
+ > never a secret, it cannot be `HttpOnly`), `defineMemoryState` (a typed
11
+ > shared box with no schema, resets on reload). One zod schema per boundary
12
+ > place (url, entry, local, session, cookie) derives the server read,
13
+ > canonical links, salvage of stale data, and a client subscription with
14
+ > exact per-key change detection. Links and GET forms write the URL before
15
+ > JavaScript loads under any router (the server renders what they wrote
16
+ > wherever it reads the search); imperative url updates ride a
17
+ > Navigation-API-intercepting router.
11
18
  >
12
19
  > Like every k8ordo package it assumes React 19 and Server Components, uses
13
20
  > only what has reached Baseline newly available, and ships no polyfills or
@@ -17,36 +24,70 @@ When installed via npm, these docs live under `node_modules/@k8ordo/state/docs/`
17
24
 
18
25
  ## Guide
19
26
 
20
- - [Design Guide](GUIDE.md): the four places and when each fits, the shared
21
- definition module, reading on the server, updating on the client, batching
22
- and update handles, subscription granularity, router requirements, typed
23
- routes via `Register`, reading local state before hydration, and pairing
24
- with `@k8ordo/form` for GET forms.
27
+ - [Design Guide](GUIDE.md): the places and when each fits, the shared
28
+ definition module, versions and migrations of stored rows, reading on the
29
+ server, cookie state the server renders (and how it relates to cookies the
30
+ server writes), updating on the client,
31
+ batching and update handles, subscription granularity, router
32
+ requirements, typed routes via `Register`, reading local state before
33
+ hydration, and pairing with `@k8ordo/form` for GET forms.
25
34
 
26
35
  ## Entry point
27
36
 
28
37
  - `@k8ordo/state` — `definePageState(key, { url?, entry? })` returns a pure
29
38
  definition usable from server and client: `parseUrl(searchParams)` reads
30
- the url slot typed with defaults applied and per-field salvage (wherever the
31
- router hands a page its search — under `@k8ordo/static` / `@k8ordo/server` a
32
- page receives `params` and `pathname` only, and the url slot is read in the
33
- browser);
34
- `href(base, values?)` builds a canonical link keeping the path literal in
35
- the type; `search(values?)` returns the query string alone.
36
- `defineLocalState(key, schema)` and `defineMemoryState(key, initial)`
37
- declare app-scope state; a local definition also exposes `storageKey` (the
38
- localStorage key, `k8ordo-state:<key>`) and `inlineRead()`, a JavaScript
39
- expression for an inline `<script>` that evaluates to the stored object or
40
- `null` before hydration — unvalidated, so read only what you need with a
41
- fallback. `useAppState(definition, keys?, { initialUrl? })`
42
- (client) works on any kind and returns `[state, update]` — state is the
43
- flat merge of a page definition's slots; `update(patch)` applies
44
- synchronously, batches per handler into one write (url+entry atomically via
45
- `navigation.navigate()`, entry-only via `updateCurrentEntry()`, local via
46
- one `setItem`), and returns `{ committed, finished }` promise handles
47
- (settled for non-navigating kinds; `{ history: 'push' }` exists only on
48
- page state). `resetStateRegistry()` clears stores between tests. Augment
39
+ the url slot typed with defaults applied and invalid values salvaged
40
+ (wherever the router hands a page its search — in `@k8ordo/framework`'s
41
+ server mode a page that exports `search = listState.url` receives the slot parsed as
42
+ `search` and is loaded again when the search moves; any other framework
43
+ page never sees the search, and the url slot is read in the browser);
44
+ `urlReader(schema)` is that reading for a url schema alone, the codec
45
+ built once (what the framework's generated table calls);
46
+ `href(path, values?)` builds a canonical link keeping the path literal in
47
+ the type — the path written from the app's root, the link carrying Vite's
48
+ `base` (`import.meta.env.BASE_URL`) in front; `search(values?)` returns the
49
+ query string alone.
50
+ `defineLocalState(key, schema, versioning?)`,
51
+ `defineSessionState(key, schema)`,
52
+ `defineCookieState(key, schema, versioning?)` and
53
+ `defineMemoryState(key, initial)` declare app-scope state;
54
+ `versioning` is `{ version, migrate(old, fromVersion) }` (type
55
+ `Versioning`): rows are then stored as `[version, values]`, a row with no
56
+ version reads as version 0, an older row goes through `migrate` and then
57
+ the schema's field-by-field salvage and is written back in the current
58
+ version by the browser store (`parseCookies` migrates but never writes), a
59
+ newer row is salvaged and left alone, and a throwing `migrate` reads as
60
+ nothing stored without touching the row; a local or session definition also exposes
61
+ `storageKey` (the key in its storage area, `k8ordo-state:<key>`) and
62
+ `inlineRead()`, a JavaScript expression for an inline `<script>` that
63
+ evaluates to the object stored in that area or `null` before hydration
64
+ (also `null` for a row of another version, when versioned) —
65
+ unvalidated, so read only what you need with a fallback. A
66
+ cookie definition exposes `cookieName` (`k8ordo-state.<key>`),
67
+ `parseCookies(cookies)` — reads the values out of a request's
68
+ percent-decoded cookies (`request.cookies` in `@k8ordo/framework`'s server mode), salvaged
69
+ like any read — and `cookieValue(values?)`, the unencoded JSON a server
70
+ hands its cookie API (`cookies().set`, which percent-encodes it) when it
71
+ sets the same cookie (`Path=/`, `SameSite=Lax`, `Max-Age=34560000`, never
72
+ `HttpOnly`). `useAppState(definition, keys?, { initialUrl? } |
73
+ { initialCookie? })` (client) works on any kind and returns
74
+ `[state, update]` — state is the flat merge of a page definition's slots;
75
+ `initialUrl` / `initialCookie` seed the server and hydration renders with
76
+ what the server read; `update(patch)` applies synchronously, batches the
77
+ calls a handler makes with no `await` between them into one write
78
+ (url+entry changes atomically via `navigation.navigate()`, entry-only
79
+ changes via `updateCurrentEntry()`, local and session via one `setItem`,
80
+ cookie via one
81
+ `cookieStore.set()`; memory applies per call), and returns
82
+ `{ committed, finished }` promise handles (for non-navigating kinds they
83
+ settle once the write lands, and a write that fails — the browser refuses
84
+ it (a full Web Storage area, a cookie over 4 KB) or the schema throws on
85
+ it — rejects them with its error;
86
+ `{ history: 'push' }` exists only on page state and takes effect only when
87
+ a url value changes).
88
+ `resetStateRegistry()` clears stores between tests. Augment
49
89
  `interface Register { routes: typeof routes }` — the same line as for
50
- `@k8ordo/router` — to constrain `href` paths to the route table's linkable
51
- paths; a router without a table registers its own path union as
90
+ `@k8ordo/router` — to check each `href` path against the route table's
91
+ linkable patterns segment by segment (`NavigablePath`, so a `/:locale`
92
+ table no longer takes every path); a router without a table registers its own path union as
52
93
  `interface Register { path }` instead.
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@k8ordo/state",
3
- "version": "0.2.0",
4
- "description": "Declare state by where it lives — URL search params, history-entry state, localStorage, or memory. One zod schema per place, over the Navigation API.",
3
+ "version": "1.0.0",
4
+ "description": "Declare state by where it lives — URL search params, history-entry state, localStorage, sessionStorage, a cookie, or memory. One zod schema per boundary place and a typed box for memory, over the Navigation API.",
5
5
  "keywords": [
6
+ "cookie",
6
7
  "localstorage",
7
8
  "navigation-api",
8
9
  "react",
9
10
  "search-params",
10
11
  "server-components",
12
+ "sessionstorage",
11
13
  "state",
12
14
  "url-state",
13
15
  "zod"
@@ -41,26 +43,29 @@
41
43
  },
42
44
  "devDependencies": {
43
45
  "@arethetypeswrong/cli": "0.18.5",
44
- "@types/react": "19.2.18",
45
- "@vitejs/plugin-react": "6.0.5",
46
- "@vitest/browser-playwright": "4.1.10",
47
- "publint": "0.3.23",
48
- "react": "19.2.8",
49
- "react-dom": "19.2.8",
46
+ "@k8ordo/router": "^1.0.0",
47
+ "@types/react": "19.3.0",
48
+ "@types/react-dom": "19.3.0",
49
+ "@vitejs/plugin-basic-ssl": "2.3.0",
50
+ "@vitejs/plugin-react": "6.1.2",
51
+ "@vitest/browser-playwright": "5.0.3",
52
+ "playwright": "1.63.0",
53
+ "publint": "0.3.25",
54
+ "react": "19.3.0",
55
+ "react-dom": "19.3.0",
50
56
  "typescript": "7.0.2",
51
- "vite": "8.2.1",
52
- "vite-plus": "0.2.9",
53
- "vitest": "4.1.10",
54
- "vitest-browser-react": "2.2.0",
55
- "zod": "4.4.3",
56
- "@k8ordo/router": "^0.1.0"
57
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
58
+ "vite-plus": "1.1.0",
59
+ "vitest": "5.0.3",
60
+ "vitest-browser-react": "2.3.0",
61
+ "zod": "4.6.5"
57
62
  },
58
63
  "peerDependencies": {
59
- "@types/react": ">=19.2.18",
60
- "react": ">=19.2.6",
61
- "typescript": ">=7.0.2",
62
- "zod": "^4.4.3",
63
- "@k8ordo/router": "^0.1.0"
64
+ "@k8ordo/router": "^1.0.0",
65
+ "@types/react": ">=19.3.0",
66
+ "react": ">=19.3.0",
67
+ "typescript": ">=7.0.0",
68
+ "zod": "^4.4.3"
64
69
  },
65
70
  "peerDependenciesMeta": {
66
71
  "@k8ordo/router": {
@@ -1,29 +0,0 @@
1
- import { StateSchema } from "./schema/object.mjs";
2
- import { StoredCodec } from "./entry/codec.mjs";
3
- //#region src/local-state.d.ts
4
- /**
5
- * App-scope state that lives in localStorage: device-persistent, shared
6
- * across tabs, never page-bound — which is why it is its own definition kind
7
- * instead of a slot on a page state.
8
- */
9
- type LocalState<Schema extends StateSchema = StateSchema> = {
10
- kind: 'local';
11
- /** Identity of this state: the storage key and the store registry slot. */
12
- key: string;
13
- /** The schema as passed. */
14
- schema: Schema;
15
- /** The localStorage key the values are written under: `k8ordo-state:<key>`. */
16
- storageKey: string;
17
- /**
18
- * A JavaScript expression, for an inline `<script>`, that evaluates in the
19
- * browser to the stored object — or `null` when nothing is stored, the JSON
20
- * is corrupt, the value is not an object, or storage cannot be read. The
21
- * schema does not run there: treat the result as untrusted and read only
22
- * the fields you need, each with its own fallback.
23
- */
24
- inlineRead: () => string;
25
- };
26
- declare const localCodecOf: (def: LocalState) => StoredCodec;
27
- declare const defineLocalState: <Schema extends StateSchema>(key: string, schema: Schema) => LocalState<Schema>;
28
- //#endregion
29
- export { LocalState, defineLocalState, localCodecOf };
@@ -1,30 +0,0 @@
1
- import { createStoredCodec } from "./entry/codec.mjs";
2
- //#region src/local-state.ts
3
- const STORAGE_KEY_PREFIX = "k8ordo-state:";
4
- /**
5
- * The key as a JavaScript string literal. `JSON.stringify` escapes it for
6
- * JavaScript; `<` is escaped on top so the literal can never close the inline
7
- * `<script>` it is written into, whatever the key contains.
8
- */
9
- const literalOf = (text) => JSON.stringify(text).replaceAll("<", String.raw`\u003c`);
10
- const inlineReadOf = (storageKey) => `(()=>{try{const v=JSON.parse(localStorage.getItem(${literalOf(storageKey)}));return v!==null&&typeof v==="object"&&!Array.isArray(v)?v:null}catch{return null}})()`;
11
- const codecs = /* @__PURE__ */ new WeakMap();
12
- const localCodecOf = (def) => {
13
- const codec = codecs.get(def);
14
- if (codec === void 0) throw new TypeError(`"${def.key}" was not created by defineLocalState of this module instance`);
15
- return codec;
16
- };
17
- const defineLocalState = (key, schema) => {
18
- const storageKey = `${STORAGE_KEY_PREFIX}${key}`;
19
- const def = {
20
- kind: "local",
21
- key,
22
- schema,
23
- storageKey,
24
- inlineRead: () => inlineReadOf(storageKey)
25
- };
26
- codecs.set(def, createStoredCodec(schema, "local"));
27
- return def;
28
- };
29
- //#endregion
30
- export { defineLocalState, localCodecOf };
@@ -1,9 +0,0 @@
1
- import { StateValues } from "../schema/object.mjs";
2
- import { LocalState } from "../local-state.mjs";
3
- import { Store } from "./core.mjs";
4
- //#region src/store/local-store.d.ts
5
- declare const localStoreOf: (def: LocalState) => Store;
6
- /** SSR and hydration see the defaults — the server has no localStorage. */
7
- declare const localInitialSnapshot: (def: LocalState) => StateValues;
8
- //#endregion
9
- export { localInitialSnapshot, localStoreOf };
@@ -1,79 +0,0 @@
1
- import { localCodecOf } from "../local-state.mjs";
2
- import { createHandle, createStoreCore, resolvePatch } from "./core.mjs";
3
- import { getOrCreateStore } from "./registry.mjs";
4
- //#region src/store/local-store.ts
5
- const createLocalStore = (def) => {
6
- const codec = localCodecOf(def);
7
- const { storageKey } = def;
8
- const read = () => {
9
- const text = localStorage.getItem(storageKey);
10
- if (text === null) return codec.parse(void 0);
11
- try {
12
- return codec.parse(JSON.parse(text));
13
- } catch {
14
- return codec.parse(void 0);
15
- }
16
- };
17
- const core = createStoreCore(codec.keys, read());
18
- let pending = null;
19
- let handle = null;
20
- const onStorage = (event) => {
21
- if (event.storageArea !== localStorage) return;
22
- if (event.key !== storageKey && event.key !== null) return;
23
- const fresh = read();
24
- core.applyNext(pending === null ? fresh : {
25
- ...fresh,
26
- ...pending
27
- });
28
- };
29
- window.addEventListener("storage", onStorage);
30
- const flush = () => {
31
- const patch = pending;
32
- const current = handle;
33
- pending = null;
34
- handle = null;
35
- const target = codec.salvage({
36
- ...read(),
37
- ...patch
38
- });
39
- const values = {};
40
- for (const key of codec.keys) values[key] = target[key];
41
- try {
42
- localStorage.setItem(storageKey, JSON.stringify(values));
43
- core.applyNext(target);
44
- current.settle();
45
- } catch (error) {
46
- current.fail(error);
47
- }
48
- };
49
- const update = (patch) => {
50
- const resolved = resolvePatch(patch, core.snapshot(), codec.keys, def.key);
51
- if (pending === null) {
52
- pending = {};
53
- handle = createHandle();
54
- queueMicrotask(flush);
55
- }
56
- Object.assign(pending, resolved);
57
- const echoed = codec.salvage({
58
- ...core.snapshot(),
59
- ...pending
60
- });
61
- for (const key of Object.keys(pending)) pending[key] = echoed[key];
62
- core.applyNext(echoed);
63
- return handle.external;
64
- };
65
- return {
66
- subscribe: core.subscribe,
67
- getSnapshot: core.getSnapshot,
68
- update,
69
- dispose: () => {
70
- window.removeEventListener("storage", onStorage);
71
- core.clear();
72
- }
73
- };
74
- };
75
- const localStoreOf = (def) => getOrCreateStore("local", def.key, () => createLocalStore(def));
76
- /** SSR and hydration see the defaults — the server has no localStorage. */
77
- const localInitialSnapshot = (def) => ({ ...localCodecOf(def).defaults });
78
- //#endregion
79
- export { localInitialSnapshot, localStoreOf };