@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.
@@ -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
- const serializeValue = (key, value) => {
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 === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
23
- throw new TypeError(`url field "${key}" holds a ${typeof value}, which has no URL serialization`);
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 multi = new Set(keys.filter((key) => baseDef(info.shape[key]).type === "array"));
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] = multi.has(key) ? got : got[0];
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
- if (Array.isArray(value)) for (const item of value) params.append(key, serializeValue(key, item));
44
- else params.append(key, serializeValue(key, value));
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
- const salvage = (values) => {
49
- const own = {};
50
- for (const key of keys) if (key in values) own[key] = values[key];
51
- return parseWithSalvage(info, own);
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,
@@ -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 "./local-state.mjs";
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 PageState ? {
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
- } : Record<never, never>;
21
- declare function useAppState<Def extends AnyState>(def: Def, options?: OptionsFor<Def>): [StateOf<Def>, UpdateFn<Def>];
22
- 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>];
23
- //#endregion
24
- export { AnyState, useAppState };
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
@@ -1,11 +1,12 @@
1
1
  "use client";
2
- import { localInitialSnapshot, localStoreOf } from "./store/local-store.mjs";
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, initialUrl) => def.kind === "page" ? pageInitialSnapshot(def, initialUrl) : def.kind === "local" ? localInitialSnapshot(def) : memoryInitialSnapshot(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 getServerSnapshot = useMemo(() => {
23
- let cached;
24
- return () => {
25
- if (cached === void 0) {
26
- const base = initialOf(def, initialUrl);
27
- if (keys === null) cached = base;
28
- else {
29
- const pick = {};
30
- for (const key of keys) pick[key] = base[key];
31
- cached = pick;
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