@pumped-fn/lite-react 1.0.0 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @pumped-fn/lite-react
2
2
 
3
+ ## 1.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 8ed17e7: - Fix watch and invalidation edge cases in `@pumped-fn/lite` by aligning `select()` with `Object.is`, snapshotting select listeners during notification, making watch option typing match the runtime contract, and surfacing invalidation-chain failures from `flush()` instead of leaking them as background rejections.
8
+ - Fix `@pumped-fn/lite-react` hook refresh behavior by keeping stale values visible during re-resolution, recomputing `useSelect` snapshots when selector or equality semantics change, tracking pending promises per controller, and suppressing non-Suspense `unhandledRejection` leaks on failed refreshes.
9
+
10
+ ## 1.1.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 1624845: feat(lite-react): add non-Suspense mode and resolve options for useAtom/useController
15
+
16
+ - Add `{ suspense: false }` option to `useAtom` returning `UseAtomState<T>` with `data`, `loading`, `error`, `controller`
17
+ - Add `{ resolve: boolean }` option to control auto-resolution behavior
18
+ - Suspense mode: `resolve` defaults to `true` (auto-resolves idle atoms)
19
+ - Non-Suspense mode: `resolve` defaults to `false` (no auto-resolve)
20
+ - Add `{ resolve: true }` option to `useController` for Suspense integration
21
+ - Export new types: `UseAtomSuspenseOptions`, `UseAtomManualOptions`, `UseAtomOptions`, `UseAtomState`, `UseControllerOptions`
22
+
3
23
  ## 1.0.0
4
24
 
5
25
  ### Major Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @pumped-fn/lite-react
2
2
 
3
- React bindings for `@pumped-fn/lite` with Suspense and ErrorBoundary integration.
3
+ React bindings for `@pumped-fn/lite` with Suspense, ErrorBoundary integration, and stale-while-revalidate refreshes.
4
4
 
5
5
  **Zero dependencies** · **<2KB bundle** · **React 18+**
6
6
 
@@ -20,12 +20,14 @@ sequenceDiagram
20
20
  alt resolved
21
21
  Controller-->>useAtom: value
22
22
  useAtom->>Controller: subscribe to changes
23
- else resolving
23
+ else resolving with stale value
24
+ Controller-->>useAtom: stale value
25
+ else resolving without value
24
26
  useAtom-->>App: throw Promise (Suspense)
25
27
  else failed
26
28
  useAtom-->>App: throw Error (ErrorBoundary)
27
29
  else idle
28
- useAtom-->>App: throw Error (not resolved)
30
+ useAtom-->>App: throw Promise (Suspense)
29
31
  end
30
32
  ```
31
33
 
@@ -37,19 +39,21 @@ flowchart TD
37
39
  Hook --> State{ctrl.state?}
38
40
 
39
41
  State -->|idle| AutoResolve[Auto-resolve + Throw Promise]
40
- State -->|resolving| Promise[Throw cached Promise]
42
+ State -->|resolving + stale value| Stale[Return stale value]
43
+ State -->|resolving + no value| Promise[Throw cached Promise]
41
44
  State -->|resolved| Value[Return value]
42
45
  State -->|failed| Stored[Throw stored error]
43
46
 
44
47
  AutoResolve --> Suspense[Suspense catches]
45
48
  Promise --> Suspense
49
+ Stale --> Render[Keep current UI]
46
50
  Stored --> ErrorBoundary[ErrorBoundary catches]
47
51
  ```
48
52
 
49
53
  | State | Hook Behavior |
50
54
  |-------|---------------|
51
55
  | `idle` | Auto-resolves and suspends — Suspense shows fallback |
52
- | `resolving` | Throws cached promise Suspense shows fallback |
56
+ | `resolving` | Returns stale value if available, otherwise throws cached promise |
53
57
  | `resolved` | Returns value, subscribes to changes |
54
58
  | `failed` | Throws stored error — ErrorBoundary catches |
55
59
 
@@ -91,6 +95,16 @@ ctrl.update(n => n + 1)
91
95
  ctrl.invalidate()
92
96
  ```
93
97
 
98
+ With `{ resolve: true }` option, triggers Suspense if atom not resolved:
99
+
100
+ ```tsx
101
+ // Suspense ensures controller is resolved before render
102
+ const ctrl = useController(configAtom, { resolve: true })
103
+ ctrl.get() // safe - Suspense guarantees resolved state
104
+ ```
105
+
106
+ While a controller is re-resolving, `{ resolve: true }` keeps suspending until the controller settles.
107
+
94
108
  ### useAtom
95
109
 
96
110
  Subscribe to atom value with Suspense integration.
@@ -109,6 +123,42 @@ function UserProfile() {
109
123
  </ErrorBoundary>
110
124
  ```
111
125
 
126
+ #### Non-Suspense Mode
127
+
128
+ For manual loading/error state handling without Suspense:
129
+
130
+ ```tsx
131
+ function UserProfile() {
132
+ const { data, loading, error, controller } = useAtom(userAtom, { suspense: false })
133
+
134
+ if (loading && data) return <div>Refreshing {data.name}...</div>
135
+ if (loading) return <div>Loading...</div>
136
+ if (error) return <div>Error: {error.message}</div>
137
+ if (!data) return <div>Not loaded</div>
138
+
139
+ return (
140
+ <div>
141
+ <h1>{data.name}</h1>
142
+ <button onClick={() => controller.invalidate()}>Refresh</button>
143
+ </div>
144
+ )
145
+ }
146
+ ```
147
+
148
+ With `{ resolve: true }`, auto-resolves on mount:
149
+
150
+ ```tsx
151
+ // Starts resolution automatically when component mounts
152
+ const { data, loading, error } = useAtom(userAtom, { suspense: false, resolve: true })
153
+ ```
154
+
155
+ | Option | Effect |
156
+ |--------|--------|
157
+ | `{ suspense: false }` | Returns state object, no auto-resolve |
158
+ | `{ suspense: false, resolve: true }` | Returns state object, auto-resolves on mount |
159
+
160
+ While `loading` is `true`, `data` may still contain the last resolved value during a refresh.
161
+
112
162
  ### useSelect
113
163
 
114
164
  Fine-grained selection — only re-renders when selected value changes.
@@ -120,7 +170,7 @@ const count = useSelect(todosAtom, todos => todos.length, (a, b) => a === b)
120
170
 
121
171
  ## Invalidation
122
172
 
123
- When an atom is invalidated, hooks automatically suspend during re-resolution:
173
+ When an atom is invalidated, `useAtom` and `useSelect` keep rendering the last value while re-resolving:
124
174
 
125
175
  ```mermaid
126
176
  sequenceDiagram
@@ -133,14 +183,16 @@ sequenceDiagram
133
183
 
134
184
  Note over Controller: ctrl.invalidate()
135
185
  Controller->>Controller: state = resolving
136
- useAtom-->>Component: throw Promise
137
- Note over Component: Suspense fallback
186
+ useAtom-->>Component: stale value
187
+ Note over Component: current UI stays visible
138
188
 
139
189
  Controller->>Controller: factory runs
140
190
  Controller->>Controller: state = resolved
141
191
  useAtom->>Component: re-render (new value)
142
192
  ```
143
193
 
194
+ `useController(atom, { resolve: true })` is different: it suspends until the controller settles again.
195
+
144
196
  ## Testing
145
197
 
146
198
  Use presets for test isolation:
