@kdeveloper/kvark 0.17.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 +158 -115
- package/dist/family.d.ts +16 -13
- package/dist/family.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/preact/index.d.ts +4 -4
- package/dist/preact/index.js +7 -33
- package/dist/preact/index.js.map +1 -1
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +7 -33
- package/dist/react/index.js.map +1 -1
- package/dist/{store-DSvJVL5c.js → store-Cq_V-puR.js} +178 -56
- package/dist/store-Cq_V-puR.js.map +1 -0
- package/dist/{types-CHPLxaFJ.d.ts → types-D8ep-ydI.d.ts} +58 -26
- package/dist/use-atom-value-BhGvD9Vf.js +35 -0
- package/dist/use-atom-value-BhGvD9Vf.js.map +1 -0
- package/dist/vue/index.d.ts +4 -4
- package/dist/vue/index.js +9 -9
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/store-DSvJVL5c.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@ Inspired by Jotai, but built around a key difference: dependencies are declared
|
|
|
11
11
|
| | Jotai | Kvark |
|
|
12
12
|
| ---------------------- | -------------------------------- | ----------------------------------------------- |
|
|
13
13
|
| Dependency declaration | Implicit (via `get(atom)` calls) | Explicit, via `dependencies` field |
|
|
14
|
-
| `atom()` signature | `atom(read, write?)` | `atom({
|
|
15
|
-
| Async model | Optional | `
|
|
14
|
+
| `atom()` signature | `atom(read, write?)` | `atom({ read, write?, dependencies? })` |
|
|
15
|
+
| Async model | Optional | `read`, `write`, `ctx.read` — always `async` |
|
|
16
16
|
| Parallel loading | Manual `Promise.all` | Built-in through `dependencies` |
|
|
17
17
|
| Stale-while-revalidate | Re-suspends on revalidation | Shows stale data; Suspense only on first load |
|
|
18
18
|
| `atomFamily` | External (`jotai/utils`) | Core, with LRU and invalidation |
|
|
19
|
-
| External invalidation |
|
|
19
|
+
| External invalidation | Manual via store mutation APIs | Explicit `store.invalidate()` / `StoreClient` |
|
|
20
20
|
| TypeScript strictness | `strict: true` recommended | `strict: true` + 8 extra flags, `any` forbidden |
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
@@ -30,20 +30,20 @@ pnpm add @kdeveloper/kvark
|
|
|
30
30
|
|
|
31
31
|
```tsx
|
|
32
32
|
import { atom, createStore } from "@kdeveloper/kvark";
|
|
33
|
-
import { Provider,
|
|
33
|
+
import { Provider, useApplyAtom, useAtomValue } from "@kdeveloper/kvark/react";
|
|
34
34
|
|
|
35
35
|
// 1. Create atoms
|
|
36
36
|
const userIdAtom = atom({
|
|
37
37
|
debugLabel: "userId",
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
read: async () => 1,
|
|
39
|
+
write: async (_ctx, _id: number) => {},
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
const userAtom = atom({
|
|
43
43
|
debugLabel: "user",
|
|
44
44
|
dependencies: { userId: userIdAtom },
|
|
45
|
-
|
|
46
|
-
const id = await ctx.
|
|
45
|
+
read: async (ctx) => {
|
|
46
|
+
const id = await ctx.read("userId");
|
|
47
47
|
const res = await fetch(`/api/users/${id}`, { signal: ctx.signal });
|
|
48
48
|
return res.json();
|
|
49
49
|
},
|
|
@@ -75,14 +75,14 @@ function UserCard() {
|
|
|
75
75
|
The same atoms and store work with `@kdeveloper/kvark/preact` — the API is identical to the React integration. Replace the React import with the Preact one:
|
|
76
76
|
|
|
77
77
|
```tsx
|
|
78
|
-
import { Provider,
|
|
78
|
+
import { Provider, useApplyAtom, useAtomValue } from "@kdeveloper/kvark/preact";
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Hooks use only `preact` and `preact/hooks` internally — no `preact/compat` aliases required.
|
|
82
82
|
|
|
83
83
|
### Vue 3
|
|
84
84
|
|
|
85
|
-
The same atoms and store work with `@kdeveloper/kvark/vue`. Composables mirror the React hooks; `useAtomValue` returns an **awaitable** [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref) — use `.value` in `<script setup>` (templates unwrap refs automatically). Until the first `
|
|
85
|
+
The same atoms and store work with `@kdeveloper/kvark/vue`. Composables mirror the React hooks; `useAtomValue` returns an **awaitable** [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref) — use `.value` in `<script setup>` (templates unwrap refs automatically). Until the first `read` resolves, that ref’s value is `undefined`; TypeScript types it as `V | undefined` (`ThenableShallowRef<V>`). After `await useAtomValue(atom)` you get `Readonly<ShallowRef<V>>` with a defined `.value`. The ref also implements `PromiseLike`, so awaiting it in an async `setup` suspends until the first `read` resolves — see [Suspense (Vue 3)](#suspense-vue-3).
|
|
86
86
|
|
|
87
87
|
Composables must run **inside** a `Provider` subtree. Put `useAtomValue` / `useAtom` in a child component (or a nested route), not in the same component that renders `Provider` without passing the store through a wrapper.
|
|
88
88
|
|
|
@@ -93,15 +93,15 @@ import { atom } from "@kdeveloper/kvark";
|
|
|
93
93
|
|
|
94
94
|
export const userIdAtom = atom({
|
|
95
95
|
debugLabel: "userId",
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
read: async () => 1,
|
|
97
|
+
write: async () => {},
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
export const userAtom = atom({
|
|
101
101
|
debugLabel: "user",
|
|
102
102
|
dependencies: { userId: userIdAtom },
|
|
103
|
-
|
|
104
|
-
const id = await ctx.
|
|
103
|
+
read: async (ctx) => {
|
|
104
|
+
const id = await ctx.read("userId");
|
|
105
105
|
const res = await fetch(`/api/users/${id}`, { signal: ctx.signal });
|
|
106
106
|
return res.json();
|
|
107
107
|
},
|
|
@@ -149,7 +149,7 @@ Unlike React, Vue’s `<Suspense>` only activates for **async** child components
|
|
|
149
149
|
|
|
150
150
|
## Store Context
|
|
151
151
|
|
|
152
|
-
Kvark can inject arbitrary application context into every atom and mutation callback through the store, similar to router context in TanStack Router. The context is immutable for the lifetime of a store and is available in `
|
|
152
|
+
Kvark can inject arbitrary application context into every atom and mutation callback through the store, similar to router context in TanStack Router. The context is immutable for the lifetime of a store and is available in `read`, `write`, `onMount`, `mutation`, `batch.fetch`, and `infinityAtom.queryFn`.
|
|
153
153
|
|
|
154
154
|
First, declare the context type once with module augmentation:
|
|
155
155
|
|
|
@@ -186,10 +186,10 @@ import { atom, mutation } from "@kdeveloper/kvark";
|
|
|
186
186
|
|
|
187
187
|
const userAtom = atom({
|
|
188
188
|
dependencies: { userId: userIdAtom },
|
|
189
|
-
|
|
190
|
-
return ctx.global.apiClient.users.get(await ctx.
|
|
189
|
+
read: async (ctx) => {
|
|
190
|
+
return ctx.global.apiClient.users.get(await ctx.read("userId"));
|
|
191
191
|
},
|
|
192
|
-
|
|
192
|
+
write: async (ctx, patch: Partial<User>) => {
|
|
193
193
|
await ctx.global.apiClient.users.update(ctx.global.currentUser.id, patch);
|
|
194
194
|
},
|
|
195
195
|
});
|
|
@@ -224,7 +224,7 @@ Batch families receive it via `input.ctx.global` inside `batch.fetch`.
|
|
|
224
224
|
|
|
225
225
|
### Atoms
|
|
226
226
|
|
|
227
|
-
An atom is the smallest unit of state. Its `
|
|
227
|
+
An atom is the smallest unit of state. Its `read` function is always `async`, and dependencies must be declared explicitly.
|
|
228
228
|
|
|
229
229
|
```ts
|
|
230
230
|
import { atom } from "@kdeveloper/kvark";
|
|
@@ -232,16 +232,16 @@ import { atom } from "@kdeveloper/kvark";
|
|
|
232
232
|
// Primitive atom — no dependencies
|
|
233
233
|
const countAtom = atom({
|
|
234
234
|
debugLabel: "count",
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
read: async () => 0,
|
|
236
|
+
write: async (_ctx, _value: number) => {},
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
// Derived atom — reads from another atom
|
|
240
240
|
const doubleAtom = atom({
|
|
241
241
|
debugLabel: "double",
|
|
242
242
|
dependencies: { count: countAtom },
|
|
243
|
-
|
|
244
|
-
const n = await ctx.
|
|
243
|
+
read: async (ctx) => {
|
|
244
|
+
const n = await ctx.read("count");
|
|
245
245
|
return n * 2;
|
|
246
246
|
},
|
|
247
247
|
});
|
|
@@ -249,7 +249,7 @@ const doubleAtom = atom({
|
|
|
249
249
|
|
|
250
250
|
#### Simple primitive atom (`atom(initialValue)`)
|
|
251
251
|
|
|
252
|
-
For purely client-side state — counters, toggles, draft fields — there is a shorthand: pass an initial value directly. The result is a `WritableAtom` whose
|
|
252
|
+
For purely client-side state — counters, toggles, draft fields — there is a shorthand: pass an initial value directly. The result is a `WritableAtom` whose apply function accepts either the next value or an updater function `(prev) => next`. Both `store.apply(atom, value)` and `store.write(atom, value)` are supported: `apply` runs the atom's `write` callback, while `write` pushes straight into the cache. React/Preact/Vue hooks (`useAtomValue`, `useApplyAtom`, `useAtom`) work as usual.
|
|
253
253
|
|
|
254
254
|
```ts
|
|
255
255
|
import { atom, createStore } from "@kdeveloper/kvark";
|
|
@@ -268,27 +268,27 @@ const clockAtom = atom(Date.now(), {
|
|
|
268
268
|
|
|
269
269
|
const store = createStore();
|
|
270
270
|
|
|
271
|
-
await store.
|
|
272
|
-
await store.
|
|
271
|
+
await store.apply(counterAtom, 5);
|
|
272
|
+
await store.apply(counterAtom, (prev) => prev + 1);
|
|
273
273
|
store.write(counterAtom, 42);
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
The shorthand is intentionally limited to `debugLabel` and `onMount`; for `dependencies`, `stalePolicy`, `retry`, or async loading use the full `atom({
|
|
276
|
+
The shorthand is intentionally limited to `debugLabel` and `onMount`; for `dependencies`, `stalePolicy`, `retry`, or async loading use the full `atom({ read, write?, ... })` form.
|
|
277
277
|
|
|
278
|
-
> Note: if your initial value is itself an object that exposes a `
|
|
278
|
+
> Note: if your initial value is itself an object that exposes a `read` method (and would be parsed as an atom config), use the explicit form: `atom({ read: async () => obj, write: async (ctx, next) => ctx.setOptimisticValue(next) })`.
|
|
279
279
|
|
|
280
280
|
### Writable atoms
|
|
281
281
|
|
|
282
|
-
An atom is writable when its config includes a `
|
|
282
|
+
An atom is writable when its config includes a `write` function. The return type becomes `WritableAtom<Value, Args>`, where `Args` is the tuple of arguments accepted by `store.apply(atom, ...args)` / `useApplyAtom(atom)(...args)`.
|
|
283
283
|
|
|
284
284
|
```ts
|
|
285
285
|
const profileAtom = atom({
|
|
286
286
|
debugLabel: "profile",
|
|
287
|
-
|
|
287
|
+
read: async () => {
|
|
288
288
|
const res = await fetch("/api/profile");
|
|
289
289
|
return res.json() as Promise<Profile>;
|
|
290
290
|
},
|
|
291
|
-
|
|
291
|
+
write: async (ctx, patch: Partial<Profile>) => {
|
|
292
292
|
await fetch("/api/profile", {
|
|
293
293
|
method: "PATCH",
|
|
294
294
|
body: JSON.stringify(patch),
|
|
@@ -298,11 +298,11 @@ const profileAtom = atom({
|
|
|
298
298
|
});
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
**Lifecycle after `
|
|
301
|
+
**Lifecycle after `apply`:** the store calls `config.write(ctx, ...args)`, waits for the returned promise, then calls `invalidate` on the atom. This marks it `stale` and triggers a background refetch via `read` — the same stale-while-revalidate flow described [above](#stale-while-revalidate).
|
|
302
302
|
|
|
303
|
-
The `ctx` passed to `
|
|
303
|
+
The `ctx` passed to `write` provides:
|
|
304
304
|
|
|
305
|
-
- **`ctx.
|
|
305
|
+
- **`ctx.read(key)`** — read any declared dependency (same as in `read`).
|
|
306
306
|
- **`ctx.signal`** — an `AbortSignal` tied to the atom's lifecycle, useful for cancelling in-flight requests.
|
|
307
307
|
- **`ctx.setOptimisticValue(value)`** — synchronously update the atom's cached value before the async work completes (see below). Also accepts a mutator `(prev) => next`.
|
|
308
308
|
- **`ctx.writeOptimistic(atom, value)`** — synchronously write into another atom's cache with automatic rollback on error (see [Dependent mutations](#dependent-mutations)). Also accepts a mutator.
|
|
@@ -310,16 +310,16 @@ The `ctx` passed to `set` provides:
|
|
|
310
310
|
|
|
311
311
|
#### Optimistic updates
|
|
312
312
|
|
|
313
|
-
Call `ctx.setOptimisticValue(value)` inside `
|
|
313
|
+
Call `ctx.setOptimisticValue(value)` inside `write` to immediately reflect the new value in the UI while the mutation runs in the background. If the mutation throws (or the signal aborts), the store automatically rolls back to the state captured before the first `setOptimisticValue` call. Derived atoms that depend on this atom are marked `stale` so they re-render too.
|
|
314
314
|
|
|
315
315
|
```ts
|
|
316
316
|
const todoAtom = atom({
|
|
317
317
|
debugLabel: "todo",
|
|
318
|
-
|
|
318
|
+
read: async () => {
|
|
319
319
|
const res = await fetch("/api/todo");
|
|
320
320
|
return res.json() as Promise<Todo>;
|
|
321
321
|
},
|
|
322
|
-
|
|
322
|
+
write: async (ctx, title: string) => {
|
|
323
323
|
ctx.setOptimisticValue({ title, done: false });
|
|
324
324
|
await fetch("/api/todo", {
|
|
325
325
|
method: "PUT",
|
|
@@ -333,17 +333,17 @@ const todoAtom = atom({
|
|
|
333
333
|
`setOptimisticValue` also accepts a mutator function — the callback receives the previous value (or `undefined` if pending) and returns the next one:
|
|
334
334
|
|
|
335
335
|
```ts
|
|
336
|
-
|
|
336
|
+
write: async (ctx) => {
|
|
337
337
|
ctx.setOptimisticValue((prev) => ({ ...prev, loading: true }));
|
|
338
338
|
await fetch("/api/update", { signal: ctx.signal });
|
|
339
339
|
},
|
|
340
340
|
```
|
|
341
341
|
|
|
342
|
-
If the `PUT` fails, the atom reverts to whatever value `
|
|
342
|
+
If the `PUT` fails, the atom reverts to whatever value `read` had loaded before the optimistic update — no manual rollback needed.
|
|
343
343
|
|
|
344
344
|
#### Dependent mutations
|
|
345
345
|
|
|
346
|
-
The `ctx` inside `
|
|
346
|
+
The `ctx` inside `write` also provides methods for cross-atom side-effects:
|
|
347
347
|
|
|
348
348
|
- **`ctx.invalidate(atom)`** — mark another atom as `stale` and schedule a refetch. Does **not** participate in rollback; best called after the async work succeeds.
|
|
349
349
|
- **`ctx.invalidateMany(atoms)`** — same as `invalidate`, but for multiple atoms at once.
|
|
@@ -354,7 +354,7 @@ A common pattern is a list atom alongside per-item atoms managed by `atomFamily`
|
|
|
354
354
|
```ts
|
|
355
355
|
const todosListAtom = atom({
|
|
356
356
|
debugLabel: "todosList",
|
|
357
|
-
|
|
357
|
+
read: async () => {
|
|
358
358
|
const res = await fetch("/api/todos");
|
|
359
359
|
return res.json() as Promise<Todo[]>;
|
|
360
360
|
},
|
|
@@ -362,11 +362,11 @@ const todosListAtom = atom({
|
|
|
362
362
|
|
|
363
363
|
const todoAtom = atomFamily({
|
|
364
364
|
debugLabel: "todo",
|
|
365
|
-
|
|
365
|
+
read: (id: number) => async () => {
|
|
366
366
|
const res = await fetch(`/api/todos/${id}`);
|
|
367
367
|
return res.json() as Promise<Todo>;
|
|
368
368
|
},
|
|
369
|
-
|
|
369
|
+
write: (id: number) => async (ctx, patch: Partial<Todo>) => {
|
|
370
370
|
ctx.setOptimisticValue((prev) => ({ ...prev!, ...patch }));
|
|
371
371
|
ctx.writeOptimistic(todosListAtom, (list) =>
|
|
372
372
|
list?.map((t) => (t.id === id ? { ...t, ...patch } : t)),
|
|
@@ -387,20 +387,20 @@ If the `PATCH` fails, both the item and the list revert to their pre-optimistic
|
|
|
387
387
|
|
|
388
388
|
Sometimes a server action updates **several** atoms at once, and there is no single writable atom that should own the mutation. Use **`mutation(fn)`** to define that work once, then run it with **`store.mutate(m, ...args)`** or **`useMutation(m)`** in components.
|
|
389
389
|
|
|
390
|
-
The callback receives a **`MutationContext`** (the same optimistic and invalidation helpers as in `
|
|
390
|
+
The callback receives a **`MutationContext`** (the same optimistic and invalidation helpers as in `write`, but **without** `setOptimisticValue` or `ctx.signal`). Standalone mutations can read any atom with **`ctx.read(atom)`**. In contrast, inside **`write`**, **`ctx.read(...)`** still means "read a declared dependency by key":
|
|
391
391
|
|
|
392
|
-
- **`ctx.
|
|
393
|
-
- **`ctx.writeOptimistic(atom, value | mutator)`** — same semantics as inside `
|
|
392
|
+
- **`ctx.read(atom)`** — read any atom with the same semantics as `store.read(atom)` / `client.read(atom)`.
|
|
393
|
+
- **`ctx.writeOptimistic(atom, value | mutator)`** — same semantics as inside `write`. On throw, every atom touched by `writeOptimistic` rolls back to its state before the first optimistic write in this run.
|
|
394
394
|
- **`ctx.invalidate` / `ctx.invalidateMany`** — mark atoms stale (not rolled back on error).
|
|
395
395
|
|
|
396
|
-
Unlike **`
|
|
396
|
+
Unlike **`write`**, a successful **`mutate` does not invalidate any atom automatically**. Call `ctx.invalidate` / `ctx.invalidateMany` when you want stale-while-revalidate after the request completes.
|
|
397
397
|
|
|
398
398
|
```ts
|
|
399
399
|
import { atom, mutation, createStore } from "@kdeveloper/kvark";
|
|
400
400
|
|
|
401
401
|
const listAtom = atom({
|
|
402
402
|
debugLabel: "list",
|
|
403
|
-
|
|
403
|
+
read: async () => {
|
|
404
404
|
const res = await fetch("/api/items");
|
|
405
405
|
return res.json() as Promise<string[]>;
|
|
406
406
|
},
|
|
@@ -408,7 +408,7 @@ const listAtom = atom({
|
|
|
408
408
|
|
|
409
409
|
const countAtom = atom({
|
|
410
410
|
debugLabel: "count",
|
|
411
|
-
|
|
411
|
+
read: async () => {
|
|
412
412
|
const res = await fetch("/api/count");
|
|
413
413
|
return (await res.json()) as { n: number };
|
|
414
414
|
},
|
|
@@ -452,22 +452,22 @@ const runAddItem = useMutation(addItem);
|
|
|
452
452
|
|
|
453
453
|
Both can update an atom's cached value, but they serve different purposes:
|
|
454
454
|
|
|
455
|
-
| | `
|
|
456
|
-
| ---------------- |
|
|
457
|
-
| **Triggered by** | Explicit call (`store.
|
|
458
|
-
| **After update** | `invalidate` → refetch via `
|
|
459
|
-
| **Use case** | Mutations, API calls, optimistic updates
|
|
455
|
+
| | `write` | `onMount` |
|
|
456
|
+
| ---------------- | --------------------------------------------- | -------------------------------------- |
|
|
457
|
+
| **Triggered by** | Explicit call (`store.apply`, `useApplyAtom`) | First subscriber mounts |
|
|
458
|
+
| **After update** | `invalidate` → refetch via `read` | No refetch — value stays as-is |
|
|
459
|
+
| **Use case** | Mutations, API calls, optimistic updates | Timers, subscriptions, imperative push |
|
|
460
460
|
|
|
461
461
|
### `onMount`
|
|
462
462
|
|
|
463
|
-
Optional lifecycle hook that runs when the atom **first gains a subscriber** in a store (for example when a component using `useAtomValue` mounts). It receives a synchronous `set(value)` that marks the atom `fresh` and notifies listeners — useful for timers, subscriptions, or imperative updates that should not go through `
|
|
463
|
+
Optional lifecycle hook that runs when the atom **first gains a subscriber** in a store (for example when a component using `useAtomValue` mounts). It receives a synchronous `set(value)` that marks the atom `fresh` and notifies listeners — useful for timers, subscriptions, or imperative updates that should not go through `read`.
|
|
464
464
|
|
|
465
465
|
You may return a cleanup function; it runs when the **last** subscriber unsubscribes (for example when the last mounted consumer unmounts). If several components subscribe to the same atom, `onMount` runs once and the cleanup runs once after all of them unsubscribe.
|
|
466
466
|
|
|
467
467
|
```ts
|
|
468
468
|
const clockAtom = atom({
|
|
469
469
|
debugLabel: "clock",
|
|
470
|
-
|
|
470
|
+
read: async () => new Date().toISOString(),
|
|
471
471
|
onMount: (set) => {
|
|
472
472
|
const id = setInterval(() => {
|
|
473
473
|
set(new Date().toISOString());
|
|
@@ -479,13 +479,13 @@ const clockAtom = atom({
|
|
|
479
479
|
|
|
480
480
|
### Parallel loading
|
|
481
481
|
|
|
482
|
-
Declaring multiple dependencies causes the Store to
|
|
482
|
+
Declaring multiple dependencies causes the Store to start loading them in parallel before calling `read`. Inside `read` you control the parallelism explicitly.
|
|
483
483
|
|
|
484
484
|
```ts
|
|
485
485
|
const dashboardAtom = atom({
|
|
486
486
|
dependencies: { user: userAtom, settings: settingsAtom },
|
|
487
|
-
|
|
488
|
-
const [user, settings] = await Promise.all([ctx.
|
|
487
|
+
read: async (ctx) => {
|
|
488
|
+
const [user, settings] = await Promise.all([ctx.read("user"), ctx.read("settings")]);
|
|
489
489
|
return { user, settings };
|
|
490
490
|
},
|
|
491
491
|
});
|
|
@@ -528,13 +528,13 @@ Available `stalePolicy` values:
|
|
|
528
528
|
|
|
529
529
|
### Retry on error
|
|
530
530
|
|
|
531
|
-
By default a failed `
|
|
531
|
+
By default a failed `read` immediately sets the atom to `error` state. You can opt into automatic retries with the `retry` and `retryDelay` options:
|
|
532
532
|
|
|
533
533
|
```ts
|
|
534
534
|
const userAtom = atom({
|
|
535
535
|
retry: 3,
|
|
536
536
|
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000),
|
|
537
|
-
|
|
537
|
+
read: async (ctx) => {
|
|
538
538
|
const res = await fetch("/api/user", { signal: ctx.signal });
|
|
539
539
|
if (!res.ok) throw new Error("Failed to fetch user");
|
|
540
540
|
return res.json();
|
|
@@ -553,7 +553,7 @@ The same options are available in `atomFamily` and `infinityAtom`.
|
|
|
553
553
|
|
|
554
554
|
### `atomFamily`
|
|
555
555
|
|
|
556
|
-
Create a family of atoms parametrised by a key.
|
|
556
|
+
Create a family of atoms parametrised by a key. Members use `read(param)` and optional `write(param)` callbacks, are cached by param, and support LRU eviction.
|
|
557
557
|
|
|
558
558
|
```ts
|
|
559
559
|
import { atomFamily } from "@kdeveloper/kvark/family";
|
|
@@ -563,8 +563,8 @@ const postFamily = atomFamily({
|
|
|
563
563
|
cachePolicy: "lru",
|
|
564
564
|
lruSize: 50,
|
|
565
565
|
dependencies: (_postId: number) => ({ user: userAtom }),
|
|
566
|
-
|
|
567
|
-
const user = await ctx.
|
|
566
|
+
read: (postId) => async (ctx) => {
|
|
567
|
+
const user = await ctx.read("user");
|
|
568
568
|
const res = await fetch(`/api/posts/${postId}?userId=${user.id}`, {
|
|
569
569
|
signal: ctx.signal,
|
|
570
570
|
});
|
|
@@ -581,24 +581,32 @@ postFamily.invalidate(42);
|
|
|
581
581
|
|
|
582
582
|
// Invalidate everything (e.g. on logout)
|
|
583
583
|
postFamily.invalidateAll();
|
|
584
|
+
|
|
585
|
+
// Cache helpers
|
|
586
|
+
postFamily.has(42);
|
|
587
|
+
postFamily.peek(42); // Atom<Post> | undefined, does not create a member
|
|
588
|
+
postFamily.remove(42);
|
|
589
|
+
postFamily.clear();
|
|
584
590
|
```
|
|
585
591
|
|
|
592
|
+
`atomFamily` and `infinityAtomFamily` expose the same cache helper shape: `has(param)`, `peek(param)`, `remove(param)`, `clear()`, and `getCache()`. `peek`/`has` inspect the current cache without creating a new atom; calling the family itself still creates or touches the member and updates LRU order.
|
|
593
|
+
|
|
586
594
|
### `atomFamily` as a dependency
|
|
587
595
|
|
|
588
|
-
A `dependencies` entry can be either a regular `Atom` or an `atomFamily`. When the entry is a family, supply the param at the call site as the **second positional argument** to `ctx.
|
|
596
|
+
A `dependencies` entry can be either a regular `Atom` or an `atomFamily`. When the entry is a family, supply the param at the call site as the **second positional argument** to `ctx.read`. Each unique param maps to its own family member; reverse-dependency edges are wired up automatically, so invalidating any read member (or the whole family) propagates `stale` to the consumer.
|
|
589
597
|
|
|
590
598
|
```ts
|
|
591
599
|
import { atom } from "@kdeveloper/kvark";
|
|
592
600
|
import { atomFamily } from "@kdeveloper/kvark/family";
|
|
593
601
|
|
|
594
602
|
const userFamily = atomFamily({
|
|
595
|
-
|
|
603
|
+
read: (id: number) => async () => fetch(`/api/users/${id}`).then((r) => r.json()),
|
|
596
604
|
});
|
|
597
605
|
|
|
598
606
|
const profileAtom = atom({
|
|
599
607
|
dependencies: { user: userFamily },
|
|
600
|
-
|
|
601
|
-
const user = await ctx.
|
|
608
|
+
read: async (ctx) => {
|
|
609
|
+
const user = await ctx.read("user", 7); // reads userFamily(7)
|
|
602
610
|
return `profile-of-${user.name}`;
|
|
603
611
|
},
|
|
604
612
|
});
|
|
@@ -607,31 +615,31 @@ const profileAtom = atom({
|
|
|
607
615
|
userFamily.invalidate(7); // marks profileAtom stale
|
|
608
616
|
```
|
|
609
617
|
|
|
610
|
-
The signature of `ctx.
|
|
618
|
+
The signature of `ctx.read` is keyed on the dependency entry:
|
|
611
619
|
|
|
612
|
-
| Entry | Call form
|
|
613
|
-
| ------------ |
|
|
614
|
-
| `Atom<V>` | `ctx.
|
|
615
|
-
| `atomFamily` | `ctx.
|
|
620
|
+
| Entry | Call form | Compile-time errors |
|
|
621
|
+
| ------------ | ---------------------- | ---------------------------------------------------- |
|
|
622
|
+
| `Atom<V>` | `ctx.read(key)` | passing a second arg → error |
|
|
623
|
+
| `atomFamily` | `ctx.read(key, param)` | omitting `param` → error; wrong `param` type → error |
|
|
616
624
|
|
|
617
625
|
```ts
|
|
618
626
|
const a = atom({
|
|
619
627
|
dependencies: { base: countAtom, user: userFamily },
|
|
620
|
-
|
|
621
|
-
await ctx.
|
|
622
|
-
await ctx.
|
|
628
|
+
read: async (ctx) => {
|
|
629
|
+
await ctx.read("base"); // ok
|
|
630
|
+
await ctx.read("user", 1); // ok
|
|
623
631
|
|
|
624
632
|
// @ts-expect-error family-key requires a parameter
|
|
625
|
-
ctx.
|
|
633
|
+
ctx.read("user");
|
|
626
634
|
// @ts-expect-error plain atom-key does not accept a parameter
|
|
627
|
-
ctx.
|
|
635
|
+
ctx.read("base", 1);
|
|
628
636
|
// @ts-expect-error param type must match the family's `Param`
|
|
629
|
-
ctx.
|
|
637
|
+
ctx.read("user", "not-a-number");
|
|
630
638
|
},
|
|
631
639
|
});
|
|
632
640
|
```
|
|
633
641
|
|
|
634
|
-
Repeated reads with the same `param` are deduplicated through the in-flight promise of the
|
|
642
|
+
Repeated reads with the same `param` are deduplicated through the in-flight promise of the already-read family member; reads with different params use distinct atoms and run independently. Family-typed dependencies are **not** preloaded before `read` runs (the param is only known at the `ctx.read` call site), so unread members never trigger a fetch.
|
|
635
643
|
|
|
636
644
|
### Object params and `paramKey`
|
|
637
645
|
|
|
@@ -643,14 +651,14 @@ import { atomFamily, stableFamilyKey } from "@kdeveloper/kvark/family";
|
|
|
643
651
|
const searchFamily = atomFamily({
|
|
644
652
|
// Two different object literals with the same fields → same atom
|
|
645
653
|
paramKey: (filters) => stableFamilyKey(filters),
|
|
646
|
-
|
|
654
|
+
read: (filters) => async () => fetchResults(filters),
|
|
647
655
|
});
|
|
648
656
|
|
|
649
657
|
searchFamily({ page: 1, query: "hello" }); // creates atom, key = '{"page":1,"query":"hello"}'
|
|
650
658
|
searchFamily({ query: "hello", page: 1 }); // returns the same atom (fields are sorted)
|
|
651
659
|
```
|
|
652
660
|
|
|
653
|
-
The `paramKey` function is called once per `family(param)` invocation. The returned key is used for all cache operations (`
|
|
661
|
+
The `paramKey` function is called once per `family(param)` invocation. The returned key is used for all cache operations (`read`, `apply`, `invalidate`, `remove`, LRU). The original `param` is still passed to `read`, `write`, and `dependencies`.
|
|
654
662
|
|
|
655
663
|
#### `stableFamilyKey(value)`
|
|
656
664
|
|
|
@@ -663,7 +671,7 @@ A built-in helper that serialises plain objects and arrays into a deterministic
|
|
|
663
671
|
|
|
664
672
|
### Batching (`atomFamily` with `batch`)
|
|
665
673
|
|
|
666
|
-
Instead of fetching each atom individually, you can batch multiple concurrent requests into a single call — inspired by [`@yornaath/batshit`](https://www.npmjs.com/package/@yornaath/batshit). Replace `
|
|
674
|
+
Instead of fetching each atom individually, you can batch multiple concurrent requests into a single call — inspired by [`@yornaath/batshit`](https://www.npmjs.com/package/@yornaath/batshit). Replace `read` with `batch`:
|
|
667
675
|
|
|
668
676
|
```ts
|
|
669
677
|
import { atomFamily, windowScheduler } from "@kdeveloper/kvark/family";
|
|
@@ -674,7 +682,7 @@ const userFamily = atomFamily({
|
|
|
674
682
|
batch: {
|
|
675
683
|
scheduler: windowScheduler(10),
|
|
676
684
|
fetch: async ({ keys, params, signal, ctx }) => {
|
|
677
|
-
const auth = await ctx.
|
|
685
|
+
const auth = await ctx.read("auth");
|
|
678
686
|
const res = await fetch(`/api/users?ids=${keys.join(",")}`, {
|
|
679
687
|
headers: { Authorization: auth.token },
|
|
680
688
|
signal,
|
|
@@ -685,13 +693,13 @@ const userFamily = atomFamily({
|
|
|
685
693
|
},
|
|
686
694
|
});
|
|
687
695
|
|
|
688
|
-
// Each call returns an atom; concurrent
|
|
696
|
+
// Each call returns an atom; concurrent reads within the scheduler
|
|
689
697
|
// window are batched into a single fetch.
|
|
690
698
|
const alice = useAtomValue(userFamily(1));
|
|
691
699
|
const bob = useAtomValue(userFamily(2));
|
|
692
700
|
```
|
|
693
701
|
|
|
694
|
-
**How it works:** when multiple atoms are
|
|
702
|
+
**How it works:** when multiple atoms are read concurrently (e.g. several components mount at once), each atom's `read` enqueues its key into the batch coordinator. The scheduler decides when to flush the queue: the default `microtaskScheduler` flushes at the end of the current microtask; `windowScheduler(ms)` waits up to `ms` milliseconds.
|
|
695
703
|
|
|
696
704
|
`batch.fetch` receives:
|
|
697
705
|
|
|
@@ -762,9 +770,9 @@ type InfiniteData<Page, Cursor> = {
|
|
|
762
770
|
};
|
|
763
771
|
```
|
|
764
772
|
|
|
765
|
-
Call `store.
|
|
773
|
+
Call `store.apply(projectsAtom, "loadNext")` (or `useApplyAtom(projectsAtom)("loadNext")`) to fetch and append the next page. If `hasNextPage` is `false`, the call is a no-op.
|
|
766
774
|
|
|
767
|
-
When the atom is invalidated, `
|
|
775
|
+
When the atom is invalidated, `read` re-fetches every cached page **sequentially** by replaying the stored `pageCursors` — the same stale-while-revalidate flow as regular atoms.
|
|
768
776
|
|
|
769
777
|
Use `maxPages` to cap the number of pages held in memory and re-fetched on invalidation:
|
|
770
778
|
|
|
@@ -799,8 +807,8 @@ const userPostsFamily = infinityAtomFamily({
|
|
|
799
807
|
});
|
|
800
808
|
|
|
801
809
|
const aliceFeed = useAtomValue(userPostsFamily(1));
|
|
802
|
-
const
|
|
803
|
-
await
|
|
810
|
+
const applyAliceFeed = useApplyAtom(userPostsFamily(1));
|
|
811
|
+
await applyAliceFeed("loadNext");
|
|
804
812
|
|
|
805
813
|
userPostsFamily.invalidate(1);
|
|
806
814
|
userPostsFamily.invalidateAll();
|
|
@@ -814,7 +822,7 @@ All hooks must be used inside a `<Provider>`.
|
|
|
814
822
|
|
|
815
823
|
### `useStore`
|
|
816
824
|
|
|
817
|
-
Returns the `Store` instance from context — for advanced cases (e.g. calling `store.
|
|
825
|
+
Returns the `Store` instance from context — for advanced cases (e.g. calling `store.read` in async setup patterns) or when you need the store outside atom helpers.
|
|
818
826
|
|
|
819
827
|
### `useAtomValue`
|
|
820
828
|
|
|
@@ -828,13 +836,13 @@ const user = useAtomValue(userAtom);
|
|
|
828
836
|
const { value, isStale, error } = useAtomValue(userAtom, { observe: true });
|
|
829
837
|
```
|
|
830
838
|
|
|
831
|
-
### `
|
|
839
|
+
### `useApplyAtom`
|
|
832
840
|
|
|
833
|
-
Returns
|
|
841
|
+
Returns a stable function that calls `store.apply` for a writable atom, without subscribing to the value.
|
|
834
842
|
|
|
835
843
|
```tsx
|
|
836
|
-
const
|
|
837
|
-
await
|
|
844
|
+
const applyCount = useApplyAtom(countAtom);
|
|
845
|
+
await applyCount(42);
|
|
838
846
|
```
|
|
839
847
|
|
|
840
848
|
### `useMutation`
|
|
@@ -848,10 +856,10 @@ await runReorder(itemId, newIndex);
|
|
|
848
856
|
|
|
849
857
|
### `useAtom`
|
|
850
858
|
|
|
851
|
-
Combines `useAtomValue` and `
|
|
859
|
+
Combines `useAtomValue` and `useApplyAtom` into a `[value, apply]` tuple.
|
|
852
860
|
|
|
853
861
|
```tsx
|
|
854
|
-
const [count,
|
|
862
|
+
const [count, applyCount] = useAtom(countAtom);
|
|
855
863
|
```
|
|
856
864
|
|
|
857
865
|
### `useAtomContext`
|
|
@@ -860,7 +868,7 @@ Imperative access to the `StoreClient` inside a callback. Does not subscribe.
|
|
|
860
868
|
|
|
861
869
|
```tsx
|
|
862
870
|
const readBalance = useAtomContext(async (client) => {
|
|
863
|
-
return client.
|
|
871
|
+
return client.read(balanceAtom);
|
|
864
872
|
});
|
|
865
873
|
|
|
866
874
|
// Call imperatively, e.g. in an event handler
|
|
@@ -869,21 +877,21 @@ const balance = await readBalance();
|
|
|
869
877
|
|
|
870
878
|
## Preact (`@kdeveloper/kvark/preact`)
|
|
871
879
|
|
|
872
|
-
Same hook names, signatures, and behaviour as the React integration: `Provider`, `useStore`, `useAtomValue`, `
|
|
880
|
+
Same hook names, signatures, and behaviour as the React integration: `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext`. All hooks must be used inside a `<Provider>`.
|
|
873
881
|
|
|
874
882
|
Internally the entry imports only from `preact` and `preact/hooks` — there is **no dependency on `preact/compat`**, so your app does not need any React compatibility aliases.
|
|
875
883
|
|
|
876
884
|
## Vue 3 (`@kdeveloper/kvark/vue`)
|
|
877
885
|
|
|
878
|
-
Same composable names and behaviour as React: `Provider`, `useStore`, `useAtomValue`, `
|
|
886
|
+
Same composable names and behaviour as React: `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext`. Wrap your app (or subtree) with `Provider` and pass `:store="store"`.
|
|
879
887
|
|
|
880
888
|
Exported types: **`ThenableShallowRef<V>`** (default `useAtomValue`), **`ThenableObservedShallowRef<V>`** and **`ObservedValue<V>`** (with `{ observe: true }`). They encode pending vs resolved ref shapes for TypeScript.
|
|
881
889
|
|
|
882
|
-
`useAtomValue` / `useAtom` expose values as **awaitable shallow refs** (`PromiseLike`) — in script code use `.value`; in templates Vue unwraps refs for you. `await useAtomValue(atom)` in an async setup suspends until the atom's first `
|
|
890
|
+
`useAtomValue` / `useAtom` expose values as **awaitable shallow refs** (`PromiseLike`) — in script code use `.value`; in templates Vue unwraps refs for you. `await useAtomValue(atom)` in an async setup suspends until the atom's first `read` resolves, integrating with `<Suspense>`.
|
|
883
891
|
|
|
884
892
|
### Suspense (Vue 3)
|
|
885
893
|
|
|
886
|
-
Vue’s `<Suspense>` boundary applies to **async** components (e.g. **`async setup`** or [top-level `await` in `<script setup>`](https://vuejs.org/guide/built-ins/suspense.html#async-setup)), not to composables alone. Because `useAtomValue` and `useAtom` return `PromiseLike` refs, you can simply `await` them to suspend until the first `
|
|
894
|
+
Vue’s `<Suspense>` boundary applies to **async** components (e.g. **`async setup`** or [top-level `await` in `<script setup>`](https://vuejs.org/guide/built-ins/suspense.html#async-setup)), not to composables alone. Because `useAtomValue` and `useAtom` return `PromiseLike` refs, you can simply `await` them to suspend until the first `read` resolves.
|
|
887
895
|
|
|
888
896
|
`atoms.ts`
|
|
889
897
|
|
|
@@ -891,7 +899,7 @@ Vue’s `<Suspense>` boundary applies to **async** components (e.g. **`async set
|
|
|
891
899
|
import { atom } from "@kdeveloper/kvark";
|
|
892
900
|
|
|
893
901
|
export const slowAtom = atom({
|
|
894
|
-
|
|
902
|
+
read: async () => {
|
|
895
903
|
await new Promise((r) => setTimeout(r, 50));
|
|
896
904
|
return "ready";
|
|
897
905
|
},
|
|
@@ -939,7 +947,7 @@ const store = createStore();
|
|
|
939
947
|
</template>
|
|
940
948
|
```
|
|
941
949
|
|
|
942
|
-
Alternatively, use **`defineComponent({ async setup() { ... } })`** and `await useAtomValue(slowAtom)` before returning the render function — the same pattern as in `test/vue/hooks.test.ts`. `useAtom` is also awaitable: `const [ref,
|
|
950
|
+
Alternatively, use **`defineComponent({ async setup() { ... } })`** and `await useAtomValue(slowAtom)` before returning the render function — the same pattern as in `test/vue/hooks.test.ts`. `useAtom` is also awaitable: `const [ref, apply] = await useAtom(writableAtom)`.
|
|
943
951
|
|
|
944
952
|
## External Invalidation
|
|
945
953
|
|
|
@@ -980,9 +988,23 @@ const unsub = client.subscribe(userAtom, (state) => {
|
|
|
980
988
|
});
|
|
981
989
|
```
|
|
982
990
|
|
|
991
|
+
`client.observe(atom, listener)` is an explicit alias for state-bearing external subscriptions. `client.subscribe` remains available for compatibility and has the same state-bearing callback shape on `StoreClient`.
|
|
992
|
+
|
|
993
|
+
### Cache-first reads
|
|
994
|
+
|
|
995
|
+
Use `read(atom)` when you want Kvark's normal async read semantics. For imperative code that prefers cached data when available, use:
|
|
996
|
+
|
|
997
|
+
```ts
|
|
998
|
+
const state = client.get(userAtom); // AtomState<User>, sync
|
|
999
|
+
const cached = client.peek(userAtom); // User | undefined, sync
|
|
1000
|
+
const user = await client.ensure(userAtom); // cached fresh/stale value, otherwise read()
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
`get(atom)` mirrors `getSnapshot(atom)`. `peek(atom)` returns the current cached value from `fresh`, `stale`, or retained `error` states without starting a read. `ensure(atom)` resolves immediately for cached `fresh`/`stale` values, or for an `error` state that retained a non-`undefined` value; otherwise it delegates to `read(atom)`.
|
|
1004
|
+
|
|
983
1005
|
### Direct Write
|
|
984
1006
|
|
|
985
|
-
When the server pushes a complete, authoritative value (e.g. via WebSocket or SSE), use `client.write` to store it directly — no refetch through `
|
|
1007
|
+
When the server pushes a complete, authoritative value (e.g. via WebSocket or SSE), use `store.write` / `client.write` to store it directly — no refetch through `read` is triggered, and the atom's `write` callback is not invoked. Any in-flight `read` for the atom is aborted, and derived atoms that depend on it are marked `stale` so they re-compute.
|
|
986
1008
|
|
|
987
1009
|
```ts
|
|
988
1010
|
ws.addEventListener("message", (event) => {
|
|
@@ -1001,24 +1023,45 @@ client.write(counterAtom, (prev) => (prev ?? 0) + 1);
|
|
|
1001
1023
|
|
|
1002
1024
|
> If `V` itself is a function type, pass a mutator that returns it: `write(atom, () => myFn)`.
|
|
1003
1025
|
|
|
1004
|
-
`write` works on any `Atom<V>` — the atom does not need a `
|
|
1026
|
+
`write` works on any `Atom<V>` — the atom does not need a `write` config. This makes it the right tool when you already have the final value and want to skip a redundant network round-trip. Use `apply` when you want to run the atom's async `write` callback instead.
|
|
1027
|
+
|
|
1028
|
+
`set(atom, value | mutator)` is an alias for `write(atom, ...)`; `run(atom, ...args)` is an alias for `apply(atom, ...args)`; `runMutation(m, ...args)` is an alias for `mutate(m, ...args)`.
|
|
1029
|
+
|
|
1030
|
+
### Inspect and dispose
|
|
1031
|
+
|
|
1032
|
+
For devtools and diagnostics, `inspect(atom)` returns lightweight metadata: label, current status, dependency counts, listener count, mount count, and whether a read promise is in flight.
|
|
1033
|
+
|
|
1034
|
+
Call `dispose()` when a store is scoped to a request, test, or worker lifetime. It aborts in-flight reads, calls active `onMount` cleanups, clears listeners, and unregisters family invalidation hooks. After disposal, store methods throw `Store has been disposed`; calling `dispose()` again is a no-op.
|
|
1005
1035
|
|
|
1006
1036
|
### `StoreClient` interface
|
|
1007
1037
|
|
|
1008
|
-
The concrete `Store` class exposes the same surface as `getClient()` — e.g. `store.mutate(m, ...args)`
|
|
1038
|
+
The concrete `Store` class exposes the same surface as `getClient()` — e.g. `store.read(atom)` / `client.read(atom)`, `store.apply(atom, ...args)` / `client.apply(atom, ...args)`, and `store.mutate(m, ...args)` / `client.mutate(m, ...args)`.
|
|
1009
1039
|
|
|
1010
1040
|
```ts
|
|
1011
|
-
import type { Atom, WritableAtom, AtomState, Mutation } from "@kdeveloper/kvark";
|
|
1041
|
+
import type { Atom, WritableAtom, AtomState, Mutation, StoreInspection } from "@kdeveloper/kvark";
|
|
1012
1042
|
|
|
1013
1043
|
interface StoreClient {
|
|
1014
|
-
get<V>(atom: Atom<V>):
|
|
1015
|
-
|
|
1044
|
+
get<V>(atom: Atom<V>): AtomState<V>;
|
|
1045
|
+
peek<V>(atom: Atom<V>): V | undefined;
|
|
1046
|
+
ensure<V>(atom: Atom<V>): Promise<V>;
|
|
1047
|
+
read<V>(atom: Atom<V>): Promise<V>;
|
|
1048
|
+
run<V, A extends readonly unknown[]>(atom: WritableAtom<V, A>, ...args: A): Promise<void>;
|
|
1049
|
+
apply<V, A extends readonly unknown[]>(atom: WritableAtom<V, A>, ...args: A): Promise<void>;
|
|
1050
|
+
runMutation<Args extends readonly unknown[]>(
|
|
1051
|
+
mutation: Mutation<Args>,
|
|
1052
|
+
...args: Args
|
|
1053
|
+
): Promise<void>;
|
|
1016
1054
|
mutate<Args extends readonly unknown[]>(mutation: Mutation<Args>, ...args: Args): Promise<void>;
|
|
1055
|
+
set<V>(atom: Atom<V>, value: V): void;
|
|
1056
|
+
set<V>(atom: Atom<V>, mutate: (prev: V | undefined) => V): void;
|
|
1017
1057
|
write<V>(atom: Atom<V>, value: V): void;
|
|
1018
1058
|
write<V>(atom: Atom<V>, mutate: (prev: V | undefined) => V): void;
|
|
1019
1059
|
invalidate(atom: Atom<unknown>): void;
|
|
1020
1060
|
invalidateMany(atoms: ReadonlyArray<Atom<unknown>>): void;
|
|
1061
|
+
observe<V>(atom: Atom<V>, listener: (state: AtomState<V>) => void): () => void;
|
|
1021
1062
|
subscribe<V>(atom: Atom<V>, listener: (state: AtomState<V>) => void): () => void;
|
|
1063
|
+
inspect(atom: Atom<unknown>): StoreInspection;
|
|
1064
|
+
dispose(): void;
|
|
1022
1065
|
}
|
|
1023
1066
|
```
|
|
1024
1067
|
|
|
@@ -1071,16 +1114,16 @@ type PostArgs = AtomArgs<typeof postAtom>; // → [postId: number]
|
|
|
1071
1114
|
type Writable = IsWritable<typeof countAtom>; // → true | false
|
|
1072
1115
|
```
|
|
1073
1116
|
|
|
1074
|
-
`WritableAtomContext` extends `MutationContext` — the same `writeOptimistic` / `invalidate` / `invalidateMany` helpers appear on both standalone mutations and writable `
|
|
1117
|
+
`WritableAtomContext` extends `MutationContext` — the same `writeOptimistic` / `invalidate` / `invalidateMany` helpers appear on both standalone mutations and writable `write` callbacks.
|
|
1075
1118
|
|
|
1076
1119
|
## Package Structure
|
|
1077
1120
|
|
|
1078
1121
|
| Import | Contents |
|
|
1079
1122
|
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1080
|
-
| `@kdeveloper/kvark` | `atom`, `infinityAtom`, `infinityAtomFamily`, `mutation`, `createStore`, all types (including `Mutation`, `MutationContext`)
|
|
1081
|
-
| `@kdeveloper/kvark/react` | `Provider`, `useStore`, `useAtomValue`, `
|
|
1082
|
-
| `@kdeveloper/kvark/preact` | `Provider`, `useStore`, `useAtomValue`, `
|
|
1083
|
-
| `@kdeveloper/kvark/vue` | `Provider`, `useStore`, `useAtomValue`, `
|
|
1123
|
+
| `@kdeveloper/kvark` | `atom`, `infinityAtom`, `infinityAtomFamily`, `mutation`, `createStore`, all types (including `Store`, `StoreClient`, `StoreInspection`, `Mutation`, `MutationContext`) |
|
|
1124
|
+
| `@kdeveloper/kvark/react` | `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext` |
|
|
1125
|
+
| `@kdeveloper/kvark/preact` | `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext` |
|
|
1126
|
+
| `@kdeveloper/kvark/vue` | `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext`; types `ThenableShallowRef`, `ThenableObservedShallowRef`, `ObservedValue` |
|
|
1084
1127
|
| `@kdeveloper/kvark/family` | `atomFamily`, `infinityAtomFamily`, `stableFamilyKey`, `microtaskScheduler`, `windowScheduler`, `maxBatchSizeScheduler`, `windowedFiniteBatchScheduler`, re-exports `atom`, `infinityAtom`, `mutation`, `createStore`; types include `AtomFamily`, `InfinityAtomFamily`, `BatchScheduler`, `BatchFetchInput`, `Mutation`, `MutationContext`, and core atom types |
|
|
1085
1128
|
|
|
1086
1129
|
The core (`@kdeveloper/kvark`) has **zero runtime dependencies**. **React**, **Preact**, and **Vue** are optional peer dependencies — install the framework you use and import from `/react`, `/preact`, or `/vue`.
|