@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/README.md +53 -28
- package/dist/base.d.mts +9 -0
- package/dist/base.mjs +15 -0
- package/dist/cookie-state.d.mts +38 -0
- package/dist/cookie-state.mjs +35 -0
- package/dist/entry/codec.d.mts +3 -4
- package/dist/index.d.mts +5 -3
- package/dist/index.mjs +4 -3
- package/dist/memory-state.d.mts +3 -4
- package/dist/page-state.d.mts +21 -12
- package/dist/page-state.mjs +17 -3
- package/dist/register.d.mts +25 -20
- package/dist/row/codec.d.mts +48 -0
- package/dist/row/codec.mjs +60 -0
- package/dist/schema/object.d.mts +8 -9
- package/dist/schema/object.mjs +10 -11
- package/dist/storage-state.d.mts +37 -0
- package/dist/storage-state.mjs +37 -0
- package/dist/store/cookie-store.d.mts +12 -0
- package/dist/store/cookie-store.mjs +129 -0
- package/dist/store/core.d.mts +12 -12
- package/dist/store/memory-store.d.mts +3 -4
- package/dist/store/page-store.d.mts +7 -6
- package/dist/store/registry.d.mts +3 -4
- package/dist/store/storage-store.d.mts +9 -0
- package/dist/store/storage-store.mjs +97 -0
- package/dist/url/codec.d.mts +4 -5
- package/dist/url/codec.mjs +32 -11
- package/dist/use-app-state.d.mts +15 -7
- package/dist/use-app-state.mjs +16 -17
- package/docs/GUIDE.md +450 -115
- package/docs/llms.txt +72 -31
- package/package.json +24 -19
- package/dist/local-state.d.mts +0 -29
- package/dist/local-state.mjs +0 -30
- package/dist/store/local-store.d.mts +0 -9
- package/dist/store/local-store.mjs +0 -79
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), `
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
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
|
|
21
|
-
definition module,
|
|
22
|
-
and
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
31
|
-
router hands a page its search —
|
|
32
|
-
page
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
51
|
-
|
|
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.
|
|
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
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
47
|
-
"
|
|
48
|
-
"react": "
|
|
49
|
-
"
|
|
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": "
|
|
52
|
-
"vite-plus": "
|
|
53
|
-
"vitest": "
|
|
54
|
-
"vitest-browser-react": "2.
|
|
55
|
-
"zod": "4.
|
|
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
|
-
"@
|
|
60
|
-
"react": ">=19.
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
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": {
|
package/dist/local-state.d.mts
DELETED
|
@@ -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 };
|
package/dist/local-state.mjs
DELETED
|
@@ -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 };
|