@@ -163,10 +215,9 @@ render(
163
215
 
164
216
  ## SSR
165
217
 
166
- SSR-compatible by design:
218
+ SSR-compatible when request-scoped atoms are resolved before rendering:
167
219
 
168
220
  - No side effects on import
169
- - Uses `useSyncExternalStore` with server snapshot
170
221
  - Scope passed as prop (no global state)
171
222
 
172
223
  ```tsx
package/dist/index.cjs CHANGED
@@ -27,12 +27,28 @@ function ScopeProvider({ scope, children }) {
27
27
  //#endregion
28
28
  //#region src/hooks.ts
29
29
  const pendingPromises = /* @__PURE__ */ new WeakMap();
30
- function getOrCreatePendingPromise(atom$1, ctrl) {
31
- let pending = pendingPromises.get(atom$1);
30
+ function getOrCreatePendingPromise(ctrl) {
31
+ let pending = pendingPromises.get(ctrl);
32
32
  if (!pending) {
33
- pending = ctrl.resolve();
34
- pendingPromises.set(atom$1, pending);
35
- pending.finally(() => pendingPromises.delete(atom$1));
33
+ if (ctrl.state === "resolving") pending = new Promise((resolve, reject) => {
34
+ const unsub = ctrl.on("*", () => {
35
+ if (ctrl.state === "resolved") {
36
+ unsub();
37
+ resolve(ctrl.get());
38
+ } else if (ctrl.state === "failed") {
39
+ unsub();
40
+ try {
41
+ ctrl.get();
42
+ } catch (e) {
43
+ reject(e);
44
+ }
45
+ }
46
+ });
47
+ });
48
+ else pending = ctrl.resolve();
49
+ pendingPromises.set(ctrl, pending);
50
+ pending.catch(() => {});
51
+ pending.then(() => pendingPromises.delete(ctrl), () => pendingPromises.delete(ctrl));
36
52
  }
37
53
  return pending;
38
54
  }
@@ -44,8 +60,10 @@ function getOrCreatePendingPromise(atom$1, ctrl) {
44
60
  *
45
61
  * @example
46
62
  * ```tsx
47
- * const scope = useScope()
48
- * await scope.resolve(myAtom)
63
+ * function MyComponent() {
64
+ * const scope = useScope()
65
+ * const handleClick = () => scope.resolve(myAtom)
66
+ * }
49
67
  * ```
50
68
  */
51
69
  function useScope() {
@@ -53,48 +71,80 @@ function useScope() {
53
71
  if (!scope) throw new Error("useScope must be used within a ScopeProvider");
54
72
  return scope;
55
73
  }
56
- /**
57
- * Get a memoized controller for an atom.
58
- *
59
- * @param atom - The atom to create a controller for
60
- * @returns A memoized Lite.Controller instance
61
- *
62
- * @example
63
- * ```tsx
64
- * const ctrl = useController(counterAtom)
65
- * ctrl.set(ctrl.get() + 1)
66
- * ```
67
- */
68
- function useController(atom$1) {
74
+ function useController(atom$1, options) {
69
75
  const scope = useScope();
70
- return (0, react.useMemo)(() => scope.controller(atom$1), [scope, atom$1]);
76
+ const ctrl = (0, react.useMemo)(() => scope.controller(atom$1), [scope, atom$1]);
77
+ if (options?.resolve) {
78
+ if (ctrl.state === "idle" || ctrl.state === "resolving") throw getOrCreatePendingPromise(ctrl);
79
+ if (ctrl.state === "failed") throw ctrl.get();
80
+ }
81
+ return ctrl;
71
82
  }
72
- /**
73
- * Subscribe to atom value with Suspense/ErrorBoundary integration.
74
- * Auto-resolves atoms lazily and throws cached Promise for Suspense.
75
- *
76
- * @param atom - The atom to read
77
- * @returns The current value of the atom
78
- *
79
- * @example
80
- * ```tsx
81
- * function UserProfile() {
82
- * const user = useAtom(userAtom)
83
- * return <div>{user.name}</div>
84
- * }
85
- * ```
86
- */
87
- function useAtom(atom$1) {
83
+ function useAtom(atom$1, options) {
88
84
  const ctrl = useController(atom$1);
89
- const atomRef = (0, react.useRef)(atom$1);
90
- atomRef.current = atom$1;
85
+ const ctrlState = ctrl.state;
86
+ const isSuspense = options?.suspense !== false;
87
+ const autoResolve = isSuspense ? options?.resolve !== false : !!options?.resolve;
88
+ const stateCache = (0, react.useRef)(null);
89
+ (0, react.useEffect)(() => {
90
+ if (!isSuspense && (ctrlState === "resolving" || autoResolve && ctrlState === "idle")) getOrCreatePendingPromise(ctrl).catch(() => {});
91
+ }, [
92
+ ctrl,
93
+ ctrlState,
94
+ autoResolve,
95
+ isSuspense
96
+ ]);
91
97
  const getSnapshot = (0, react.useCallback)(() => {
92
- const state = ctrl.state;
93
- if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
94
- if (state === "failed") throw ctrl.get();
95
- return ctrl.get();
96
- }, [ctrl]);
97
- return (0, react.useSyncExternalStore)((0, react.useCallback)((onStoreChange) => ctrl.on("resolved", onStoreChange), [ctrl]), getSnapshot, getSnapshot);
98
+ if (isSuspense) {
99
+ if (ctrl.state === "idle") {
100
+ if (autoResolve) throw getOrCreatePendingPromise(ctrl);
101
+ throw new Error("Atom is not resolved. Set resolve: true or resolve the atom before rendering.");
102
+ }
103
+ if (ctrl.state === "failed") throw ctrl.get();
104
+ try {
105
+ return ctrl.get();
106
+ } catch {
107
+ throw getOrCreatePendingPromise(ctrl);
108
+ }
109
+ }
110
+ let data;
111
+ let error;
112
+ if (ctrl.state === "resolved" || ctrl.state === "resolving") try {
113
+ data = ctrl.get();
114
+ } catch {}
115
+ else if (ctrl.state === "failed") try {
116
+ ctrl.get();
117
+ } catch (e) {
118
+ error = e instanceof Error ? e : new Error(String(e));
119
+ }
120
+ const loading = ctrl.state === "resolving" || autoResolve && ctrl.state === "idle";
121
+ if (stateCache.current && stateCache.current.ctrl === ctrl && stateCache.current.ctrlState === ctrl.state && stateCache.current.data === data && stateCache.current.error === error && stateCache.current.loading === loading) return stateCache.current.result;
122
+ const result = {
123
+ data,
124
+ loading,
125
+ error,
126
+ controller: ctrl
127
+ };
128
+ stateCache.current = {
129
+ ctrl,
130
+ ctrlState: ctrl.state,
131
+ data,
132
+ error,
133
+ loading,
134
+ result
135
+ };
136
+ return result;
137
+ }, [
138
+ ctrl,
139
+ autoResolve,
140
+ isSuspense
141
+ ]);
142
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((onStoreChange) => {
143
+ return ctrl.on("*", () => {
144
+ if (!isSuspense && ctrl.state === "resolving") getOrCreatePendingPromise(ctrl).catch(() => {});
145
+ onStoreChange();
146
+ });
147
+ }, [ctrl, isSuspense]), getSnapshot, getSnapshot);
98
148
  }
99
149
  /**
100
150
  * Select a derived value from an atom with fine-grained reactivity.
@@ -111,33 +161,41 @@ function useAtom(atom$1) {
111
161
  * ```
112
162
  */
113
163
  function useSelect(atom$1, selector, eq) {
114
- const scope = useScope();
115
164
  const ctrl = useController(atom$1);
116
- const atomRef = (0, react.useRef)(atom$1);
117
- atomRef.current = atom$1;
118
165
  const selectorRef = (0, react.useRef)(selector);
119
166
  const eqRef = (0, react.useRef)(eq);
120
167
  selectorRef.current = selector;
121
168
  eqRef.current = eq;
122
- const handleRef = (0, react.useRef)(null);
123
- const getOrCreateHandle = (0, react.useCallback)(() => {
124
- if (!handleRef.current || handleRef.current.scope !== scope || handleRef.current.atom !== atom$1) handleRef.current = {
125
- scope,
126
- atom: atom$1,
127
- handle: scope.select(atom$1, selectorRef.current, { eq: eqRef.current })
128
- };
129
- return handleRef.current.handle;
130
- }, [scope, atom$1]);
169
+ const selectionCache = (0, react.useRef)(null);
131
170
  const getSnapshot = (0, react.useCallback)(() => {
132
171
  const state = ctrl.state;
133
- if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
172
+ if (state === "idle") throw getOrCreatePendingPromise(ctrl);
134
173
  if (state === "failed") throw ctrl.get();
135
- return getOrCreateHandle().get();
136
- }, [ctrl, getOrCreateHandle]);
174
+ let value;
175
+ try {
176
+ value = ctrl.get();
177
+ } catch {
178
+ throw getOrCreatePendingPromise(ctrl);
179
+ }
180
+ const nextSelector = selectorRef.current;
181
+ const nextEq = eqRef.current;
182
+ const current = selectionCache.current;
183
+ if (current && current.ctrl === ctrl && current.ctrlState === state && Object.is(current.source, value) && current.selector === nextSelector && current.eq === nextEq) return current.value;
184
+ const nextValue = nextSelector(value);
185
+ const selectedValue = current && current.ctrl === ctrl && current.selector === nextSelector && (nextEq ?? Object.is)(current.value, nextValue) ? current.value : nextValue;
186
+ selectionCache.current = {
187
+ ctrl,
188
+ ctrlState: state,
189
+ source: value,
190
+ selector: nextSelector,
191
+ eq: nextEq,
192
+ value: selectedValue
193
+ };
194
+ return selectedValue;
195
+ }, [ctrl]);
137
196
  return (0, react.useSyncExternalStore)((0, react.useCallback)((onStoreChange) => {
138
- if (ctrl.state !== "resolved") return () => {};
139
- return getOrCreateHandle().subscribe(onStoreChange);
140
- }, [ctrl, getOrCreateHandle]), getSnapshot, getSnapshot);
197
+ return ctrl.on("*", onStoreChange);
198
+ }, [ctrl]), getSnapshot, getSnapshot);
141
199
  }
142
200
 
143
201
  //#endregion
package/dist/index.d.cts CHANGED
@@ -28,6 +28,26 @@ declare function ScopeProvider({
28
28
  }: ScopeProviderProps): react_jsx_runtime0.JSX.Element;
29
29
  //#endregion
30
30
  //#region src/hooks.d.ts
