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 +22 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/resolve_hydrated_value.d.ts +35 -0
- package/dist/client/resolve_hydrated_value.d.ts.map +1 -0
- package/dist/client/resolve_hydrated_value.js +36 -0
- package/package.json +2 -2
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
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/client/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
58
|
+
"hazo_connect": "^3.7.0",
|
|
59
59
|
"server-only": "^0.0.1"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|