@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 CHANGED
@@ -3,240 +3,402 @@
3
3
  [![npm version](https://badge.fury.io/js/%40adbl%2Fcells.svg)](https://badge.fury.io/js/%40adbl%2Fcells)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Cells is a powerful yet lightweight library for reactive state management in JavaScript applications. It offers an intuitive API that simplifies the complexities of managing and propagating state changes throughout your application.
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
- - **Simple API**: Easy to learn and use, even for developers new to reactive programming.
11
- - **Lightweight**: No external dependencies, keeping your project lean.
12
- - **Flexible**: Works seamlessly with any JavaScript framework or vanilla JS.
13
- - **Type-safe**: Built with TypeScript, providing excellent type inference and checking.
14
- - **Performant**: Optimized for efficiency, with features like batched updates to minimize unnecessary computations.
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
- ## Core Concepts
31
-
32
- ### 1. Source Cells
41
+ ```bash
42
+ pnpm add @adbl/cells
43
+ ```
33
44
 
34
- Source cells are the building blocks of your reactive state. They hold values that can change over time, automatically notifying dependents when updates occur.
45
+ ## Quick Start
35
46
 
36
47
  ```javascript
37
48
  import { Cell } from '@adbl/cells';
38
49
 
39
- const count = Cell.source(0);
40
- console.log(count.get()); // Output: 0
50
+ // 1. Create a source cell
51
+ const name = Cell.source('World');
41
52
 
42
- count.set(5);
43
- console.log(count.get()); // Output: 5
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
- ### 2. Derived Cells
63
+ ---
64
+
65
+ ## Guide
47
66
 
48
- Derived cells allow you to create computed values based on other cells. They update automatically when their dependencies change, ensuring your derived state is always in sync.
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(5);
57
- console.log(doubledCount.get()); // Output: 10
76
+ count.set(1);
77
+ console.log(count.get()); // 1
58
78
  ```
59
79
 
60
- ### 3. Reactive Effects
80
+ #### Derived Cells
61
81
 
62
- Easily set up listeners to react to changes in cell values, allowing you to create side effects or update your UI in response to state changes.
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(0);
85
+ const count = Cell.source(1);
86
+ const double = Cell.derived(() => count.get() * 2);
66
87
 
67
- count.listen((newValue) => {
68
- console.log(`Count changed to: ${newValue}`);
69
- });
88
+ console.log(double.get()); // 2
70
89
 
71
- count.set(3); // Output: "Count changed to: 3"
72
- count.set(7); // Output: "Count changed to: 7"
90
+ count.set(5);
91
+ console.log(double.get()); // 10
73
92
  ```
74
93
 
75
- ### 5. Batch Updates
94
+ #### Effects (`listen`)
76
95
 
77
- When you need to perform multiple updates but only want to trigger effects once, you can use batch updates to optimize performance:
96
+ Run side effects when a cell changes.
78
97
 
79
98
  ```javascript
80
- const cell1 = Cell.source(0);
81
- const cell2 = Cell.source(0);
99
+ const count = Cell.source(0);
82
100
 
83
- const callback = () => {
84
- console.log('Update occurred');
85
- };
101
+ // Runs only on updates
102
+ const unsubscribe = count.listen((val) => console.log(val));
86
103
 
87
- cell1.listen(callback);
88
- cell2.listen(callback);
104
+ // Runs immediately, then on updates
105
+ count.runAndListen((val) => console.log('Current:', val));
89
106
 
90
- Cell.batch(() => {
91
- cell1.set(1);
92
- cell2.set(2);
93
- });
94
- // Output: "Update occurred" (only once)
107
+ // Cleanup when done
108
+ unsubscribe();
95
109
  ```
96
110
 
97
- ### 6. Async Operations
111
+ ---
112
+
113
+ ### 2. Asynchronous State
114
+
115
+ Cells shines when handling async operations, replacing manual promise handling with declarative primitives.
98
116
 
99
- Cells provides utilities for handling asynchronous operations, making it easy to manage loading states, data, and errors:
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 fetchUser = Cell.async(async (userId) => {
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 { pending, data, error, run } = fetchUser;
124
+ const user = Cell.derivedAsync(async (get, signal) => {
125
+ // 'get' tracks dependencies
126
+ const id = get(userId);
108
127
 
109
- pending.listen((isPending) => {
110
- console.log(isPending ? 'Loading...' : 'Done!');
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
- data.listen((userData) => {
114
- if (userData) {
115
- console.log('User data:', userData);
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
- run(123); // Triggers the async operation
137
+ // Access the data
138
+ const data = await user.get();
120
139
  ```
121
140
 
122
- #### Automatic Abort of Previous Operations
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
- Additionally, the getter function receives an `AbortSignal` via `this.signal`, which you can use to cancel long-running operations or check for abortion:
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 fetchUser = Cell.async(async function (userId) {
130
- const response = await fetch(`https://api.example.com/users/${userId}`, {
131
- signal: this.signal, // Pass the abort signal to fetch
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 response.json();
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
- const { data, run } = fetchUser;
164
+ #### Composite Cells
137
165
 
138
- // Start first request
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
- // Start second request - this aborts the first
142
- run(456);
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
- // Only the result of the second request (user 456) will be applied
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
- ### 7. Flattening
187
+ ---
188
+
189
+ ### 3. Advanced Patterns
148
190
 
149
- Cells offers utility functions to work with nested cell structures, making it easier to handle complex state shapes:
191
+ #### Batch Updates
192
+
193
+ Group multiple updates into a single notification to avoid unnecessary re-computations.
150
194
 
151
195
  ```javascript
152
- const nestedCell = Cell.source(Cell.source(5));
153
- const flattenedValue = Cell.flatten(nestedCell);
154
- console.log(flattenedValue); // Output: 5
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
- const arrayOfCells = [Cell.source(1), Cell.source(2), Cell.source(3)];
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
- const objectWithCells = { a: Cell.source(1), b: Cell.source(2) };
161
- const flattenedObject = Cell.flattenObject(objectWithCells);
162
- console.log(flattenedObject); // Output: { a: 1, b: 2 }
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
- ### 8. Custom Equality Checks
214
+ #### Custom Equality
166
215
 
167
- For more complex objects, you can provide custom equality functions to determine when a cell's value has truly changed:
216
+ Customize how cells detect changes.
168
217
 
169
218
  ```javascript
170
- const userCell = Cell.source(
171
- { name: 'Alice', age: 30 },
219
+ const user = Cell.source(
220
+ { id: 1, name: 'Alice' },
172
221
  {
173
- equals: (a, b) => a.name === b.name && a.age === b.age,
222
+ equals: (a, b) => a.id === b.id, // Only update if ID changes
174
223
  }
175
224
  );
176
225
  ```
177
226
 
178
- ### 9. Named Effects
227
+ #### Memory Management (Contexts)
179
228
 
180
- To aid in debugging, you can name your effects, making it easier to track and manage them:
229
+ For high-performance scenarios involving many dynamically created cells, use `LocalContext` for manual disposal.
181
230
 
182
231
  ```javascript
183
- const count = Cell.source(0);
232
+ const ctx = Cell.context();
184
233
 
185
- count.listen((value) => console.log(`Count is now: ${value}`), {
186
- name: 'countLogger',
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
- console.log(count.isListeningTo('countLogger')); // Output: true
190
-
191
- count.stopListeningTo('countLogger');
240
+ // Clean up everything at once
241
+ ctx.destroy();
192
242
  ```
193
243
 
194
- ## Advanced Features and API Details
244
+ ---
195
245
 
196
- ### Cell Options
246
+ ## API Reference
197
247
 
198
- When creating a source cell, you have fine-grained control over its behavior:
248
+ ### `Cell` Static Methods
199
249
 
200
- ```javascript
201
- const cell = Cell.source(initialValue, {
202
- immutable: boolean, // If true, the cell will not allow updates
203
- deep: boolean, // By default, the cell only reacts to changes at the top level of objects. Setting deep to true will proxy the cell to all nested properties and trigger updates when they change as well.
204
- equals: (oldValue, newValue) => boolean, // Custom equality function
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
- When setting up listeners or effects, you can customize their behavior:
322
+ Options for `listen()` and `runAndListen()`:
211
323
 
212
- ```javascript
213
- cell.listen(callback, {
214
- once: boolean, // If true, the effect will only run once
215
- signal: AbortSignal, // An AbortSignal to cancel the effect
216
- name: string, // A name for the effect (useful for debugging)
217
- priority: number, // The priority of the effect (higher priority effects run first)
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
- ### Explicit Disposal (Contexts)
366
+ ---
222
367
 
223
- By default, Cells uses `WeakRef` and Garbage Collection to manage memory. This is easy to use but can lead to "ghost computations", where listeners and derived cells keep running for a short time after they are no longer needed.
368
+ ## Contributing
224
369
 
225
- For high-performance scenarios, you can use a `LocalContext` to group dependencies and kill them synchronously.
370
+ Contributions are welcome! Here's how to get started:
226
371
 
227
- ```javascript
228
- const ctx = Cell.context();
229
- const source = Cell.source(1);
372
+ ### Development Setup
230
373
 
231
- Cell.runWithContext(ctx, () => {
232
- // This listener is now bound to 'ctx' (Strong Reference)
233
- source.listen((val) => console.log(val));
234
- });
374
+ ```bash
375
+ # Clone the repository
376
+ git clone https://github.com/adebola-io/signals.git
377
+ cd signals
235
378
 
236
- source.set(2); // Logs: 2
379
+ # Install dependencies
380
+ npm install
237
381
 
238
- // Synchronously remove all listeners created in that block
239
- ctx.destroy();
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
- source.set(3); // Nothing happens
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)