hazo_state 0.1.2 → 0.2.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/CHANGE_LOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this package are documented here. Versions follow semver.
4
4
 
5
+ ## 0.2.0 — 2026-07-21
6
+
7
+ ### Added
8
+ - **`resolveHydratedValue<T>(local, remote, opts?)`** — new pure export from
9
+ `hazo_state/client`. Reconciles a "local-first" candidate against a "remote"
10
+ candidate for the common localStorage-then-DB hydration pattern: absent side
11
+ falls back to whichever is present; when both are present, an injectable
12
+ `opts.merge(local, remote)` decides the outcome, defaulting to **remote-wins**
13
+ when no `merge` is supplied. No React, no DOM, no fetch — safe to unit-test
14
+ without jsdom and reusable outside a hook (scripts, server-side reconciliation,
15
+ etc). 10 unit tests (this package's first-ever test suite, run via the
16
+ already-declared `vitest` dev dependency — no new test infra needed).
17
+
18
+ ### Not shipped (scope note)
19
+ - A full `useSyncedLocalState<T>` hook (localStorage-first hydrate + debounced
20
+ remote save + silent-degrade-on-failure) was evaluated as the natural next
21
+ layer on top of `resolveHydratedValue`, extracted from stocktools'
22
+ `tax-situation-context.tsx`. **Deliberately deferred** — see
23
+ `design/DECISIONS.md` D-004 for the concrete race condition found during
24
+ design that blocked a confident port. FR-021 filed as `planned`, not
25
+ `shipped`.
26
+
5
27
  ## 0.1.2 — 2026-06-12
6
28
 
7
29
  ### Fixed
@@ -1,4 +1,6 @@
1
1
  import type { StateLevel } from '../index.js';
2
+ export { resolveHydratedValue } from './resolve_hydrated_value.js';
3
+ export type { ResolveHydratedValueOptions } from './resolve_hydrated_value.js';
2
4
  export interface UseHazoStateOptions {
3
5
  /** Visibility level. Default: 'global'. */
4
6
  level?: StateLevel;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,aAAa,CAAA;AAKzD,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sDAAsD;IACtD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnF,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AA0BD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,kBAAkB,CA+K5F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAClE,YAAY,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAA;AAI9E,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sDAAsD;IACtD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnF,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AA0BD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,kBAAkB,CA+K5F"}
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { useCallback, useEffect, useRef, useState } from 'react';
15
15
  import { ConflictError } from '../index.js';
16
+ export { resolveHydratedValue } from './resolve_hydrated_value.js';
16
17
  const DEFAULT_ENDPOINT = '/api/hazo-state';
17
18
  function storageKey(key, level, prefix) {
18
19
  return prefix ? `${prefix}:${key}` : `hazo_state:${level}:${key}`;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * hazo_state/client — resolveHydratedValue
3
+ *
4
+ * Pure reconciliation step for "localStorage-first, then merge with a remote
5
+ * copy" hydration. Deliberately has NO React, NO DOM, NO fetch — just the
6
+ * decision of what value wins once a local candidate and a remote candidate
7
+ * are both known (or absent). This is the seam a stateful hook (or any other
8
+ * transport) plugs into; keeping it pure makes it unit-testable without a
9
+ * DOM/jsdom environment and reusable outside a hook (e.g. a one-shot script).
10
+ *
11
+ * Default policy: **remote wins when present**. `null`/`undefined` on either
12
+ * side falls back to whichever side is available. Callers needing a different
13
+ * reconciliation (field-level merge, local-wins, newest-timestamp-wins, …)
14
+ * inject an explicit `merge` function — the default is a policy choice, not a
15
+ * hardcoded rule.
16
+ */
17
+ export interface ResolveHydratedValueOptions<T> {
18
+ /**
19
+ * Called only when BOTH `local` and `remote` are present (non-null,
20
+ * non-undefined). Its return value wins outright. Omit to use the default
21
+ * "remote wins" policy.
22
+ */
23
+ merge?: (local: T, remote: T) => T;
24
+ }
25
+ /**
26
+ * Reconciles a local candidate and a remote candidate into the value a
27
+ * consumer should hydrate with.
28
+ *
29
+ * - Both absent → `null`.
30
+ * - Only one present → that one.
31
+ * - Both present, no `merge` supplied → `remote` (default policy).
32
+ * - Both present, `merge` supplied → `merge(local, remote)`.
33
+ */
34
+ export declare function resolveHydratedValue<T>(local: T | null | undefined, remote: T | null | undefined, opts?: ResolveHydratedValueOptions<T>): T | null;
35
+ //# sourceMappingURL=resolve_hydrated_value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve_hydrated_value.d.ts","sourceRoot":"","sources":["../../src/client/resolve_hydrated_value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,2BAA2B,CAAC,CAAC;IAC5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3B,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EAC5B,IAAI,CAAC,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACpC,CAAC,GAAG,IAAI,CASV"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * hazo_state/client — resolveHydratedValue
3
+ *
4
+ * Pure reconciliation step for "localStorage-first, then merge with a remote
5
+ * copy" hydration. Deliberately has NO React, NO DOM, NO fetch — just the
6
+ * decision of what value wins once a local candidate and a remote candidate
7
+ * are both known (or absent). This is the seam a stateful hook (or any other
8
+ * transport) plugs into; keeping it pure makes it unit-testable without a
9
+ * DOM/jsdom environment and reusable outside a hook (e.g. a one-shot script).
10
+ *
11
+ * Default policy: **remote wins when present**. `null`/`undefined` on either
12
+ * side falls back to whichever side is available. Callers needing a different
13
+ * reconciliation (field-level merge, local-wins, newest-timestamp-wins, …)
14
+ * inject an explicit `merge` function — the default is a policy choice, not a
15
+ * hardcoded rule.
16
+ */
17
+ /**
18
+ * Reconciles a local candidate and a remote candidate into the value a
19
+ * consumer should hydrate with.
20
+ *
21
+ * - Both absent → `null`.
22
+ * - Only one present → that one.
23
+ * - Both present, no `merge` supplied → `remote` (default policy).
24
+ * - Both present, `merge` supplied → `merge(local, remote)`.
25
+ */
26
+ export function resolveHydratedValue(local, remote, opts) {
27
+ const hasLocal = local !== null && local !== undefined;
28
+ const hasRemote = remote !== null && remote !== undefined;
29
+ if (!hasLocal && !hasRemote)
30
+ return null;
31
+ if (!hasRemote)
32
+ return local;
33
+ if (!hasLocal)
34
+ return remote;
35
+ return opts?.merge ? opts.merge(local, remote) : remote;
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_state",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Generic transactional KV store for the hazo ecosystem — TTL, optimistic CAS, atomic helpers, turnkey route/client",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,7 @@
55
55
  "node": ">=18.0.0"
56
56
  },
57
57
  "dependencies": {
58
- "hazo_connect": "^3.5.1",
58
+ "hazo_connect": "^3.7.0",
59
59
  "server-only": "^0.0.1"
60
60
  },
61
61
  "devDependencies": {