31
+ interface UseAtomSuspenseOptions {
32
+ suspense?: true;
33
+ /** @default true */
34
+ resolve?: boolean;
35
+ }
36
+ interface UseAtomManualOptions {
37
+ suspense: false;
38
+ /** @default false */
39
+ resolve?: boolean;
40
+ }
41
+ type UseAtomOptions = UseAtomSuspenseOptions | UseAtomManualOptions;
42
+ interface UseAtomState<T> {
43
+ data: T | undefined;
44
+ loading: boolean;
45
+ error: Error | undefined;
46
+ controller: Lite$1.Controller<T>;
47
+ }
48
+ interface UseControllerOptions {
49
+ resolve?: boolean;
50
+ }
31
51
  /**
32
52
  * Access the current Lite.Scope from context.
33
53
  *
@@ -36,17 +56,16 @@ declare function ScopeProvider({
36
56
  *
37
57
  * @example
38
58
  * ```tsx
39
- * const scope = useScope()
40
- * await scope.resolve(myAtom)
59
+ * function MyComponent() {
60
+ * const scope = useScope()
61
+ * const handleClick = () => scope.resolve(myAtom)
62
+ * }
41
63
  * ```
42
64
  */
43
65
  declare function useScope(): Lite$1.Scope;
44
66
  /**
45
67
  * Get a memoized controller for an atom.
46
68
  *
47
- * @param atom - The atom to create a controller for
48
- * @returns A memoized Lite.Controller instance
49
- *
50
69
  * @example
51
70
  * ```tsx
52
71
  * const ctrl = useController(counterAtom)
@@ -54,22 +73,19 @@ declare function useScope(): Lite$1.Scope;
54
73
  * ```
55
74
  */
56
75
  declare function useController<T>(atom: Lite$1.Atom<T>): Lite$1.Controller<T>;
76
+ declare function useController<T>(atom: Lite$1.Atom<T>, options: UseControllerOptions): Lite$1.Controller<T>;
57
77
  /**
58
78
  * Subscribe to atom value with Suspense/ErrorBoundary integration.
59
- * Auto-resolves atoms lazily and throws cached Promise for Suspense.
60
- *
61
- * @param atom - The atom to read
62
- * @returns The current value of the atom
63
79
  *
64
80
  * @example
65
81
  * ```tsx
66
- * function UserProfile() {
67
- * const user = useAtom(userAtom)
68
- * return <div>{user.name}</div>
69
- * }
82
+ * const user = useAtom(userAtom)
83
+ * const { data, loading, error } = useAtom(userAtom, { suspense: false })
70
84
  * ```
71
85
  */
72
86
  declare function useAtom<T>(atom: Lite$1.Atom<T>): T;
87
+ declare function useAtom<T>(atom: Lite$1.Atom<T>, options: UseAtomSuspenseOptions): T;
88
+ declare function useAtom<T>(atom: Lite$1.Atom<T>, options: UseAtomManualOptions): UseAtomState<T>;
73
89
  /**
74
90
  * Select a derived value from an atom with fine-grained reactivity.
75
91
  * Only re-renders when the selected value changes per equality function.
@@ -86,5 +102,5 @@ declare function useAtom<T>(atom: Lite$1.Atom<T>): T;
86
102
  */
87
103
  declare function useSelect<T, S>(atom: Lite$1.Atom<T>, selector: (value: T) => S, eq?: (a: S, b: S) => boolean): S;
88
104
  //#endregion
89
- export { type Lite, ScopeContext, ScopeProvider, type ScopeProviderProps, atom, createScope, flow, preset, useAtom, useController, useScope, useSelect };
105
+ export { type Lite, ScopeContext, ScopeProvider, type ScopeProviderProps, type UseAtomManualOptions, type UseAtomOptions, type UseAtomState, type UseAtomSuspenseOptions, type UseControllerOptions, atom, createScope, flow, preset, useAtom, useController, useScope, useSelect };
90
106
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAMM,cAAY,MAAA,CAAA,QAAA,MAAA,CAAA;AALyB,UAOjC,kBAAA,CAFiD;EAEjD,KAAA,EACD,MAAA,CAAK,KADJ;EAeD,QAAA,EAbG,SAaU;;;;;;;;;ACtBqB;AA2BZ;;iBDLtB,aAAA,CCyB4B;EAAA,KAAA;EAAA;AAAA,CAAA,EDzBO,kBCyBP,CAAA,EDzByB,kBAAA,CAAA,GAAA,CAAA,OCyBzB;;;;;;;;AD/CM;AAKzB;AAIG;;;;;iBCkBZ,QAAA,CAAA,CDLqD,ECKzC,MAAA,CAAK,KDLoC;;;;ACtBnB;AA2BZ;;;;;;AAoB+B;;iBAArD,aAoBsB,CAAA,CAAA,CAAA,CAAA,IAAA,EApBC,MAAA,CAAK,IAoBN,CApBW,CAoBX,CAAA,CAAA,EApBgB,MAAA,CAAK,UAoBrB,CApBgC,CAoBhC,CAAA;;;AAAW;;;;;;;;;;;;;iBAAjC,iBAAiB,MAAA,CAAK,KAAK,KAAK;;;;;;;;;;;;;;;iBAsChC,sBACD,MAAA,CAAK,KAAK,sBACE,MAAM,YACf,MAAM,gBACd"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAMM,cAAY,MAAA,CAAA,QAAA,MAAA,CAAA;AALyB,UAOjC,kBAAA,CAFiD;EAEjD,KAAA,EACD,MAAA,CAAK,KADJ;EAeD,QAAA,EAbG,SAaU;;;;;;;;;ACtBqB;AAGX;AAMF;AAMqC,iBDO1D,aAAA,CCLa;EAAA,KAAA;EAAA;AAAA,CAAA,EDKsB,kBCLtB,CAAA,EDKwC,kBAAA,CAAA,GAAA,CAAA,OCLxC;;;UAdZ,sBAAA;;;;;ADHiC,UCSjC,oBAAA,CDJQ;EAER,QAAA,EAAA,KAAA;EAeD;EAAgB,OAAA,CAAA,EAAA,OAAA;;KCPpB,cAAA,GAAiB,sBDOsB,GCPG,oBDOH;UCLlC,YDKoD,CAAA,CAAA,CAAA,CAAA;EAAA,IAAA,ECJtD,CDIsD,GAAA,SAAA;;SCFrD;cACK,MAAA,CAAK,WAAW;AArBa;AAGX,UAqBtB,oBAAA,CAfoB;EAMzB,OAAA,CAAA,EAAA,OAAc;AAAgD;;;;;;AAMtC;AAGC;AAgDC;;;;;;AAiB+B;iBAjBrD,QAAA,CAAA,CAkBiC,EAlBrB,MAAA,CAAK,KAkBgB;;;;;;AAAmD;;;;iBADpF,aA2BiC,CAAA,CAAA,CAAA,CAAA,IAAA,EA3BV,MAAA,CAAK,IA2BK,CA3BA,CA2BA,CAAA,CAAA,EA3BK,MAAA,CAAK,UA2BV,CA3BqB,CA2BrB,CAAA;AAAA,iBA1BjC,aA2BO,CAAA,CAAA,CAAA,CAAA,IAAA,EA3BgB,MAAA,CAAK,IA2BrB,CA3B0B,CA2B1B,CAAA,EAAA,OAAA,EA3BuC,oBA2BvC,CAAA,EA3B8D,MAAA,CAAK,UA2BnE,CA3B8E,CA2B9E,CAAA;;;;;;AAA2D;;;;iBADlE,OAE4E,CAAA,CAAA,CAAA,CAAA,IAAA,EAF3D,MAAA,CAAK,IAEsD,CAFjD,CAEiD,CAAA,CAAA,EAF5C,CAE4C;iBAD5E,OAC+D,CAAA,CAAA,CAAA,CAAA,IAAA,EAD9C,MAAA,CAAK,IACyC,CADpC,CACoC,CAAA,EAAA,OAAA,EADvB,sBACuB,CAAA,EADE,CACF;iBAA/D,OAA2E,CAAA,CAAA,CAAA,CAAA,IAAA,EAA1D,MAAA,CAAK,IAAqD,CAAhD,CAAgD,CAAA,EAAA,OAAA,EAAnC,oBAAmC,CAAA,EAAZ,YAAY,CAAC,CAAD,CAAA;AAAA;;;;;;;;;;;;;;iBA0G3E,sBACD,MAAA,CAAK,KAAK,sBACE,MAAM,YACf,MAAM,gBACd"}
package/dist/index.d.mts CHANGED
@@ -28,6 +28,26 @@ declare function ScopeProvider({
28
28
  }: ScopeProviderProps): react_jsx_runtime0.JSX.Element;
29
29
  //#endregion
30
30
  //#region src/hooks.d.ts
