@k8ordo/state 0.1.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 +168 -0
- 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 +31 -12
- 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/page-store.mjs +29 -32
- 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 +45 -12
- package/dist/use-app-state.d.mts +21 -9
- package/dist/use-app-state.mjs +16 -17
- package/docs/GUIDE.md +515 -91
- package/docs/llms.txt +74 -23
- package/package.json +29 -16
- package/dist/local-state.d.mts +0 -19
- package/dist/local-state.mjs +0 -19
- package/dist/store/local-store.d.mts +0 -9
- package/dist/store/local-store.mjs +0 -80
package/dist/url/codec.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { analyzeSchema, parseWithSalvage, sameValue } from "../schema/object.mjs";
|
|
2
|
+
import { safeEncode } from "zod/v4/core";
|
|
2
3
|
//#region src/url/codec.ts
|
|
3
4
|
const defOf = (schema) => schema._zod.def;
|
|
4
5
|
/**
|
|
@@ -17,21 +18,45 @@ const readAll = (input, key) => {
|
|
|
17
18
|
if (value === void 0) return [];
|
|
18
19
|
return typeof value === "string" ? [value] : value;
|
|
19
20
|
};
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* 真偽値は、フィールド(配列なら要素)のスキーマ自身が encode した綴りで書く。`String(true)` の
|
|
23
|
+
* `"true"` は `z.stringbool({ truthy: ['yes'] })` が読み返せず、既定値に落ちる。
|
|
24
|
+
*/
|
|
25
|
+
const spellBoolean = (schema, value) => {
|
|
26
|
+
try {
|
|
27
|
+
const encoded = safeEncode(schema, value);
|
|
28
|
+
if (encoded.success && typeof encoded.data === "string") return encoded.data;
|
|
29
|
+
} catch {}
|
|
30
|
+
return String(value);
|
|
31
|
+
};
|
|
32
|
+
const serializeValue = (schema, key, value) => {
|
|
21
33
|
if (typeof value === "string") return value;
|
|
22
|
-
if (typeof value === "
|
|
23
|
-
|
|
34
|
+
if (typeof value === "boolean") return spellBoolean(schema, value);
|
|
35
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
36
|
+
throw new TypeError(`url field "${key}" holds a value of type ${typeof value}, which has no URL serialization`);
|
|
37
|
+
};
|
|
38
|
+
const refuseBoolean = (schema, name) => {
|
|
39
|
+
if (baseDef(schema).type === "boolean") throw new TypeError(`url boolean fields must use z.stringbool() — a URL carries strings, and "false" is not false to z.boolean() or z.coerce.boolean(): ${name}`);
|
|
24
40
|
};
|
|
25
41
|
const createUrlCodec = (schema) => {
|
|
26
42
|
const info = analyzeSchema(schema, "url");
|
|
27
43
|
const { keys, defaults } = info;
|
|
28
|
-
const
|
|
44
|
+
const items = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
const base = baseDef(info.shape[key]);
|
|
47
|
+
if (base.type === "array") {
|
|
48
|
+
if (!sameValue(defaults[key], [])) throw new TypeError(`url array fields must default to [] — an absent param and an empty list are the same URL: ${key}`);
|
|
49
|
+
const item = base.element;
|
|
50
|
+
refuseBoolean(item, `${key}[]`);
|
|
51
|
+
items.set(key, item);
|
|
52
|
+
} else refuseBoolean(info.shape[key], key);
|
|
53
|
+
}
|
|
29
54
|
const parse = (input) => {
|
|
30
55
|
const raw = {};
|
|
31
56
|
for (const key of keys) {
|
|
32
57
|
const got = readAll(input, key);
|
|
33
58
|
if (got.length === 0) continue;
|
|
34
|
-
raw[key] =
|
|
59
|
+
raw[key] = items.has(key) ? got : got[0];
|
|
35
60
|
}
|
|
36
61
|
return parseWithSalvage(info, raw);
|
|
37
62
|
};
|
|
@@ -40,16 +65,24 @@ const createUrlCodec = (schema) => {
|
|
|
40
65
|
for (const key of keys) {
|
|
41
66
|
const value = key in values ? values[key] : defaults[key];
|
|
42
67
|
if (value === void 0 || sameValue(defaults[key], value)) continue;
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
const field = info.shape[key];
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
const item = items.get(key) ?? field;
|
|
71
|
+
for (const each of value) params.append(key, serializeValue(item, key, each));
|
|
72
|
+
} else params.append(key, serializeValue(field, key, value));
|
|
45
73
|
}
|
|
46
74
|
return params.toString();
|
|
47
75
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The values as they will come back: written to a query string and read
|
|
78
|
+
* again. Going through the URL rather than handing the typed values
|
|
79
|
+
* straight to the schema is what makes a one-way spelling work — the
|
|
80
|
+
* `string → boolean` of `z.stringbool()` never sees a boolean, exactly as
|
|
81
|
+
* on arrival — and it is the only reading under which `update()` and a
|
|
82
|
+
* visitor's URL are the same path. A value with no URL spelling throws
|
|
83
|
+
* here, which is the earliest anyone can be told.
|
|
84
|
+
*/
|
|
85
|
+
const salvage = (values) => parse(new URLSearchParams(search(values)));
|
|
53
86
|
return {
|
|
54
87
|
keys,
|
|
55
88
|
defaults,
|
package/dist/use-app-state.d.mts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { StateSchema } from "./schema/object.mjs";
|
|
2
|
+
import { CookieState } from "./cookie-state.mjs";
|
|
1
3
|
import { OutputOf, PageState } from "./page-state.mjs";
|
|
2
|
-
import { LocalState } from "./
|
|
4
|
+
import { LocalState, SessionState } from "./storage-state.mjs";
|
|
3
5
|
import { MemoryState } from "./memory-state.mjs";
|
|
4
6
|
import { UpdateHandle } from "./store/core.mjs";
|
|
5
7
|
import { UpdateOptions } from "./store/page-store.mjs";
|
|
6
8
|
import { output } from "zod/v4/core";
|
|
7
9
|
//#region src/use-app-state.d.ts
|
|
8
|
-
type AnyState = PageState | LocalState | MemoryState;
|
|
9
|
-
type StateOf<Def> = Def extends PageState<infer Url, infer Entry> ? OutputOf<Url> & OutputOf<Entry> : Def extends LocalState<infer Schema> ? output<Schema> : Def extends MemoryState<infer Values> ? Values : never;
|
|
10
|
+
export type AnyState = PageState | LocalState | SessionState | CookieState | MemoryState;
|
|
11
|
+
type StateOf<Def> = Def extends PageState<infer Url, infer Entry> ? OutputOf<Url> & OutputOf<Entry> : Def extends LocalState<infer Schema> ? output<Schema> : Def extends SessionState<infer Schema> ? output<Schema> : Def extends CookieState<infer Schema> ? output<Schema> : Def extends MemoryState<infer Values> ? Values : never;
|
|
10
12
|
type UrlStateOf<Def> = Def extends PageState<infer Url> ? OutputOf<Url> : never;
|
|
11
13
|
type UpdateFn<Def> = (patch: Readonly<Partial<StateOf<Def>>> | ((current: StateOf<Def>) => Partial<StateOf<Def>>), ...rest: Def extends PageState ? [options?: UpdateOptions] : []) => UpdateHandle;
|
|
12
|
-
type OptionsFor<Def> = Def extends
|
|
14
|
+
type OptionsFor<Def> = Def extends {
|
|
15
|
+
kind: 'page';
|
|
16
|
+
url: StateSchema;
|
|
17
|
+
} ? {
|
|
13
18
|
/**
|
|
14
19
|
* The RSC-parsed url state, passed down as a prop. It seeds SSR and
|
|
15
20
|
* the hydration render; without it those two renders see the defaults,
|
|
@@ -17,8 +22,15 @@ type OptionsFor<Def> = Def extends PageState ? {
|
|
|
17
22
|
* server-side source, so it always starts from defaults there.
|
|
18
23
|
*/
|
|
19
24
|
initialUrl?: Readonly<UrlStateOf<Def>>;
|
|
20
|
-
} :
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
} : Def extends CookieState<infer Schema> ? {
|
|
26
|
+
/**
|
|
27
|
+
* What `parseCookies(request.cookies)` returned on the server, passed
|
|
28
|
+
* down as a prop. It seeds SSR and the hydration render; without it
|
|
29
|
+
* those two renders see the defaults, which flashes once the cookie
|
|
30
|
+
* is read in the browser.
|
|
31
|
+
*/
|
|
32
|
+
initialCookie?: Readonly<output<Schema>>;
|
|
33
|
+
} : never;
|
|
34
|
+
export declare function useAppState<Def extends AnyState>(def: Def, options?: OptionsFor<Def>): [StateOf<Def>, UpdateFn<Def>];
|
|
35
|
+
export declare function useAppState<Def extends AnyState, const Key extends Extract<keyof StateOf<Def>, string>>(def: Def, keys: readonly Key[], options?: OptionsFor<Def>): [Pick<StateOf<Def>, Key>, UpdateFn<Def>];
|
|
36
|
+
//#endregion
|
package/dist/use-app-state.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { cookieInitialSnapshot, cookieStoreOf } from "./store/cookie-store.mjs";
|
|
3
3
|
import { memoryInitialSnapshot, memoryStoreOf } from "./store/memory-store.mjs";
|
|
4
4
|
import { pageInitialSnapshot, pageStoreOf } from "./store/page-store.mjs";
|
|
5
|
+
import { localStoreOf, sessionStoreOf, storageInitialSnapshot } from "./store/storage-store.mjs";
|
|
5
6
|
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
6
7
|
//#region src/use-app-state.ts
|
|
7
|
-
const storeOf = (def) => def.kind === "page" ? pageStoreOf(def) : def.kind === "local" ? localStoreOf(def) : memoryStoreOf(def);
|
|
8
|
-
const initialOf = (def,
|
|
8
|
+
const storeOf = (def) => def.kind === "page" ? pageStoreOf(def) : def.kind === "local" ? localStoreOf(def) : def.kind === "session" ? sessionStoreOf(def) : def.kind === "cookie" ? cookieStoreOf(def) : memoryStoreOf(def);
|
|
9
|
+
const initialOf = (def, options) => def.kind === "page" ? pageInitialSnapshot(def, options?.initialUrl) : def.kind === "local" || def.kind === "session" ? storageInitialSnapshot(def) : def.kind === "cookie" ? cookieInitialSnapshot(def, options?.initialCookie) : memoryInitialSnapshot(def);
|
|
9
10
|
const FULL = "*";
|
|
10
11
|
function useAppState(def, keysOrOptions, maybeOptions) {
|
|
11
12
|
const gotKeys = Array.isArray(keysOrOptions);
|
|
@@ -19,25 +20,23 @@ function useAppState(def, keysOrOptions, maybeOptions) {
|
|
|
19
20
|
keys
|
|
20
21
|
]);
|
|
21
22
|
const initialUrl = options?.initialUrl;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return cached;
|
|
35
|
-
};
|
|
23
|
+
const initialCookie = options?.initialCookie;
|
|
24
|
+
const serverSnapshot = useMemo(() => {
|
|
25
|
+
const base = initialOf(def, {
|
|
26
|
+
initialUrl,
|
|
27
|
+
initialCookie
|
|
28
|
+
});
|
|
29
|
+
if (keys === null) return base;
|
|
30
|
+
const pick = {};
|
|
31
|
+
for (const key of keys) pick[key] = base[key];
|
|
32
|
+
return pick;
|
|
36
33
|
}, [
|
|
37
34
|
def,
|
|
38
35
|
initialUrl,
|
|
36
|
+
initialCookie,
|
|
39
37
|
keys
|
|
40
38
|
]);
|
|
39
|
+
const getServerSnapshot = useCallback(() => serverSnapshot, [serverSnapshot]);
|
|
41
40
|
return [useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot), useCallback((patch, updateOptions) => storeOf(def).update(patch, updateOptions), [def])];
|
|
42
41
|
}
|
|
43
42
|
//#endregion
|