@kdeveloper/kvark 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @kdeveloper/kvark
|
|
2
2
|
|
|
3
|
-
Atomic state management for React with **explicit dependency graphs**, stale-while-revalidate, and first-class external invalidation.
|
|
3
|
+
Atomic state management for **React** and **Vue 3** with **explicit dependency graphs**, stale-while-revalidate, and first-class external invalidation.
|
|
4
4
|
|
|
5
|
-
Inspired by Jotai, but built around a key difference: dependencies are declared upfront rather than inferred at runtime. This makes the data graph statically analysable, enables parallel loading, and allows invalidation from anywhere — WebSocket handlers, SSE streams, timers, Service Workers — without
|
|
5
|
+
Inspired by Jotai, but built around a key difference: dependencies are declared upfront rather than inferred at runtime. This makes the data graph statically analysable, enables parallel loading, and allows invalidation from anywhere — WebSocket handlers, SSE streams, timers, Service Workers — without framework hooks.
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
#
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @kdeveloper/kvark
|
|
9
|
+
# optional peers: react >=18 and/or vue >=3.4 (install the one you use)
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
## Why Kvark?
|
|
@@ -26,7 +26,7 @@ npm install @kdeveloper/kvark
|
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
pnpm add @kdeveloper/kvark
|
|
29
|
-
# react >=18
|
|
29
|
+
# optional peers: react >=18 and/or vue >=3.4
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Quick Start
|
|
@@ -73,6 +73,69 @@ function UserCard() {
|
|
|
73
73
|
}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### Vue 3
|
|
77
|
+
|
|
78
|
+
The same atoms and store work with `@kdeveloper/kvark/vue`. Composables mirror the React hooks; `useAtomValue` returns a [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref) to the value — use `.value` in `<script setup>` (templates unwrap refs automatically).
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
|
|
82
|
+
`atoms.ts` — lift the atom definitions from the Quick Start into a shared module:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { atom } from "@kdeveloper/kvark";
|
|
86
|
+
|
|
87
|
+
export const userIdAtom = atom({
|
|
88
|
+
debugLabel: "userId",
|
|
89
|
+
get: async () => 1,
|
|
90
|
+
set: async () => {},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const userAtom = atom({
|
|
94
|
+
debugLabel: "user",
|
|
95
|
+
dependencies: { userId: userIdAtom },
|
|
96
|
+
get: async (ctx) => {
|
|
97
|
+
const id = await ctx.get("userId");
|
|
98
|
+
const res = await fetch(`/api/users/${id}`, { signal: ctx.signal });
|
|
99
|
+
return res.json();
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`UserCard.vue`
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<script setup lang="ts">
|
|
108
|
+
import { useAtomValue } from "@kdeveloper/kvark/vue";
|
|
109
|
+
import { userAtom } from "./atoms";
|
|
110
|
+
|
|
111
|
+
const user = useAtomValue(userAtom);
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<template>
|
|
115
|
+
<div>{{ user.name }}</div>
|
|
116
|
+
</template>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`App.vue`
|
|
120
|
+
|
|
121
|
+
```vue
|
|
122
|
+
<script setup lang="ts">
|
|
123
|
+
import { createStore } from "@kdeveloper/kvark";
|
|
124
|
+
import { Provider } from "@kdeveloper/kvark/vue";
|
|
125
|
+
import UserCard from "./UserCard.vue";
|
|
126
|
+
|
|
127
|
+
const store = createStore();
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<template>
|
|
131
|
+
<Provider :store="store">
|
|
132
|
+
<UserCard />
|
|
133
|
+
</Provider>
|
|
134
|
+
</template>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Unlike React’s `Suspense` around `useAtomValue`, Vue’s `<Suspense>` only activates for **async** child components. To show a fallback until the first `get` completes, use **`async setup`**, `useStore()`, `useAtomValue(atom)`, then **`await store.resolve(atom)`**, and wrap that child in `<Suspense>` — see [Suspense (Vue 3)](#suspense-vue-3).
|
|
138
|
+
|
|
76
139
|
## Core Concepts
|
|
77
140
|
|
|
78
141
|
### Atoms
|
|
@@ -174,7 +237,7 @@ Both can update an atom's cached value, but they serve different purposes:
|
|
|
174
237
|
|
|
175
238
|
### `onMount`
|
|
176
239
|
|
|
177
|
-
Optional lifecycle hook that runs when the atom **first gains a subscriber** in a store (for example when a
|
|
240
|
+
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`.
|
|
178
241
|
|
|
179
242
|
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.
|
|
180
243
|
|
|
@@ -300,10 +363,14 @@ A built-in helper that serialises plain objects and arrays into a deterministic
|
|
|
300
363
|
- Not supported: class instances (other than plain `Object`/`Array`), `Map`, `Set`, `Symbol`, functions, circular references.
|
|
301
364
|
- Memoises by object reference via a `WeakMap` — repeated calls with the **same object reference** are O(1). Treat param objects as **immutable** after the first call; mutating an object after it has been memoised will silently return the stale key.
|
|
302
365
|
|
|
303
|
-
## React
|
|
366
|
+
## React (`@kdeveloper/kvark/react`)
|
|
304
367
|
|
|
305
368
|
All hooks must be used inside a `<Provider>`.
|
|
306
369
|
|
|
370
|
+
### `useStore`
|
|
371
|
+
|
|
372
|
+
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.
|
|
373
|
+
|
|
307
374
|
### `useAtomValue`
|
|
308
375
|
|
|
309
376
|
Reads an atom value and subscribes to updates. Suspends on first load.
|
|
@@ -346,9 +413,77 @@ const readBalance = useAtomContext(async (client) => {
|
|
|
346
413
|
const balance = await readBalance();
|
|
347
414
|
```
|
|
348
415
|
|
|
416
|
+
## Vue 3 (`@kdeveloper/kvark/vue`)
|
|
417
|
+
|
|
418
|
+
Same composable names and behaviour as React: `Provider`, `useStore`, `useAtomValue`, `useSetAtom`, `useAtom`, `useAtomContext`. Wrap your app (or subtree) with `Provider` and pass `:store="store"`.
|
|
419
|
+
|
|
420
|
+
`useAtomValue` / `useAtom` expose values as **shallow refs** — in script code use `.value`; in templates Vue unwraps refs for you.
|
|
421
|
+
|
|
422
|
+
### Suspense (Vue 3)
|
|
423
|
+
|
|
424
|
+
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. To get a loading fallback until the first `get` finishes, the child that uses `useAtomValue` should **`await store.resolve(atom)`** after subscribing, and be wrapped in `<Suspense>`.
|
|
425
|
+
|
|
426
|
+
`atoms.ts`
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { atom } from "@kdeveloper/kvark";
|
|
430
|
+
|
|
431
|
+
export const slowAtom = atom({
|
|
432
|
+
get: async () => {
|
|
433
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
434
|
+
return "ready";
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`AsyncRead.vue` (under `Provider`; top-level `await` makes this component async for Suspense)
|
|
440
|
+
|
|
441
|
+
```vue
|
|
442
|
+
<script setup lang="ts">
|
|
443
|
+
import { useAtomValue, useStore } from "@kdeveloper/kvark/vue";
|
|
444
|
+
import { slowAtom } from "./atoms";
|
|
445
|
+
|
|
446
|
+
const store = useStore();
|
|
447
|
+
const value = useAtomValue(slowAtom);
|
|
448
|
+
await store.resolve(slowAtom);
|
|
449
|
+
</script>
|
|
450
|
+
|
|
451
|
+
<template>
|
|
452
|
+
<div>{{ value }}</div>
|
|
453
|
+
</template>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`App.vue`
|
|
457
|
+
|
|
458
|
+
```vue
|
|
459
|
+
<script setup lang="ts">
|
|
460
|
+
import { Suspense } from "vue";
|
|
461
|
+
import { createStore } from "@kdeveloper/kvark";
|
|
462
|
+
import { Provider } from "@kdeveloper/kvark/vue";
|
|
463
|
+
import AsyncRead from "./AsyncRead.vue";
|
|
464
|
+
|
|
465
|
+
const store = createStore();
|
|
466
|
+
</script>
|
|
467
|
+
|
|
468
|
+
<template>
|
|
469
|
+
<Provider :store="store">
|
|
470
|
+
<Suspense>
|
|
471
|
+
<template #default>
|
|
472
|
+
<AsyncRead />
|
|
473
|
+
</template>
|
|
474
|
+
<template #fallback>
|
|
475
|
+
<div>Loading…</div>
|
|
476
|
+
</template>
|
|
477
|
+
</Suspense>
|
|
478
|
+
</Provider>
|
|
479
|
+
</template>
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Alternatively, use **`defineComponent({ async setup() { ... } })`** and `await store.resolve(slowAtom)` before returning the render function — the same pattern as in `test/vue/hooks.test.ts`.
|
|
483
|
+
|
|
349
484
|
## External Invalidation
|
|
350
485
|
|
|
351
|
-
`StoreClient` exposes the store's full capabilities outside
|
|
486
|
+
`StoreClient` exposes the store's full capabilities outside your component tree — useful for WebSocket handlers, SSE streams, polling timers, and Service Workers.
|
|
352
487
|
|
|
353
488
|
```ts
|
|
354
489
|
import { createStore } from "@kdeveloper/kvark";
|
|
@@ -377,7 +512,7 @@ sse.addEventListener("prices.updated", () => {
|
|
|
377
512
|
// Polling
|
|
378
513
|
setInterval(() => client.invalidate(statusAtom), 30_000);
|
|
379
514
|
|
|
380
|
-
// Subscribe to state changes outside
|
|
515
|
+
// Subscribe to state changes outside components
|
|
381
516
|
const unsub = client.subscribe(userAtom, (state) => {
|
|
382
517
|
if (state.status === "fresh") {
|
|
383
518
|
analytics.identify(state.value.id);
|
|
@@ -424,6 +559,8 @@ interface StoreClient {
|
|
|
424
559
|
|
|
425
560
|
## Provider and Store Setup
|
|
426
561
|
|
|
562
|
+
**React**
|
|
563
|
+
|
|
427
564
|
```tsx
|
|
428
565
|
// store.ts
|
|
429
566
|
import { createStore } from "@kdeveloper/kvark";
|
|
@@ -442,6 +579,23 @@ export function App() {
|
|
|
442
579
|
}
|
|
443
580
|
```
|
|
444
581
|
|
|
582
|
+
**Vue 3**
|
|
583
|
+
|
|
584
|
+
```vue
|
|
585
|
+
<script setup lang="ts">
|
|
586
|
+
import { createStore } from "@kdeveloper/kvark";
|
|
587
|
+
import { Provider } from "@kdeveloper/kvark/vue";
|
|
588
|
+
|
|
589
|
+
const store = createStore();
|
|
590
|
+
</script>
|
|
591
|
+
|
|
592
|
+
<template>
|
|
593
|
+
<Provider :store="store">
|
|
594
|
+
<RouterView />
|
|
595
|
+
</Provider>
|
|
596
|
+
</template>
|
|
597
|
+
```
|
|
598
|
+
|
|
445
599
|
## Utility Types
|
|
446
600
|
|
|
447
601
|
```ts
|
|
@@ -454,19 +608,21 @@ type Writable = IsWritable<typeof countAtom>; // → true | false
|
|
|
454
608
|
|
|
455
609
|
## Package Structure
|
|
456
610
|
|
|
457
|
-
| Import | Contents
|
|
458
|
-
| -------------------------- |
|
|
459
|
-
| `@kdeveloper/kvark` | `atom`, `createStore`, all types
|
|
460
|
-
| `@kdeveloper/kvark/react` | `Provider`, `useAtomValue`, `useSetAtom`, `useAtom`, `useAtomContext`
|
|
461
|
-
| `@kdeveloper/kvark/
|
|
611
|
+
| Import | Contents |
|
|
612
|
+
| -------------------------- | ------------------------------------------------------------------------------------------- |
|
|
613
|
+
| `@kdeveloper/kvark` | `atom`, `createStore`, all types |
|
|
614
|
+
| `@kdeveloper/kvark/react` | `Provider`, `useStore`, `useAtomValue`, `useSetAtom`, `useAtom`, `useAtomContext` |
|
|
615
|
+
| `@kdeveloper/kvark/vue` | `Provider`, `useStore`, `useAtomValue`, `useSetAtom`, `useAtom`, `useAtomContext` |
|
|
616
|
+
| `@kdeveloper/kvark/family` | `atomFamily`, `stableFamilyKey`, re-exports `atom` / `createStore` and core types |
|
|
462
617
|
|
|
463
|
-
The core (`@kdeveloper/kvark`) has **zero runtime dependencies**. React
|
|
618
|
+
The core (`@kdeveloper/kvark`) has **zero runtime dependencies**. **React** and **Vue** are optional peer dependencies — install the framework you use and import from `/react` or `/vue`.
|
|
464
619
|
|
|
465
620
|
## Requirements
|
|
466
621
|
|
|
467
622
|
- **Node.js** ≥ 20
|
|
468
623
|
- **TypeScript** ≥ 6 (strict mode recommended)
|
|
469
|
-
- **React** ≥ 18 (for `@kdeveloper/kvark/react`)
|
|
624
|
+
- **React** ≥ 18 (optional peer, for `@kdeveloper/kvark/react`)
|
|
625
|
+
- **Vue** ≥ 3.4 (optional peer, for `@kdeveloper/kvark/vue`)
|
|
470
626
|
|
|
471
627
|
## License
|
|
472
628
|
|