31
+ interface UseAtomSuspenseOptions {
32
+ suspense?: true;
33
+ /** @default true */
34
+ resolve?: boolean;
35
+ }
36
+ interface UseAtomManualOptions {
37
+ suspense: false;
38
+ /** @default false */
39
+ resolve?: boolean;
40
+ }
41
+ type UseAtomOptions = UseAtomSuspenseOptions | UseAtomManualOptions;
42
+ interface UseAtomState<T> {
43
+ data: T | undefined;
44
+ loading: boolean;
45
+ error: Error | undefined;
46
+ controller: Lite$1.Controller<T>;
47
+ }
48
+ interface UseControllerOptions {
49
+ resolve?: boolean;
50
+ }
31
51
  /**
32
52
  * Access the current Lite.Scope from context.
33
53
  *
@@ -36,17 +56,16 @@ declare function ScopeProvider({
36
56
  *
37
57
  * @example
38
58
  * ```tsx
39
- * const scope = useScope()
40
- * await scope.resolve(myAtom)
59
+ * function MyComponent() {
60
+ * const scope = useScope()
61
+ * const handleClick = () => scope.resolve(myAtom)
62
+ * }
41
63
  * ```
42
64
  */
43
65
  declare function useScope(): Lite$1.Scope;
44
66
  /**
45
67
  * Get a memoized controller for an atom.
46
68
  *
47
- * @param atom - The atom to create a controller for
48
- * @returns A memoized Lite.Controller instance
49
- *
50
69
  * @example
51
70
  * ```tsx
52
71
  * const ctrl = useController(counterAtom)
@@ -54,22 +73,19 @@ declare function useScope(): Lite$1.Scope;
54
73
  * ```
55
74
  */
56
75
  declare function useController<T>(atom: Lite$1.Atom<T>): Lite$1.Controller<T>;
76
+ declare function useController<T>(atom: Lite$1.Atom<T>, options: UseControllerOptions): Lite$1.Controller<T>;
57
77
  /**
58
78
  * Subscribe to atom value with Suspense/ErrorBoundary integration.
59
- * Auto-resolves atoms lazily and throws cached Promise for Suspense.
60
- *
61
- * @param atom - The atom to read
62
- * @returns The current value of the atom
63
79
  *
64
80
  * @example
65
81
  * ```tsx
66
- * function UserProfile() {
67
- * const user = useAtom(userAtom)
68
- * return <div>{user.name}</div>
69
- * }
82
+ * const user = useAtom(userAtom)
83
+ * const { data, loading, error } = useAtom(userAtom, { suspense: false })
70
84
  * ```
71
85
  */
72
86
  declare function useAtom<T>(atom: Lite$1.Atom<T>): T;
87
+ declare function useAtom<T>(atom: Lite$1.Atom<T>, options: UseAtomSuspenseOptions): T;
88
+ declare function useAtom<T>(atom: Lite$1.Atom<T>, options: UseAtomManualOptions): UseAtomState<T>;
73
89
  /**
74
90
  * Select a derived value from an atom with fine-grained reactivity.
75
91
  * Only re-renders when the selected value changes per equality function.
@@ -86,5 +102,5 @@ declare function useAtom<T>(atom: Lite$1.Atom<T>): T;
86
102
  */
87
103
  declare function useSelect<T, S>(atom: Lite$1.Atom<T>, selector: (value: T) => S, eq?: (a: S, b: S) => boolean): S;
88
104
  //#endregion
