@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 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({ get, set?, dependencies? })` |
15
- | Async model | Optional | `get`, `set`, `ctx.get` — always `async` |
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 | `store.set()` | Explicit `store.invalidate()` / `StoreClient` |
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, useAtomValue, useSetAtom } from "@kdeveloper/kvark/react";
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
- get: async () => 1,
39
- set: async (_ctx, _id: number) => {},
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
- get: async (ctx) => {
46
- const id = await ctx.get("userId");
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, useAtomValue, useSetAtom } from "@kdeveloper/kvark/preact";
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 `get` 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 `get` resolves — see [Suspense (Vue 3)](#suspense-vue-3).
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
- get: async () => 1,
97
- set: async () => {},
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
- get: async (ctx) => {
104
- const id = await ctx.get("userId");
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 `get`, `set`, `onMount`, `mutation`, `batch.fetch`, and `infinityAtom.queryFn`.
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
- get: async (ctx) => {
190
- return ctx.global.apiClient.users.get(await ctx.get("userId"));
189
+ read: async (ctx) => {
190
+ return ctx.global.apiClient.users.get(await ctx.read("userId"));
191
191
  },
192
- set: async (ctx, patch: Partial<User>) => {
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 `get` function is always `async`, and dependencies must be declared explicitly.
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
- get: async () => 0,
236
- set: async (_ctx, _value: number) => {},
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
- get: async (ctx) => {
244
- const n = await ctx.get("count");
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 setter accepts either the next value or an updater function `(prev) => next`. Both `store.set(atom, value)` and `store.write(atom, value)` are supported, and React/Preact/Vue hooks (`useAtomValue`, `useSetAtom`, `useAtom`) work as usual.
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.set(counterAtom, 5);
272
- await store.set(counterAtom, (prev) => prev + 1);
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({ get, set?, ... })` form.
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 `get` method (and would be parsed as an atom config), use the explicit form: `atom({ get: async () => obj, set: async (ctx, next) => ctx.setOptimisticValue(next) })`.
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 `set` function. The return type becomes `WritableAtom<Value, Args>`, where `Args` is the tuple of arguments the setter accepts.
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
- get: async () => {
287
+ read: async () => {
288
288
  const res = await fetch("/api/profile");
289
289
  return res.json() as Promise<Profile>;
290
290
  },
291
- set: async (ctx, patch: Partial<Profile>) => {
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 `set`:** the store calls `config.set(ctx, ...args)`, waits for the returned promise, then calls `invalidate` on the atom. This marks it `stale` and triggers a background refetch via `get` — the same stale-while-revalidate flow described [above](#stale-while-revalidate).
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 `set` provides:
303
+ The `ctx` passed to `write` provides:
304
304
 
305
- - **`ctx.get(key)`** — read any declared dependency (same as in `get`).
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 `set` 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.
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
- get: async () => {
318
+ read: async () => {
319
319
  const res = await fetch("/api/todo");
320
320
  return res.json() as Promise<Todo>;
321
321
  },
322
- set: async (ctx, title: string) => {
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
- set: async (ctx) => {
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 `get` had loaded before the optimistic update — no manual rollback needed.
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 `set` also provides methods for cross-atom side-effects:
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
- get: async () => {
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
- get: (id: number) => async () => {
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
- set: (id: number) => async (ctx, patch: Partial<Todo>) => {
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 `set`, but **without** `setOptimisticValue` or `ctx.signal`). Standalone mutations can read any atom with **`ctx.get(atom)`**. In contrast, inside **`set`**, **`ctx.get(...)`** still means "read a declared dependency by key":
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.get(atom)`** — resolve any atom with the same semantics as `store.get(atom)` / `client.get(atom)`.
393
- - **`ctx.writeOptimistic(atom, value | mutator)`** — same semantics as inside `set`. On throw, every atom touched by `writeOptimistic` rolls back to its state before the first optimistic write in this run.
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 **`set`**, a successful **`mutate` does not invalidate any atom automatically**. Call `ctx.invalidate` / `ctx.invalidateMany` when you want stale-while-revalidate after the request completes.
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
- get: async () => {
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
- get: async () => {
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
- | | `set` | `onMount` |
456
- | ---------------- | ----------------------------------------- | -------------------------------------- |
457
- | **Triggered by** | Explicit call (`store.set`, `useSetAtom`) | First subscriber mounts |
458
- | **After update** | `invalidate` → refetch via `get` | No refetch — value stays as-is |
459
- | **Use case** | Mutations, API calls, optimistic updates | Timers, subscriptions, imperative push |
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 `get`.
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
- get: async () => new Date().toISOString(),
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 resolve them in parallel before calling `get`. Inside `get` you control the parallelism explicitly.
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
- get: async (ctx) => {
488
- const [user, settings] = await Promise.all([ctx.get("user"), ctx.get("settings")]);
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 `get` immediately sets the atom to `error` state. You can opt into automatic retries with the `retry` and `retryDelay` options:
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
- get: async (ctx) => {
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. Atoms are cached by param; supports LRU eviction.
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
- get: (postId) => async (ctx) => {
567
- const user = await ctx.get("user");
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.get`. Each unique param resolves 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.
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
- get: (id: number) => async () => fetch(`/api/users/${id}`).then((r) => r.json()),
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
- get: async (ctx) => {
601
- const user = await ctx.get("user", 7); // resolves userFamily(7)
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.get` is keyed on the dependency entry:
618
+ The signature of `ctx.read` is keyed on the dependency entry:
611
619
 
612
- | Entry | Call form | Compile-time errors |
613
- | ------------ | --------------------- | ---------------------------------------------------- |
614
- | `Atom<V>` | `ctx.get(key)` | passing a second arg → error |
615
- | `atomFamily` | `ctx.get(key, param)` | omitting `param` → error; wrong `param` type → error |
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
- get: async (ctx) => {
621
- await ctx.get("base"); // ok
622
- await ctx.get("user", 1); // ok
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.get("user");
633
+ ctx.read("user");
626
634
  // @ts-expect-error plain atom-key does not accept a parameter
627
- ctx.get("base", 1);
635
+ ctx.read("base", 1);
628
636
  // @ts-expect-error param type must match the family's `Param`
629
- ctx.get("user", "not-a-number");
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 resolved family member; reads with different params resolve to distinct atoms (and run independently). Family-typed dependencies are **not** pre-resolved before `get` runs (the param is only known at the `ctx.get` call site), so unread members never trigger a fetch.
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
- get: (filters) => async () => fetchResults(filters),
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 (`get`, `set`, `invalidate`, `remove`, LRU). The original `param` is still passed to `get`, `set`, and `dependencies`.
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 `get` with `batch`:
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.get("auth");
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 resolves within the scheduler
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 resolved concurrently (e.g. several components mount at once), each atom's `get` 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.
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.set(projectsAtom, "loadNext")` (or `useSetAtom(projectsAtom)("loadNext")`) to fetch and append the next page. If `hasNextPage` is `false`, the call is a no-op.
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, `get` re-fetches every cached page **sequentially** by replaying the stored `pageCursors` — the same stale-while-revalidate flow as regular atoms.
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 setAliceFeed = useSetAtom(userPostsFamily(1));
803
- await setAliceFeed("loadNext");
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.resolve` in async setup patterns) or when you need the store outside atom helpers.
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
- ### `useSetAtom`
839
+ ### `useApplyAtom`
832
840
 
833
- Returns the setter for a writable atom, without subscribing to the value.
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 setCount = useSetAtom(countAtom);
837
- await setCount(42);
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 `useSetAtom` into a `[value, setter]` tuple.
859
+ Combines `useAtomValue` and `useApplyAtom` into a `[value, apply]` tuple.
852
860
 
853
861
  ```tsx
854
- const [count, setCount] = useAtom(countAtom);
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.get(balanceAtom);
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`, `useSetAtom`, `useMutation`, `useAtom`, `useAtomContext`. All hooks must be used inside a `<Provider>`.
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`, `useSetAtom`, `useMutation`, `useAtom`, `useAtomContext`. Wrap your app (or subtree) with `Provider` and pass `:store="store"`.
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 `get` resolves, integrating with `<Suspense>`.
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 `get` resolves.
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
- get: async () => {
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, set] = await useAtom(writableAtom)`.
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 `get` is triggered. Any in-flight `get` for the atom is aborted, and derived atoms that depend on it are marked `stale` so they re-compute.
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 `set` config. This makes it the right tool when you already have the final value and want to skip a redundant network round-trip.
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)` and `client.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>): Promise<V>;
1015
- set<V, A extends readonly unknown[]>(atom: WritableAtom<V, A>, ...args: A): Promise<void>;
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 `set` callbacks.
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`, `useSetAtom`, `useMutation`, `useAtom`, `useAtomContext` |
1082
- | `@kdeveloper/kvark/preact` | `Provider`, `useStore`, `useAtomValue`, `useSetAtom`, `useMutation`, `useAtom`, `useAtomContext` |
1083
- | `@kdeveloper/kvark/vue` | `Provider`, `useStore`, `useAtomValue`, `useSetAtom`, `useMutation`, `useAtom`, `useAtomContext`; types `ThenableShallowRef`, `ThenableObservedShallowRef`, `ObservedValue` |
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`.