@adbl/cells 0.0.21 → 0.0.23
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 +293 -131
- package/dist/library/classes.d.ts +339 -101
- package/dist/library/classes.js +700 -321
- package/dist/library/classes.js.map +1 -1
- package/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,240 +3,402 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/%40adbl%2Fcells)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
A lightweight, type-safe library for reactive state management. Cells simplifies complex state propagation with an intuitive API that handles synchronous updates, asynchronous data fetching, and race conditions automatically.
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
- [Features](#features)
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
|
+
- [Guide](#guide)
|
|
14
|
+
- [Core Concepts](#1-core-concepts)
|
|
15
|
+
- [Asynchronous State](#2-asynchronous-state)
|
|
16
|
+
- [Advanced Patterns](#3-advanced-patterns)
|
|
17
|
+
- [API Reference](#api-reference)
|
|
18
|
+
- [TypeScript Support](#typescript-support)
|
|
19
|
+
- [Contributing](#contributing)
|
|
20
|
+
- [License](#license)
|
|
7
21
|
|
|
8
22
|
## Features
|
|
9
23
|
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
24
|
+
- **Fine-grained Reactivity** - Updates only what changes, avoiding unnecessary re-renders
|
|
25
|
+
- **Async Primitives** - First-class support for async state with built-in loading and error tracking
|
|
26
|
+
- **Race Condition Handling** - Automatically cancels stale async requests via `AbortSignal`
|
|
27
|
+
- **Glitch-free** - Guarantees consistency across derived values with topological update ordering
|
|
28
|
+
- **Type-safe** - Full TypeScript support with inferred types
|
|
29
|
+
- **Zero Dependencies** - Keeps your bundle small (~3KB minified)
|
|
15
30
|
|
|
16
31
|
## Installation
|
|
17
32
|
|
|
18
|
-
Get started with Cells in your project:
|
|
19
|
-
|
|
20
33
|
```bash
|
|
21
34
|
npm install @adbl/cells
|
|
22
35
|
```
|
|
23
36
|
|
|
24
|
-
Or if you prefer Yarn:
|
|
25
|
-
|
|
26
37
|
```bash
|
|
27
38
|
yarn add @adbl/cells
|
|
28
39
|
```
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add @adbl/cells
|
|
43
|
+
```
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
## Quick Start
|
|
35
46
|
|
|
36
47
|
```javascript
|
|
37
48
|
import { Cell } from '@adbl/cells';
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
// 1. Create a source cell
|
|
51
|
+
const name = Cell.source('World');
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
// 2. Create a derived cell (updates automatically)
|
|
54
|
+
const greeting = Cell.derived(() => `Hello, ${name.get()}!`);
|
|
55
|
+
|
|
56
|
+
// 3. Listen for changes
|
|
57
|
+
greeting.listen((msg) => console.log(msg));
|
|
58
|
+
|
|
59
|
+
// 4. Update the source
|
|
60
|
+
name.set('Cells'); // Console: "Hello, Cells!"
|
|
44
61
|
```
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Guide
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
### 1. Core Concepts
|
|
68
|
+
|
|
69
|
+
#### Source Cells
|
|
70
|
+
|
|
71
|
+
The root of your state graph. You can read, subscribe to, and modify them.
|
|
49
72
|
|
|
50
73
|
```javascript
|
|
51
74
|
const count = Cell.source(0);
|
|
52
|
-
const doubledCount = Cell.derived(() => count.get() * 2);
|
|
53
|
-
|
|
54
|
-
console.log(doubledCount.get()); // Output: 0
|
|
55
75
|
|
|
56
|
-
count.set(
|
|
57
|
-
console.log(
|
|
76
|
+
count.set(1);
|
|
77
|
+
console.log(count.get()); // 1
|
|
58
78
|
```
|
|
59
79
|
|
|
60
|
-
|
|
80
|
+
#### Derived Cells
|
|
61
81
|
|
|
62
|
-
|
|
82
|
+
Computed values that update automatically when dependencies change. They are eager and always kept in sync.
|
|
63
83
|
|
|
64
84
|
```javascript
|
|
65
|
-
const count = Cell.source(
|
|
85
|
+
const count = Cell.source(1);
|
|
86
|
+
const double = Cell.derived(() => count.get() * 2);
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
console.log(`Count changed to: ${newValue}`);
|
|
69
|
-
});
|
|
88
|
+
console.log(double.get()); // 2
|
|
70
89
|
|
|
71
|
-
count.set(
|
|
72
|
-
|
|
90
|
+
count.set(5);
|
|
91
|
+
console.log(double.get()); // 10
|
|
73
92
|
```
|
|
74
93
|
|
|
75
|
-
|
|
94
|
+
#### Effects (`listen`)
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
Run side effects when a cell changes.
|
|
78
97
|
|
|
79
98
|
```javascript
|
|
80
|
-
const
|
|
81
|
-
const cell2 = Cell.source(0);
|
|
99
|
+
const count = Cell.source(0);
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
101
|
+
// Runs only on updates
|
|
102
|
+
const unsubscribe = count.listen((val) => console.log(val));
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
// Runs immediately, then on updates
|
|
105
|
+
count.runAndListen((val) => console.log('Current:', val));
|
|
89
106
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
cell2.set(2);
|
|
93
|
-
});
|
|
94
|
-
// Output: "Update occurred" (only once)
|
|
107
|
+
// Cleanup when done
|
|
108
|
+
unsubscribe();
|
|
95
109
|
```
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 2. Asynchronous State
|
|
114
|
+
|
|
115
|
+
Cells shines when handling async operations, replacing manual promise handling with declarative primitives.
|
|
98
116
|
|
|
99
|
-
|
|
117
|
+
#### Async Derived Cells
|
|
118
|
+
|
|
119
|
+
Use `Cell.derivedAsync` for data fetching or heavy computations. It automatically exposes `pending` and `error` states.
|
|
100
120
|
|
|
101
121
|
```javascript
|
|
102
|
-
const
|
|
103
|
-
const response = await fetch(`https://api.example.com/users/${userId}`);
|
|
104
|
-
return response.json();
|
|
105
|
-
});
|
|
122
|
+
const userId = Cell.source(1);
|
|
106
123
|
|
|
107
|
-
const
|
|
124
|
+
const user = Cell.derivedAsync(async (get, signal) => {
|
|
125
|
+
// 'get' tracks dependencies
|
|
126
|
+
const id = get(userId);
|
|
108
127
|
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
// 'signal' handles cancellation automatically if userId changes
|
|
129
|
+
const res = await fetch(`/api/users/${id}`, { signal });
|
|
130
|
+
return res.json();
|
|
111
131
|
});
|
|
112
132
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
});
|
|
133
|
+
// Built-in status tracking
|
|
134
|
+
user.pending.listen((isLoading) => console.log(isLoading ? 'Loading...' : 'Done'));
|
|
135
|
+
user.error.listen((err) => err && console.error(err));
|
|
118
136
|
|
|
119
|
-
|
|
137
|
+
// Access the data
|
|
138
|
+
const data = await user.get();
|
|
120
139
|
```
|
|
121
140
|
|
|
122
|
-
####
|
|
123
|
-
|
|
124
|
-
When you call `run()` multiple times on the same async cell, any previous ongoing async operations are automatically aborted. This prevents race conditions and ensures that only the result of the latest `run()` call is applied.
|
|
141
|
+
#### Task Cells
|
|
125
142
|
|
|
126
|
-
|
|
143
|
+
Use `Cell.task` for user-triggered actions (e.g., form submissions). Unlike derived cells, these only execute when triggered with `runWith`.
|
|
127
144
|
|
|
128
145
|
```javascript
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
146
|
+
const login = Cell.task(async (creds, signal) => {
|
|
147
|
+
const res = await fetch('/api/login', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: JSON.stringify(creds),
|
|
150
|
+
signal,
|
|
132
151
|
});
|
|
133
|
-
return
|
|
152
|
+
return res.json();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Trigger the task
|
|
156
|
+
const result = await login.runWith({ user: 'admin', pass: '1234' });
|
|
157
|
+
|
|
158
|
+
// Track status
|
|
159
|
+
login.pending.listen((isPending) => {
|
|
160
|
+
submitButton.disabled = isPending;
|
|
134
161
|
});
|
|
162
|
+
```
|
|
135
163
|
|
|
136
|
-
|
|
164
|
+
#### Composite Cells
|
|
137
165
|
|
|
138
|
-
|
|
139
|
-
run(123);
|
|
166
|
+
Group multiple async cells into a synchronized unit. Useful for preventing partial updates or ensuring "all-or-nothing" behavior.
|
|
140
167
|
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
```javascript
|
|
169
|
+
const profile = Cell.derivedAsync(fetchProfile);
|
|
170
|
+
const posts = Cell.derivedAsync(fetchPosts);
|
|
171
|
+
|
|
172
|
+
// Waits for BOTH to finish before updating
|
|
173
|
+
const dashboard = Cell.composite({ profile, posts });
|
|
174
|
+
|
|
175
|
+
dashboard.pending.listen((isPending) => showSpinner(isPending));
|
|
176
|
+
dashboard.error.listen((err) => err && showError(err));
|
|
143
177
|
|
|
144
|
-
|
|
178
|
+
dashboard.loaded.listen(async (ready) => {
|
|
179
|
+
if (ready) {
|
|
180
|
+
const profileData = await dashboard.values.profile.get();
|
|
181
|
+
const postsData = await dashboard.values.posts.get();
|
|
182
|
+
renderDashboard(profileData, postsData);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
145
185
|
```
|
|
146
186
|
|
|
147
|
-
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### 3. Advanced Patterns
|
|
148
190
|
|
|
149
|
-
|
|
191
|
+
#### Batch Updates
|
|
192
|
+
|
|
193
|
+
Group multiple updates into a single notification to avoid unnecessary re-computations.
|
|
150
194
|
|
|
151
195
|
```javascript
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
196
|
+
Cell.batch(() => {
|
|
197
|
+
firstName.set('John');
|
|
198
|
+
lastName.set('Doe');
|
|
199
|
+
// Effects run once here, after the block finishes
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Peeking
|
|
155
204
|
|
|
156
|
-
|
|
157
|
-
const flattenedArray = Cell.flattenArray(arrayOfCells);
|
|
158
|
-
console.log(flattenedArray); // Output: [1, 2, 3]
|
|
205
|
+
Read a value *without* subscribing to it.
|
|
159
206
|
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
207
|
+
```javascript
|
|
208
|
+
const sum = Cell.derived(() => {
|
|
209
|
+
// Re-runs if 'a' changes, but NOT if 'b' changes
|
|
210
|
+
return a.get() + b.peek();
|
|
211
|
+
});
|
|
163
212
|
```
|
|
164
213
|
|
|
165
|
-
|
|
214
|
+
#### Custom Equality
|
|
166
215
|
|
|
167
|
-
|
|
216
|
+
Customize how cells detect changes.
|
|
168
217
|
|
|
169
218
|
```javascript
|
|
170
|
-
const
|
|
171
|
-
{ name: 'Alice'
|
|
219
|
+
const user = Cell.source(
|
|
220
|
+
{ id: 1, name: 'Alice' },
|
|
172
221
|
{
|
|
173
|
-
equals: (a, b) => a.
|
|
222
|
+
equals: (a, b) => a.id === b.id, // Only update if ID changes
|
|
174
223
|
}
|
|
175
224
|
);
|
|
176
225
|
```
|
|
177
226
|
|
|
178
|
-
|
|
227
|
+
#### Memory Management (Contexts)
|
|
179
228
|
|
|
180
|
-
|
|
229
|
+
For high-performance scenarios involving many dynamically created cells, use `LocalContext` for manual disposal.
|
|
181
230
|
|
|
182
231
|
```javascript
|
|
183
|
-
const
|
|
232
|
+
const ctx = Cell.context();
|
|
184
233
|
|
|
185
|
-
|
|
186
|
-
|
|
234
|
+
Cell.runWithContext(ctx, () => {
|
|
235
|
+
// All listeners and derived cells created here are bound to 'ctx'
|
|
236
|
+
source.listen(handler);
|
|
237
|
+
const derived = Cell.derived(() => source.get() * 2);
|
|
187
238
|
});
|
|
188
239
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
count.stopListeningTo('countLogger');
|
|
240
|
+
// Clean up everything at once
|
|
241
|
+
ctx.destroy();
|
|
192
242
|
```
|
|
193
243
|
|
|
194
|
-
|
|
244
|
+
---
|
|
195
245
|
|
|
196
|
-
|
|
246
|
+
## API Reference
|
|
197
247
|
|
|
198
|
-
|
|
248
|
+
### `Cell` Static Methods
|
|
199
249
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
| Method | Description |
|
|
251
|
+
|--------|-------------|
|
|
252
|
+
| `source(value, options?)` | Creates a mutable source cell. |
|
|
253
|
+
| `derived(fn)` | Creates a computed cell from other cells. |
|
|
254
|
+
| `derivedAsync(fn)` | Creates an async computed cell with cancellation support. |
|
|
255
|
+
| `task(fn)` | Creates a triggerable async task. |
|
|
256
|
+
| `composite(map)` | Combines multiple cells into a synchronized object. |
|
|
257
|
+
| `batch(fn)` | Batches updates to prevent multiple effect triggers. |
|
|
258
|
+
| `context()` | Creates a new `LocalContext` for scoped memory management. |
|
|
259
|
+
| `runWithContext(ctx, fn)` | Executes a function within a specific `LocalContext`. |
|
|
260
|
+
| `isCell(value)` | Returns `true` if the value is a Cell. |
|
|
261
|
+
|
|
262
|
+
### Cell Instance Methods
|
|
263
|
+
|
|
264
|
+
| Method | Description |
|
|
265
|
+
|--------|-------------|
|
|
266
|
+
| `get()` | Returns the value. Registers dependency if in a derivation. |
|
|
267
|
+
| `peek()` | Returns the value without registering a dependency. |
|
|
268
|
+
| `listen(callback, options?)` | Subscribes to changes. Returns an unsubscribe function. |
|
|
269
|
+
| `runAndListen(callback, options?)` | Runs callback immediately, then subscribes to future changes. |
|
|
270
|
+
| `ignore(callback)` | Removes a previously registered listener. |
|
|
271
|
+
| `valueOf()` | Returns the raw value (for implicit coercion). |
|
|
272
|
+
| `toString()` | Returns the stringified value. |
|
|
273
|
+
|
|
274
|
+
### `SourceCell` Methods
|
|
275
|
+
|
|
276
|
+
| Method | Description |
|
|
277
|
+
|--------|-------------|
|
|
278
|
+
| `set(value)` | Updates the cell's value and notifies listeners. |
|
|
279
|
+
|
|
280
|
+
### `AsyncCell` Properties and Methods
|
|
281
|
+
|
|
282
|
+
Available on `AsyncDerivedCell` and `AsyncTaskCell`:
|
|
283
|
+
|
|
284
|
+
| Property/Method | Description |
|
|
285
|
+
|-----------------|-------------|
|
|
286
|
+
| `pending` | A `Cell<boolean>` indicating loading state. |
|
|
287
|
+
| `error` | A `Cell<Error \| null>` holding the last error. |
|
|
288
|
+
| `get()` | Returns a `Promise` that resolves to the value. |
|
|
289
|
+
| `peek()` | Returns a `Promise` without registering dependencies. |
|
|
290
|
+
|
|
291
|
+
### `AsyncDerivedCell` Methods
|
|
292
|
+
|
|
293
|
+
| Method | Description |
|
|
294
|
+
|--------|-------------|
|
|
295
|
+
| `revalidate()` | Forces a refresh of the async computation. |
|
|
296
|
+
|
|
297
|
+
### `AsyncTaskCell` Methods
|
|
298
|
+
|
|
299
|
+
| Method | Description |
|
|
300
|
+
|--------|-------------|
|
|
301
|
+
| `runWith(input)` | Executes the task with the given input. Returns a `Promise`. |
|
|
302
|
+
|
|
303
|
+
### `Composite` Object
|
|
304
|
+
|
|
305
|
+
Returned by `Cell.composite()`:
|
|
306
|
+
|
|
307
|
+
| Property | Description |
|
|
308
|
+
|----------|-------------|
|
|
309
|
+
| `values` | Object containing synchronized async cells for each input key. |
|
|
310
|
+
| `pending` | A `Cell<boolean>` that is `true` while any input is pending. |
|
|
311
|
+
| `error` | A `Cell<Error \| null>` with the first error from any input. |
|
|
312
|
+
| `loaded` | A `Cell<boolean>` that becomes `true` after initial load completes. |
|
|
313
|
+
|
|
314
|
+
### `LocalContext` Methods
|
|
315
|
+
|
|
316
|
+
| Method | Description |
|
|
317
|
+
|--------|-------------|
|
|
318
|
+
| `destroy()` | Disposes all listeners and derived cells bound to this context. |
|
|
207
319
|
|
|
208
320
|
### Effect Options
|
|
209
321
|
|
|
210
|
-
|
|
322
|
+
Options for `listen()` and `runAndListen()`:
|
|
211
323
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
324
|
+
| Option | Type | Description |
|
|
325
|
+
|--------|------|-------------|
|
|
326
|
+
| `once` | `boolean` | Remove the listener after it fires once. |
|
|
327
|
+
| `signal` | `AbortSignal` | Automatically remove listener when signal aborts. |
|
|
328
|
+
| `weak` | `boolean` | Use a weak reference (listener may be garbage collected). |
|
|
329
|
+
| `priority` | `number` | Execution order (higher runs first, default: 0). |
|
|
330
|
+
|
|
331
|
+
### Cell Options
|
|
332
|
+
|
|
333
|
+
Options for `Cell.source()`:
|
|
334
|
+
|
|
335
|
+
| Option | Type | Description |
|
|
336
|
+
|--------|------|-------------|
|
|
337
|
+
| `equals` | `(a, b) => boolean` | Custom equality function for change detection. |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## TypeScript Support
|
|
342
|
+
|
|
343
|
+
Cells is written in JavaScript with comprehensive JSDoc annotations and ships with TypeScript declaration files.
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { Cell, SourceCell, DerivedCell, AsyncDerivedCell } from '@adbl/cells';
|
|
347
|
+
|
|
348
|
+
// Types are inferred automatically
|
|
349
|
+
const count: SourceCell<number> = Cell.source(0);
|
|
350
|
+
const doubled: DerivedCell<number> = Cell.derived(() => count.get() * 2);
|
|
351
|
+
|
|
352
|
+
// Async cells with proper typing
|
|
353
|
+
const user: AsyncDerivedCell<User> = Cell.derivedAsync(async (get) => {
|
|
354
|
+
const id = get(userId);
|
|
355
|
+
const res = await fetch(`/api/users/${id}`);
|
|
356
|
+
return res.json() as User;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Task cells with input/output types
|
|
360
|
+
const submitForm = Cell.task(async (data: FormData, signal: AbortSignal) => {
|
|
361
|
+
const res = await fetch('/api/submit', { method: 'POST', body: data, signal });
|
|
362
|
+
return res.json() as SubmitResult;
|
|
218
363
|
});
|
|
219
364
|
```
|
|
220
365
|
|
|
221
|
-
|
|
366
|
+
---
|
|
222
367
|
|
|
223
|
-
|
|
368
|
+
## Contributing
|
|
224
369
|
|
|
225
|
-
|
|
370
|
+
Contributions are welcome! Here's how to get started:
|
|
226
371
|
|
|
227
|
-
|
|
228
|
-
const ctx = Cell.context();
|
|
229
|
-
const source = Cell.source(1);
|
|
372
|
+
### Development Setup
|
|
230
373
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
374
|
+
```bash
|
|
375
|
+
# Clone the repository
|
|
376
|
+
git clone https://github.com/adebola-io/signals.git
|
|
377
|
+
cd signals
|
|
235
378
|
|
|
236
|
-
|
|
379
|
+
# Install dependencies
|
|
380
|
+
npm install
|
|
237
381
|
|
|
238
|
-
|
|
239
|
-
|
|
382
|
+
# Run tests in watch mode
|
|
383
|
+
npm test
|
|
384
|
+
|
|
385
|
+
# Run tests once
|
|
386
|
+
npm run test-once
|
|
387
|
+
|
|
388
|
+
# Build the project
|
|
389
|
+
npm run build
|
|
390
|
+
```
|
|
240
391
|
|
|
241
|
-
|
|
392
|
+
### Running Tests
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
npm test
|
|
242
396
|
```
|
|
397
|
+
|
|
398
|
+
The test suite uses [Vitest](https://vitest.dev/) and covers all core functionality including async behavior and race conditions.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## License
|
|
403
|
+
|
|
404
|
+
MIT © [Sefunmi Adebola Akomolafe](https://github.com/adebola-io)
|