89
- export { type Lite, ScopeContext, ScopeProvider, type ScopeProviderProps, atom, createScope, flow, preset, useAtom, useController, useScope, useSelect };
105
+ export { type Lite, ScopeContext, ScopeProvider, type ScopeProviderProps, type UseAtomManualOptions, type UseAtomOptions, type UseAtomState, type UseAtomSuspenseOptions, type UseControllerOptions, atom, createScope, flow, preset, useAtom, useController, useScope, useSelect };
90
106
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAMM,cAAY,MAAA,CAAA,QAAA,MAAA,CAAA;AALyB,UAOjC,kBAAA,CAFiD;EAEjD,KAAA,EACD,MAAA,CAAK,KADJ;EAeD,QAAA,EAbG,SAaU;;;;;;;;;ACtBqB;AA2BZ;;iBDLtB,aAAA,CCyB4B;EAAA,KAAA;EAAA;AAAA,CAAA,EDzBO,kBCyBP,CAAA,EDzByB,kBAAA,CAAA,GAAA,CAAA,OCyBzB;;;;;;;;AD/CM;AAKzB;AAIG;;;;;iBCkBZ,QAAA,CAAA,CDLqD,ECKzC,MAAA,CAAK,KDLoC;;;;ACtBnB;AA2BZ;;;;;;AAoB+B;;iBAArD,aAoBsB,CAAA,CAAA,CAAA,CAAA,IAAA,EApBC,MAAA,CAAK,IAoBN,CApBW,CAoBX,CAAA,CAAA,EApBgB,MAAA,CAAK,UAoBrB,CApBgC,CAoBhC,CAAA;;;AAAW;;;;;;;;;;;;;iBAAjC,iBAAiB,MAAA,CAAK,KAAK,KAAK;;;;;;;;;;;;;;;iBAsChC,sBACD,MAAA,CAAK,KAAK,sBACE,MAAM,YACf,MAAM,gBACd"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAMM,cAAY,MAAA,CAAA,QAAA,MAAA,CAAA;AALyB,UAOjC,kBAAA,CAFiD;EAEjD,KAAA,EACD,MAAA,CAAK,KADJ;EAeD,QAAA,EAbG,SAaU;;;;;;;;;ACtBqB;AAGX;AAMF;AAMqC,iBDO1D,aAAA,CCLa;EAAA,KAAA;EAAA;AAAA,CAAA,EDKsB,kBCLtB,CAAA,EDKwC,kBAAA,CAAA,GAAA,CAAA,OCLxC;;;UAdZ,sBAAA;;;;;ADHiC,UCSjC,oBAAA,CDJQ;EAER,QAAA,EAAA,KAAA;EAeD;EAAgB,OAAA,CAAA,EAAA,OAAA;;KCPpB,cAAA,GAAiB,sBDOsB,GCPG,oBDOH;UCLlC,YDKoD,CAAA,CAAA,CAAA,CAAA;EAAA,IAAA,ECJtD,CDIsD,GAAA,SAAA;;SCFrD;cACK,MAAA,CAAK,WAAW;AArBa;AAGX,UAqBtB,oBAAA,CAfoB;EAMzB,OAAA,CAAA,EAAA,OAAc;AAAgD;;;;;;AAMtC;AAGC;AAgDC;;;;;;AAiB+B;iBAjBrD,QAAA,CAAA,CAkBiC,EAlBrB,MAAA,CAAK,KAkBgB;;;;;;AAAmD;;;;iBADpF,aA2BiC,CAAA,CAAA,CAAA,CAAA,IAAA,EA3BV,MAAA,CAAK,IA2BK,CA3BA,CA2BA,CAAA,CAAA,EA3BK,MAAA,CAAK,UA2BV,CA3BqB,CA2BrB,CAAA;AAAA,iBA1BjC,aA2BO,CAAA,CAAA,CAAA,CAAA,IAAA,EA3BgB,MAAA,CAAK,IA2BrB,CA3B0B,CA2B1B,CAAA,EAAA,OAAA,EA3BuC,oBA2BvC,CAAA,EA3B8D,MAAA,CAAK,UA2BnE,CA3B8E,CA2B9E,CAAA;;;;;;AAA2D;;;;iBADlE,OAE4E,CAAA,CAAA,CAAA,CAAA,IAAA,EAF3D,MAAA,CAAK,IAEsD,CAFjD,CAEiD,CAAA,CAAA,EAF5C,CAE4C;iBAD5E,OAC+D,CAAA,CAAA,CAAA,CAAA,IAAA,EAD9C,MAAA,CAAK,IACyC,CADpC,CACoC,CAAA,EAAA,OAAA,EADvB,sBACuB,CAAA,EADE,CACF;iBAA/D,OAA2E,CAAA,CAAA,CAAA,CAAA,IAAA,EAA1D,MAAA,CAAK,IAAqD,CAAhD,CAAgD,CAAA,EAAA,OAAA,EAAnC,oBAAmC,CAAA,EAAZ,YAAY,CAAC,CAAD,CAAA;AAAA;;;;;;;;;;;;;;iBA0G3E,sBACD,MAAA,CAAK,KAAK,sBACE,MAAM,YACf,MAAM,gBACd"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { atom, createScope, flow, preset } from "@pumped-fn/lite";
2
- import { createContext, useCallback, useContext, useMemo, useRef, useSyncExternalStore } from "react";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/context.tsx
@@ -27,12 +27,28 @@ function ScopeProvider({ scope, children }) {
27
27
  //#endregion
28
28
  //#region src/hooks.ts
29
29
  const pendingPromises = /* @__PURE__ */ new WeakMap();
30
- function getOrCreatePendingPromise(atom$1, ctrl) {
31
- let pending = pendingPromises.get(atom$1);
30
+ function getOrCreatePendingPromise(ctrl) {
31
+ let pending = pendingPromises.get(ctrl);
32
32
  if (!pending) {
33
- pending = ctrl.resolve();
34
- pendingPromises.set(atom$1, pending);
35
- pending.finally(() => pendingPromises.delete(atom$1));
33
+ if (ctrl.state === "resolving") pending = new Promise((resolve, reject) => {
34
+ const unsub = ctrl.on("*", () => {
35
+ if (ctrl.state === "resolved") {
36
+ unsub();
37
+ resolve(ctrl.get());
38
+ } else if (ctrl.state === "failed") {
39
+ unsub();
40
+ try {
41
+ ctrl.get();
42
+ } catch (e) {
43
+ reject(e);
44
+ }
45
+ }
46
+ });
47
+ });
48
+ else pending = ctrl.resolve();
49
+ pendingPromises.set(ctrl, pending);
50
+ pending.catch(() => {});
51
+ pending.then(() => pendingPromises.delete(ctrl), () => pendingPromises.delete(ctrl));
36
52
  }
37
53
  return pending;
38
54
  }
@@ -44,8 +60,10 @@ function getOrCreatePendingPromise(atom$1, ctrl) {
44
60
  *
45
61
  * @example
46
62
  * ```tsx
47
- * const scope = useScope()
48
- * await scope.resolve(myAtom)
63
+ * function MyComponent() {
64
+ * const scope = useScope()
65
+ * const handleClick = () => scope.resolve(myAtom)
66
+ * }
49
67
  * ```
50
68
  */
51
69
  function useScope() {
@@ -53,48 +71,80 @@ function useScope() {
53
71
  if (!scope) throw new Error("useScope must be used within a ScopeProvider");
54
72
  return scope;
55
73
  }
56
- /**
57
- * Get a memoized controller for an atom.
58
- *
59
- * @param atom - The atom to create a controller for
60
- * @returns A memoized Lite.Controller instance
61
- *
62
- * @example
63
- * ```tsx
64
- * const ctrl = useController(counterAtom)
65
- * ctrl.set(ctrl.get() + 1)
66
- * ```
67
- */
68
- function useController(atom$1) {
74
+ function useController(atom$1, options) {
69
75
  const scope = useScope();
70
- return useMemo(() => scope.controller(atom$1), [scope, atom$1]);
76
+ const ctrl = useMemo(() => scope.controller(atom$1), [scope, atom$1]);
77
+ if (options?.resolve) {
78
+ if (ctrl.state === "idle" || ctrl.state === "resolving") throw getOrCreatePendingPromise(ctrl);
79
+ if (ctrl.state === "failed") throw ctrl.get();
80
+ }
81
+ return ctrl;
71
82
  }
72
- /**
73
- * Subscribe to atom value with Suspense/ErrorBoundary integration.
74
- * Auto-resolves atoms lazily and throws cached Promise for Suspense.
75
- *
76
- * @param atom - The atom to read
77
- * @returns The current value of the atom
78
- *
79
- * @example
80
- * ```tsx
81
- * function UserProfile() {
82
- * const user = useAtom(userAtom)
83
- * return <div>{user.name}</div>
84
- * }
85
- * ```
86
- */
87
- function useAtom(atom$1) {
83
+ function useAtom(atom$1, options) {
88
84
  const ctrl = useController(atom$1);
89
- const atomRef = useRef(atom$1);
90
- atomRef.current = atom$1;
85
+ const ctrlState = ctrl.state;
86
+ const isSuspense = options?.suspense !== false;
87
+ const autoResolve = isSuspense ? options?.resolve !== false : !!options?.resolve;
88
+ const stateCache = useRef(null);
89
+ useEffect(() => {
90
+ if (!isSuspense && (ctrlState === "resolving" || autoResolve && ctrlState === "idle")) getOrCreatePendingPromise(ctrl).catch(() => {});
91
+ }, [
92
+ ctrl,
93
+ ctrlState,
94
+ autoResolve,
95
+ isSuspense
96
+ ]);
91
97
  const getSnapshot = useCallback(() => {
92
- const state = ctrl.state;
93
- if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
94
- if (state === "failed") throw ctrl.get();
95
- return ctrl.get();
96
- }, [ctrl]);
97
- return useSyncExternalStore(useCallback((onStoreChange) => ctrl.on("resolved", onStoreChange), [ctrl]), getSnapshot, getSnapshot);
98
+ if (isSuspense) {
99
+ if (ctrl.state === "idle") {
100
+ if (autoResolve) throw getOrCreatePendingPromise(ctrl);
101
+ throw new Error("Atom is not resolved. Set resolve: true or resolve the atom before rendering.");
102
+ }
103
+ if (ctrl.state === "failed") throw ctrl.get();
104
+ try {
105
+ return ctrl.get();
106
+ } catch {
107
+ throw getOrCreatePendingPromise(ctrl);
108
+ }
109
+ }
110
+ let data;
111
+ let error;
112
+ if (ctrl.state === "resolved" || ctrl.state === "resolving") try {
113
+ data = ctrl.get();
114
+ } catch {}
115
+ else if (ctrl.state === "failed") try {
116
+ ctrl.get();
117
+ } catch (e) {
118
+ error = e instanceof Error ? e : new Error(String(e));
119
+ }
120
+ const loading = ctrl.state === "resolving" || autoResolve && ctrl.state === "idle";
121
+ if (stateCache.current && stateCache.current.ctrl === ctrl && stateCache.current.ctrlState === ctrl.state && stateCache.current.data === data && stateCache.current.error === error && stateCache.current.loading === loading) return stateCache.current.result;
122
+ const result = {
123
+ data,
124
+ loading,
125
+ error,
126
+ controller: ctrl
127
+ };
128
+ stateCache.current = {
129
+ ctrl,
130
+ ctrlState: ctrl.state,
131
+ data,
132
+ error,
133
+ loading,
134
+ result
135
+ };
136
+ return result;
137
+ }, [
138
+ ctrl,
139
+ autoResolve,
140
+ isSuspense
141
+ ]);
142
+ return useSyncExternalStore(useCallback((onStoreChange) => {
143
+ return ctrl.on("*", () => {
144
+ if (!isSuspense && ctrl.state === "resolving") getOrCreatePendingPromise(ctrl).catch(() => {});
145
+ onStoreChange();
146
+ });
147
+ }, [ctrl, isSuspense]), getSnapshot, getSnapshot);
98
148
  }
99
149
  /**
100
150
  * Select a derived value from an atom with fine-grained reactivity.
@@ -111,33 +161,41 @@ function useAtom(atom$1) {
111
161
  * ```
112
162
  */
113
163
  function useSelect(atom$1, selector, eq) {
114
- const scope = useScope();
115
164
  const ctrl = useController(atom$1);
116
- const atomRef = useRef(atom$1);
117
- atomRef.current = atom$1;
118
165
  const selectorRef = useRef(selector);
119
166
  const eqRef = useRef(eq);
120
167
  selectorRef.current = selector;
121
168
  eqRef.current = eq;
122
- const handleRef = useRef(null);
123
- const getOrCreateHandle = useCallback(() => {
124
- if (!handleRef.current || handleRef.current.scope !== scope || handleRef.current.atom !== atom$1) handleRef.current = {
125
- scope,
126
- atom: atom$1,
127
- handle: scope.select(atom$1, selectorRef.current, { eq: eqRef.current })
128
- };
129
- return handleRef.current.handle;
130
- }, [scope, atom$1]);
169
+ const selectionCache = useRef(null);
131
170
  const getSnapshot = useCallback(() => {
132
171
  const state = ctrl.state;
133
- if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
172
+ if (state === "idle") throw getOrCreatePendingPromise(ctrl);
134
173
  if (state === "failed") throw ctrl.get();
135
- return getOrCreateHandle().get();
136
- }, [ctrl, getOrCreateHandle]);
174
+ let value;
175
+ try {
176
+ value = ctrl.get();
177
+ } catch {
178
+ throw getOrCreatePendingPromise(ctrl);
179
+ }
180
+ const nextSelector = selectorRef.current;
181
+ const nextEq = eqRef.current;
182
+ const current = selectionCache.current;
183
+ if (current && current.ctrl === ctrl && current.ctrlState === state && Object.is(current.source, value) && current.selector === nextSelector && current.eq === nextEq) return current.value;
184
+ const nextValue = nextSelector(value);
185
+ const selectedValue = current && current.ctrl === ctrl && current.selector === nextSelector && (nextEq ?? Object.is)(current.value, nextValue) ? current.value : nextValue;
186
+ selectionCache.current = {
187
+ ctrl,
188
+ ctrlState: state,
189
+ source: value,
190
+ selector: nextSelector,
191
+ eq: nextEq,
192
+ value: selectedValue
193
+ };
194
+ return selectedValue;
195
+ }, [ctrl]);
137
196
  return useSyncExternalStore(useCallback((onStoreChange) => {
138
- if (ctrl.state !== "resolved") return () => {};
139
- return getOrCreateHandle().subscribe(onStoreChange);
140
- }, [ctrl, getOrCreateHandle]), getSnapshot, getSnapshot);
197
+ return ctrl.on("*", onStoreChange);
198
+ }, [ctrl]), getSnapshot, getSnapshot);
141
199
  }
142
200
 
143
201
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["atom"],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":["import { createContext, type ReactNode } from 'react'\nimport { type Lite } from '@pumped-fn/lite'\n\n/**\n * React context for Lite.Scope.\n */\nconst ScopeContext = createContext<Lite.Scope | null>(null)\n\ninterface ScopeProviderProps {\n scope: Lite.Scope\n children: ReactNode\n}\n\n/**\n * Provider component for Lite.Scope.\n *\n * @example\n * ```tsx\n * <ScopeProvider scope={scope}>\n * <App />\n * </ScopeProvider>\n * ```\n */\nfunction ScopeProvider({ scope, children }: ScopeProviderProps) {\n return (\n <ScopeContext.Provider value={scope}>\n {children}\n </ScopeContext.Provider>\n )\n}\n\nexport { ScopeContext, ScopeProvider }\nexport type { ScopeProviderProps }\n","import { useCallback, useContext, useMemo, useRef, useSyncExternalStore } from 'react'\nimport { type Lite } from '@pumped-fn/lite'\nimport { ScopeContext } from './context'\n\nconst pendingPromises = new WeakMap<Lite.Atom<unknown>, Promise<unknown>>()\n\nfunction getOrCreatePendingPromise<T>(atom: Lite.Atom<T>, ctrl: Lite.Controller<T>): Promise<T> {\n let pending = pendingPromises.get(atom) as Promise<T> | undefined\n if (!pending) {\n pending = ctrl.resolve()\n pendingPromises.set(atom, pending)\n pending.finally(() => pendingPromises.delete(atom))\n }\n return pending\n}\n\n/**\n * Access the current Lite.Scope from context.\n *\n * @returns The current Lite.Scope instance from context\n * @throws When called outside of a ScopeProvider\n *\n * @example\n * ```tsx\n * const scope = useScope()\n * await scope.resolve(myAtom)\n * ```\n */\nfunction useScope(): Lite.Scope {\n const scope = useContext(ScopeContext)\n if (!scope) {\n throw new Error(\"useScope must be used within a ScopeProvider\")\n }\n return scope\n}\n\n/**\n * Get a memoized controller for an atom.\n *\n * @param atom - The atom to create a controller for\n * @returns A memoized Lite.Controller instance\n *\n * @example\n * ```tsx\n * const ctrl = useController(counterAtom)\n * ctrl.set(ctrl.get() + 1)\n * ```\n */\nfunction useController<T>(atom: Lite.Atom<T>): Lite.Controller<T> {\n const scope = useScope()\n return useMemo(() => scope.controller(atom), [scope, atom])\n}\n\n/**\n * Subscribe to atom value with Suspense/ErrorBoundary integration.\n * Auto-resolves atoms lazily and throws cached Promise for Suspense.\n *\n * @param atom - The atom to read\n * @returns The current value of the atom\n *\n * @example\n * ```tsx\n * function UserProfile() {\n * const user = useAtom(userAtom)\n * return <div>{user.name}</div>\n * }\n * ```\n */\nfunction useAtom<T>(atom: Lite.Atom<T>): T {\n const ctrl = useController(atom)\n const atomRef = useRef(atom)\n atomRef.current = atom\n\n const getSnapshot = useCallback((): T => {\n const state = ctrl.state\n if (state === 'idle' || state === 'resolving') {\n throw getOrCreatePendingPromise(atomRef.current, ctrl)\n }\n if (state === 'failed') {\n throw ctrl.get()\n }\n return ctrl.get()\n }, [ctrl])\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => ctrl.on('resolved', onStoreChange),\n [ctrl]\n )\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/**\n * Select a derived value from an atom with fine-grained reactivity.\n * Only re-renders when the selected value changes per equality function.\n *\n * @param atom - The atom to select from\n * @param selector - Function to extract a derived value\n * @param eq - Optional equality function\n * @returns The selected value\n *\n * @example\n * ```tsx\n * const name = useSelect(userAtom, user => user.name)\n * ```\n */\nfunction useSelect<T, S>(\n atom: Lite.Atom<T>,\n selector: (value: T) => S,\n eq?: (a: S, b: S) => boolean\n): S {\n const scope = useScope()\n const ctrl = useController(atom)\n\n const atomRef = useRef(atom)\n atomRef.current = atom\n\n const selectorRef = useRef(selector)\n const eqRef = useRef(eq)\n selectorRef.current = selector\n eqRef.current = eq\n\n const handleRef = useRef<{\n scope: Lite.Scope\n atom: Lite.Atom<T>\n handle: Lite.SelectHandle<S>\n } | null>(null)\n\n const getOrCreateHandle = useCallback(() => {\n if (\n !handleRef.current ||\n handleRef.current.scope !== scope ||\n handleRef.current.atom !== atom\n ) {\n const handle = scope.select(atom, selectorRef.current, { eq: eqRef.current })\n handleRef.current = { scope, atom, handle }\n }\n return handleRef.current.handle\n }, [scope, atom])\n\n const getSnapshot = useCallback((): S => {\n const state = ctrl.state\n if (state === 'idle' || state === 'resolving') {\n throw getOrCreatePendingPromise(atomRef.current, ctrl)\n }\n if (state === 'failed') {\n throw ctrl.get()\n }\n return getOrCreateHandle().get()\n }, [ctrl, getOrCreateHandle])\n\n const subscribe = useCallback((onStoreChange: () => void) => {\n if (ctrl.state !== 'resolved') {\n return () => {}\n }\n return getOrCreateHandle().subscribe(onStoreChange)\n }, [ctrl, getOrCreateHandle])\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\nexport { useScope, useController, useAtom, useSelect }\n"],"mappings":";;;;;;;;AAMA,MAAM,eAAe,cAAiC,KAAK;;;;;;;;;;;AAiB3D,SAAS,cAAc,EAAE,OAAO,YAAgC;AAC9D,QACE,oBAAC,aAAa;EAAS,OAAO;EAC3B;GACqB;;;;;ACvB5B,MAAM,kCAAkB,IAAI,SAA+C;AAE3E,SAAS,0BAA6B,QAAoB,MAAsC;CAC9F,IAAI,UAAU,gBAAgB,IAAIA,OAAK;AACvC,KAAI,CAAC,SAAS;AACZ,YAAU,KAAK,SAAS;AACxB,kBAAgB,IAAIA,QAAM,QAAQ;AAClC,UAAQ,cAAc,gBAAgB,OAAOA,OAAK,CAAC;;AAErD,QAAO;;;;;;;;;;;;;;AAeT,SAAS,WAAuB;CAC9B,MAAM,QAAQ,WAAW,aAAa;AACtC,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,+CAA+C;AAEjE,QAAO;;;;;;;;;;;;;;AAeT,SAAS,cAAiB,QAAwC;CAChE,MAAM,QAAQ,UAAU;AACxB,QAAO,cAAc,MAAM,WAAWA,OAAK,EAAE,CAAC,OAAOA,OAAK,CAAC;;;;;;;;;;;;;;;;;AAkB7D,SAAS,QAAW,QAAuB;CACzC,MAAM,OAAO,cAAcA,OAAK;CAChC,MAAM,UAAU,OAAOA,OAAK;AAC5B,SAAQ,UAAUA;CAElB,MAAM,cAAc,kBAAqB;EACvC,MAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,UAAU,UAAU,YAChC,OAAM,0BAA0B,QAAQ,SAAS,KAAK;AAExD,MAAI,UAAU,SACZ,OAAM,KAAK,KAAK;AAElB,SAAO,KAAK,KAAK;IAChB,CAAC,KAAK,CAAC;AAOV,QAAO,qBALW,aACf,kBAA8B,KAAK,GAAG,YAAY,cAAc,EACjE,CAAC,KAAK,CACP,EAEsC,aAAa,YAAY;;;;;;;;;;;;;;;;AAiBlE,SAAS,UACP,QACA,UACA,IACG;CACH,MAAM,QAAQ,UAAU;CACxB,MAAM,OAAO,cAAcA,OAAK;CAEhC,MAAM,UAAU,OAAOA,OAAK;AAC5B,SAAQ,UAAUA;CAElB,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,QAAQ,OAAO,GAAG;AACxB,aAAY,UAAU;AACtB,OAAM,UAAU;CAEhB,MAAM,YAAY,OAIR,KAAK;CAEf,MAAM,oBAAoB,kBAAkB;AAC1C,MACE,CAAC,UAAU,WACX,UAAU,QAAQ,UAAU,SAC5B,UAAU,QAAQ,SAASA,OAG3B,WAAU,UAAU;GAAE;GAAO;GAAM,QADpB,MAAM,OAAOA,QAAM,YAAY,SAAS,EAAE,IAAI,MAAM,SAAS,CAAC;GAClC;AAE7C,SAAO,UAAU,QAAQ;IACxB,CAAC,OAAOA,OAAK,CAAC;CAEjB,MAAM,cAAc,kBAAqB;EACvC,MAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,UAAU,UAAU,YAChC,OAAM,0BAA0B,QAAQ,SAAS,KAAK;AAExD,MAAI,UAAU,SACZ,OAAM,KAAK,KAAK;AAElB,SAAO,mBAAmB,CAAC,KAAK;IAC/B,CAAC,MAAM,kBAAkB,CAAC;AAS7B,QAAO,qBAPW,aAAa,kBAA8B;AAC3D,MAAI,KAAK,UAAU,WACjB,cAAa;AAEf,SAAO,mBAAmB,CAAC,UAAU,cAAc;IAClD,CAAC,MAAM,kBAAkB,CAAC,EAEU,aAAa,YAAY"}
1
+ {"version":3,"file":"index.mjs","names":["atom","data: T | undefined","error: Error | undefined","result: UseAtomState<T>","value: T"],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":["import { createContext, type ReactNode } from 'react'\nimport { type Lite } from '@pumped-fn/lite'\n\n/**\n * React context for Lite.Scope.\n */\nconst ScopeContext = createContext<Lite.Scope | null>(null)\n\ninterface ScopeProviderProps {\n scope: Lite.Scope\n children: ReactNode\n}\n\n/**\n * Provider component for Lite.Scope.\n *\n * @example\n * ```tsx\n * <ScopeProvider scope={scope}>\n * <App />\n * </ScopeProvider>\n * ```\n */\nfunction ScopeProvider({ scope, children }: ScopeProviderProps) {\n return (\n <ScopeContext.Provider value={scope}>\n {children}\n </ScopeContext.Provider>\n )\n}\n\nexport { ScopeContext, ScopeProvider }\nexport type { ScopeProviderProps }\n","import { useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'\nimport { type Lite } from '@pumped-fn/lite'\nimport { ScopeContext } from './context'\n\ninterface UseAtomSuspenseOptions {\n suspense?: true\n /** @default true */\n resolve?: boolean\n}\n\ninterface UseAtomManualOptions {\n suspense: false\n /** @default false */\n resolve?: boolean\n}\n\ntype UseAtomOptions = UseAtomSuspenseOptions | UseAtomManualOptions\n\ninterface UseAtomState<T> {\n data: T | undefined\n loading: boolean\n error: Error | undefined\n controller: Lite.Controller<T>\n}\n\ninterface UseControllerOptions {\n resolve?: boolean\n}\n\nconst pendingPromises = new WeakMap<Lite.Controller<unknown>, Promise<unknown>>()\n\nfunction getOrCreatePendingPromise<T>(ctrl: Lite.Controller<T>): Promise<T> {\n let pending = pendingPromises.get(ctrl) as Promise<T> | undefined\n if (!pending) {\n if (ctrl.state === 'resolving') {\n pending = new Promise<T>((resolve, reject) => {\n const unsub = ctrl.on('*', () => {\n if (ctrl.state === 'resolved') {\n unsub()\n resolve(ctrl.get())\n } else if (ctrl.state === 'failed') {\n unsub()\n try { ctrl.get() } catch (e) { reject(e) }\n }\n })\n })\n } else {\n pending = ctrl.resolve()\n }\n pendingPromises.set(ctrl, pending)\n void pending.catch(() => {})\n pending.then(\n () => pendingPromises.delete(ctrl),\n () => pendingPromises.delete(ctrl)\n )\n }\n return pending\n}\n\n/**\n * Access the current Lite.Scope from context.\n *\n * @returns The current Lite.Scope instance from context\n * @throws When called outside of a ScopeProvider\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const scope = useScope()\n * const handleClick = () => scope.resolve(myAtom)\n * }\n * ```\n */\nfunction useScope(): Lite.Scope {\n const scope = useContext(ScopeContext)\n if (!scope) {\n throw new Error(\"useScope must be used within a ScopeProvider\")\n }\n return scope\n}\n\n/**\n * Get a memoized controller for an atom.\n *\n * @example\n * ```tsx\n * const ctrl = useController(counterAtom)\n * ctrl.set(ctrl.get() + 1)\n * ```\n */\nfunction useController<T>(atom: Lite.Atom<T>): Lite.Controller<T>\nfunction useController<T>(atom: Lite.Atom<T>, options: UseControllerOptions): Lite.Controller<T>\nfunction useController<T>(atom: Lite.Atom<T>, options?: UseControllerOptions): Lite.Controller<T> {\n const scope = useScope()\n const ctrl = useMemo(() => scope.controller(atom), [scope, atom])\n\n if (options?.resolve) {\n if (ctrl.state === 'idle' || ctrl.state === 'resolving') {\n throw getOrCreatePendingPromise(ctrl)\n }\n if (ctrl.state === 'failed') {\n throw ctrl.get()\n }\n }\n\n return ctrl\n}\n\n/**\n * Subscribe to atom value with Suspense/ErrorBoundary integration.\n *\n * @example\n * ```tsx\n * const user = useAtom(userAtom)\n * const { data, loading, error } = useAtom(userAtom, { suspense: false })\n * ```\n */\nfunction useAtom<T>(atom: Lite.Atom<T>): T\nfunction useAtom<T>(atom: Lite.Atom<T>, options: UseAtomSuspenseOptions): T\nfunction useAtom<T>(atom: Lite.Atom<T>, options: UseAtomManualOptions): UseAtomState<T>\nfunction useAtom<T>(atom: Lite.Atom<T>, options?: UseAtomOptions): T | UseAtomState<T> {\n const ctrl = useController(atom)\n const ctrlState = ctrl.state\n\n const isSuspense = options?.suspense !== false\n const autoResolve = isSuspense ? options?.resolve !== false : !!options?.resolve\n\n const stateCache = useRef<{\n ctrl: Lite.Controller<T>\n ctrlState: Lite.Controller<T>['state']\n data: T | undefined\n error: Error | undefined\n loading: boolean\n result: UseAtomState<T>\n } | null>(null)\n\n useEffect(() => {\n if (!isSuspense && (ctrlState === 'resolving' || (autoResolve && ctrlState === 'idle'))) {\n void getOrCreatePendingPromise(ctrl).catch(() => {})\n }\n }, [ctrl, ctrlState, autoResolve, isSuspense])\n\n const getSnapshot = useCallback((): T | UseAtomState<T> => {\n if (isSuspense) {\n if (ctrl.state === 'idle') {\n if (autoResolve) {\n throw getOrCreatePendingPromise(ctrl)\n }\n throw new Error('Atom is not resolved. Set resolve: true or resolve the atom before rendering.')\n }\n if (ctrl.state === 'failed') {\n throw ctrl.get()\n }\n try {\n return ctrl.get()\n } catch {\n throw getOrCreatePendingPromise(ctrl)\n }\n }\n\n let data: T | undefined\n let error: Error | undefined\n\n if (ctrl.state === 'resolved' || ctrl.state === 'resolving') {\n try {\n data = ctrl.get()\n } catch {}\n } else if (ctrl.state === 'failed') {\n try {\n ctrl.get()\n } catch (e) {\n error = e instanceof Error ? e : new Error(String(e))\n }\n }\n\n const loading = ctrl.state === 'resolving' || (autoResolve && ctrl.state === 'idle')\n\n if (\n stateCache.current &&\n stateCache.current.ctrl === ctrl &&\n stateCache.current.ctrlState === ctrl.state &&\n stateCache.current.data === data &&\n stateCache.current.error === error &&\n stateCache.current.loading === loading\n ) {\n return stateCache.current.result\n }\n\n const result: UseAtomState<T> = {\n data,\n loading,\n error,\n controller: ctrl,\n }\n\n stateCache.current = { ctrl, ctrlState: ctrl.state, data, error, loading, result }\n return result\n }, [ctrl, autoResolve, isSuspense])\n\n const subscribe = useCallback((onStoreChange: () => void) => {\n return ctrl.on('*', () => {\n if (!isSuspense && ctrl.state === 'resolving') {\n void getOrCreatePendingPromise(ctrl).catch(() => {})\n }\n onStoreChange()\n })\n }, [ctrl, isSuspense])\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/**\n * Select a derived value from an atom with fine-grained reactivity.\n * Only re-renders when the selected value changes per equality function.\n *\n * @param atom - The atom to select from\n * @param selector - Function to extract a derived value\n * @param eq - Optional equality function\n * @returns The selected value\n *\n * @example\n * ```tsx\n * const name = useSelect(userAtom, user => user.name)\n * ```\n */\nfunction useSelect<T, S>(\n atom: Lite.Atom<T>,\n selector: (value: T) => S,\n eq?: (a: S, b: S) => boolean\n): S {\n const ctrl = useController(atom)\n\n const selectorRef = useRef(selector)\n const eqRef = useRef(eq)\n selectorRef.current = selector\n eqRef.current = eq\n\n const selectionCache = useRef<{\n ctrl: Lite.Controller<T>\n ctrlState: Lite.Controller<T>['state']\n source: T\n selector: (value: T) => S\n eq: ((a: S, b: S) => boolean) | undefined\n value: S\n } | null>(null)\n\n const getSnapshot = useCallback((): S => {\n const state = ctrl.state\n if (state === 'idle') {\n throw getOrCreatePendingPromise(ctrl)\n }\n if (state === 'failed') {\n throw ctrl.get()\n }\n let value: T\n try {\n value = ctrl.get()\n } catch {\n throw getOrCreatePendingPromise(ctrl)\n }\n\n const nextSelector = selectorRef.current\n const nextEq = eqRef.current\n const current = selectionCache.current\n if (\n current &&\n current.ctrl === ctrl &&\n current.ctrlState === state &&\n Object.is(current.source, value) &&\n current.selector === nextSelector &&\n current.eq === nextEq\n ) {\n return current.value\n }\n\n const nextValue = nextSelector(value)\n const selectedValue = current &&\n current.ctrl === ctrl &&\n current.selector === nextSelector &&\n (nextEq ?? Object.is)(current.value, nextValue)\n ? current.value\n : nextValue\n\n selectionCache.current = {\n ctrl,\n ctrlState: state,\n source: value,\n selector: nextSelector,\n eq: nextEq,\n value: selectedValue,\n }\n\n return selectedValue\n }, [ctrl])\n\n const subscribe = useCallback((onStoreChange: () => void) => {\n return ctrl.on('*', onStoreChange)\n }, [ctrl])\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\nexport { useScope, useController, useAtom, useSelect }\nexport type { UseAtomSuspenseOptions, UseAtomManualOptions, UseAtomOptions, UseAtomState, UseControllerOptions }\n"],"mappings":";;;;;;;;AAMA,MAAM,eAAe,cAAiC,KAAK;;;;;;;;;;;AAiB3D,SAAS,cAAc,EAAE,OAAO,YAAgC;AAC9D,QACE,oBAAC,aAAa;EAAS,OAAO;EAC3B;GACqB;;;;;ACE5B,MAAM,kCAAkB,IAAI,SAAqD;AAEjF,SAAS,0BAA6B,MAAsC;CAC1E,IAAI,UAAU,gBAAgB,IAAI,KAAK;AACvC,KAAI,CAAC,SAAS;AACZ,MAAI,KAAK,UAAU,YACjB,WAAU,IAAI,SAAY,SAAS,WAAW;GAC5C,MAAM,QAAQ,KAAK,GAAG,WAAW;AAC/B,QAAI,KAAK,UAAU,YAAY;AAC7B,YAAO;AACP,aAAQ,KAAK,KAAK,CAAC;eACV,KAAK,UAAU,UAAU;AAClC,YAAO;AACP,SAAI;AAAE,WAAK,KAAK;cAAU,GAAG;AAAE,aAAO,EAAE;;;KAE1C;IACF;MAEF,WAAU,KAAK,SAAS;AAE1B,kBAAgB,IAAI,MAAM,QAAQ;AAClC,EAAK,QAAQ,YAAY,GAAG;AAC5B,UAAQ,WACA,gBAAgB,OAAO,KAAK,QAC5B,gBAAgB,OAAO,KAAK,CACnC;;AAEH,QAAO;;;;;;;;;;;;;;;;AAiBT,SAAS,WAAuB;CAC9B,MAAM,QAAQ,WAAW,aAAa;AACtC,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,+CAA+C;AAEjE,QAAO;;AAcT,SAAS,cAAiB,QAAoB,SAAoD;CAChG,MAAM,QAAQ,UAAU;CACxB,MAAM,OAAO,cAAc,MAAM,WAAWA,OAAK,EAAE,CAAC,OAAOA,OAAK,CAAC;AAEjE,KAAI,SAAS,SAAS;AACpB,MAAI,KAAK,UAAU,UAAU,KAAK,UAAU,YAC1C,OAAM,0BAA0B,KAAK;AAEvC,MAAI,KAAK,UAAU,SACjB,OAAM,KAAK,KAAK;;AAIpB,QAAO;;AAeT,SAAS,QAAW,QAAoB,SAA+C;CACrF,MAAM,OAAO,cAAcA,OAAK;CAChC,MAAM,YAAY,KAAK;CAEvB,MAAM,aAAa,SAAS,aAAa;CACzC,MAAM,cAAc,aAAa,SAAS,YAAY,QAAQ,CAAC,CAAC,SAAS;CAEzE,MAAM,aAAa,OAOT,KAAK;AAEf,iBAAgB;AACd,MAAI,CAAC,eAAe,cAAc,eAAgB,eAAe,cAAc,QAC7E,CAAK,0BAA0B,KAAK,CAAC,YAAY,GAAG;IAErD;EAAC;EAAM;EAAW;EAAa;EAAW,CAAC;CAE9C,MAAM,cAAc,kBAAuC;AACzD,MAAI,YAAY;AACd,OAAI,KAAK,UAAU,QAAQ;AACzB,QAAI,YACF,OAAM,0BAA0B,KAAK;AAEvC,UAAM,IAAI,MAAM,gFAAgF;;AAElG,OAAI,KAAK,UAAU,SACjB,OAAM,KAAK,KAAK;AAElB,OAAI;AACF,WAAO,KAAK,KAAK;WACX;AACN,UAAM,0BAA0B,KAAK;;;EAIzC,IAAIC;EACJ,IAAIC;AAEJ,MAAI,KAAK,UAAU,cAAc,KAAK,UAAU,YAC9C,KAAI;AACF,UAAO,KAAK,KAAK;UACX;WACC,KAAK,UAAU,SACxB,KAAI;AACF,QAAK,KAAK;WACH,GAAG;AACV,WAAQ,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,EAAE,CAAC;;EAIzD,MAAM,UAAU,KAAK,UAAU,eAAgB,eAAe,KAAK,UAAU;AAE7E,MACE,WAAW,WACX,WAAW,QAAQ,SAAS,QAC5B,WAAW,QAAQ,cAAc,KAAK,SACtC,WAAW,QAAQ,SAAS,QAC5B,WAAW,QAAQ,UAAU,SAC7B,WAAW,QAAQ,YAAY,QAE/B,QAAO,WAAW,QAAQ;EAG5B,MAAMC,SAA0B;GAC9B;GACA;GACA;GACA,YAAY;GACb;AAED,aAAW,UAAU;GAAE;GAAM,WAAW,KAAK;GAAO;GAAM;GAAO;GAAS;GAAQ;AAClF,SAAO;IACN;EAAC;EAAM;EAAa;EAAW,CAAC;AAWnC,QAAO,qBATW,aAAa,kBAA8B;AAC3D,SAAO,KAAK,GAAG,WAAW;AACxB,OAAI,CAAC,cAAc,KAAK,UAAU,YAChC,CAAK,0BAA0B,KAAK,CAAC,YAAY,GAAG;AAEtD,kBAAe;IACf;IACD,CAAC,MAAM,WAAW,CAAC,EAEiB,aAAa,YAAY;;;;;;;;;;;;;;;;AAiBlE,SAAS,UACP,QACA,UACA,IACG;CACH,MAAM,OAAO,cAAcH,OAAK;CAEhC,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,QAAQ,OAAO,GAAG;AACxB,aAAY,UAAU;AACtB,OAAM,UAAU;CAEhB,MAAM,iBAAiB,OAOb,KAAK;CAEf,MAAM,cAAc,kBAAqB;EACvC,MAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,OACZ,OAAM,0BAA0B,KAAK;AAEvC,MAAI,UAAU,SACZ,OAAM,KAAK,KAAK;EAElB,IAAII;AACJ,MAAI;AACF,WAAQ,KAAK,KAAK;UACZ;AACN,SAAM,0BAA0B,KAAK;;EAGvC,MAAM,eAAe,YAAY;EACjC,MAAM,SAAS,MAAM;EACrB,MAAM,UAAU,eAAe;AAC/B,MACE,WACA,QAAQ,SAAS,QACjB,QAAQ,cAAc,SACtB,OAAO,GAAG,QAAQ,QAAQ,MAAM,IAChC,QAAQ,aAAa,gBACrB,QAAQ,OAAO,OAEf,QAAO,QAAQ;EAGjB,MAAM,YAAY,aAAa,MAAM;EACrC,MAAM,gBAAgB,WACpB,QAAQ,SAAS,QACjB,QAAQ,aAAa,iBACpB,UAAU,OAAO,IAAI,QAAQ,OAAO,UAAU,GAC7C,QAAQ,QACR;AAEJ,iBAAe,UAAU;GACvB;GACA,WAAW;GACX,QAAQ;GACR,UAAU;GACV,IAAI;GACJ,OAAO;GACR;AAED,SAAO;IACN,CAAC,KAAK,CAAC;AAMV,QAAO,qBAJW,aAAa,kBAA8B;AAC3D,SAAO,KAAK,GAAG,KAAK,cAAc;IACjC,CAAC,KAAK,CAAC,EAE6B,aAAa,YAAY"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pumped-fn/lite-react",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "React integration for @pumped-fn/lite",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -31,13 +31,14 @@
31
31
  "devDependencies": {
32
32
  "@testing-library/jest-dom": "^6.9.1",
33
33
  "@testing-library/react": "^16.3.0",
34
- "@types/react": "^18.3.18",
35
- "jsdom": "^27.2.0",
34
+ "@types/node": "^20.19.22",
35
+ "@types/react": "^18.3.27",
36
+ "jsdom": "^27.3.0",
36
37
  "react": "^18.3.1",
37
- "tsdown": "^0.16.5",
38
+ "tsdown": "^0.17.2",
38
39
  "typescript": "^5.9.3",
39
- "vitest": "^4.0.5",
40
- "@pumped-fn/lite": "1.6.0"
40
+ "vitest": "^4.0.18",
41
+ "@pumped-fn/lite": "2.1.2"
41
42
  },
42
43
  "engines": {
43
44
  "node": ">=18"