ccstate 4.13.0 → 5.2.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 +126 -77
- package/core/index.cjs +69 -215
- package/core/index.cjs.map +1 -1
- package/core/index.d.cts +6 -6
- package/core/index.d.ts +6 -6
- package/core/index.js +70 -215
- package/core/index.js.map +1 -1
- package/debug/index.cjs +85 -286
- package/debug/index.cjs.map +1 -1
- package/debug/index.d.cts +6 -6
- package/debug/index.d.ts +6 -6
- package/debug/index.js +85 -286
- package/debug/index.js.map +1 -1
- package/index.cjs +67 -296
- package/index.cjs.map +1 -1
- package/index.d.cts +7 -8
- package/index.d.ts +7 -8
- package/index.js +68 -296
- package/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
[](https://deepwiki.com/e7h4n/ccstate)
|
|
5
6
|
[](https://coveralls.io/github/e7h4n/ccstate?branch=main)
|
|
6
7
|

|
|
7
8
|

|
|
@@ -136,7 +137,7 @@ const otherStore = createStore(); // another new Map()
|
|
|
136
137
|
otherStore.get(count$); // anotherMap[$count] ?? $count.init, returns 0
|
|
137
138
|
```
|
|
138
139
|
|
|
139
|
-
This should be easy to understand. If `Store` only needed to support `State` types, a simple Map would be sufficient. However, CCState needs to support
|
|
140
|
+
This should be easy to understand. If `Store` only needed to support `State` types, a simple Map would be sufficient. However, CCState needs to support another signal type. Next, let's introduce `Computed`, CCState's reactive computation unit.
|
|
140
141
|
|
|
141
142
|
### Computed
|
|
142
143
|
|
|
@@ -191,11 +192,10 @@ Since `Command` can call the `set` method, it produces side effects on the `Stor
|
|
|
191
192
|
|
|
192
193
|
- Calling a `Command` through `store.set`
|
|
193
194
|
- Being called by the `set` method within other `Command`s
|
|
194
|
-
- Being triggered by subscription relationships established through `store.sub`
|
|
195
195
|
|
|
196
|
-
###
|
|
196
|
+
### Watching to Changes
|
|
197
197
|
|
|
198
|
-
CCState provides a `
|
|
198
|
+
CCState provides a `watch` method on the store to observe state change.
|
|
199
199
|
|
|
200
200
|
```typescript
|
|
201
201
|
import { createStore, state, computed, command } from 'ccstate';
|
|
@@ -204,35 +204,40 @@ const base$ = state(0);
|
|
|
204
204
|
const double$ = computed((get) => get(base$) * 2);
|
|
205
205
|
|
|
206
206
|
const store = createStore();
|
|
207
|
-
store.
|
|
208
|
-
double
|
|
209
|
-
|
|
210
|
-
console.log('double', get(double$));
|
|
211
|
-
}),
|
|
212
|
-
);
|
|
207
|
+
store.watch((get) => {
|
|
208
|
+
console.log('double', get(double$));
|
|
209
|
+
});
|
|
213
210
|
|
|
214
211
|
store.set(base$, 10); // will log to console 'double 20'
|
|
215
212
|
```
|
|
216
213
|
|
|
217
|
-
|
|
214
|
+
Using an AbortSignal to cancel any watcher.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
const ctrl = new AbortController();
|
|
218
|
+
store.watch(
|
|
219
|
+
(get) => {
|
|
220
|
+
console.log('double', get(double$));
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
signal: ctrl.signal,
|
|
224
|
+
},
|
|
225
|
+
);
|
|
218
226
|
|
|
219
|
-
|
|
220
|
-
|
|
227
|
+
ctrl.abort(); // will cancel watch
|
|
228
|
+
```
|
|
221
229
|
|
|
222
|
-
|
|
230
|
+
Watchers intentionally do not have access to the store's `set` method. CCState encourages implementing derived computation logic through `Computed` rather than modifying data through change callbacks.
|
|
223
231
|
|
|
224
232
|
```typescript
|
|
225
|
-
// 🙅 use
|
|
233
|
+
// 🙅 use watch & store.set
|
|
226
234
|
const user$ = state(undefined);
|
|
227
235
|
const userId$ = state(0);
|
|
228
|
-
store.
|
|
229
|
-
userId
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
set(user$, user);
|
|
234
|
-
}),
|
|
235
|
-
);
|
|
236
|
+
store.watch((get) => {
|
|
237
|
+
const userId = get(userId$);
|
|
238
|
+
const user = fetch('/api/users/' + userId).then((resp) => resp.json());
|
|
239
|
+
store.set(user$, user);
|
|
240
|
+
});
|
|
236
241
|
|
|
237
242
|
// ✅ use computed
|
|
238
243
|
const userId$ = state(0);
|
|
@@ -252,11 +257,11 @@ Here's a simple rule of thumb:
|
|
|
252
257
|
|
|
253
258
|
### Comprasion
|
|
254
259
|
|
|
255
|
-
| Type | get | set |
|
|
256
|
-
| -------- | --- | --- |
|
|
257
|
-
| State | ✅ | ✅ |
|
|
258
|
-
| Computed | ✅ | ❌ |
|
|
259
|
-
| Command | ❌ | ✅ |
|
|
260
|
+
| Type | get | set |
|
|
261
|
+
| -------- | --- | --- |
|
|
262
|
+
| State | ✅ | ✅ |
|
|
263
|
+
| Computed | ✅ | ❌ |
|
|
264
|
+
| Command | ❌ | ✅ |
|
|
260
265
|
|
|
261
266
|
That's it! Next, you can learn how to use CCState in React.
|
|
262
267
|
|
|
@@ -313,13 +318,85 @@ It is easy to see that `userId` passed by the function parameters do not have re
|
|
|
313
318
|
The `Command` has the ability to receive parameters, so we can encapsulate the above example into a `Command`:
|
|
314
319
|
|
|
315
320
|
```javascript
|
|
316
|
-
const getUser$ = command(({ set, get }, userId,
|
|
321
|
+
const getUser$ = command(({ set, get }, userId, signal) => {
|
|
317
322
|
return fetch(`/api/users/${userId}`, { signal });
|
|
318
323
|
});
|
|
319
324
|
```
|
|
320
325
|
|
|
321
326
|
However, this method requires that the caller must also be a `Command` in order to access the `set` method, which can easily lead to destructive passing of `Command`. If a `Command` itself does not produce side effects, then it should be considered to be written as a `Computed`.
|
|
322
327
|
|
|
328
|
+
### Robust Async Flow Cancellation
|
|
329
|
+
|
|
330
|
+
CCState's design philosophy aims to use the language's native capabilities for flow control as much as possible, including direct support for async/await. For Promise cancellation issues brought by asynchronous operations, CCState prefers to handle them using AbortSignal like fetch. When the following rules are satisfied, CCState can achieve very robust Async Flow cancellation capabilities, preventing unexpected side effects from occurring after the upstream task is aborted.
|
|
331
|
+
|
|
332
|
+
#### Rule 1: await in commands must perform abort signal check
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const updateUser$ = command(async ({ get }, signal: AbortSignal) => {
|
|
336
|
+
const currentUser = await get(currentUser$); // Here we assume currentUser$ returns a Promise<User>
|
|
337
|
+
signal.throwIfAbort(); // Must perform throwIfAbort check on the line after await
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
If the function after await accepts AbortSignal as a parameter, then the caller has no responsibility to perform throwIfAbort check, as it's assumed the callee has already done such check.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
const updateUser$ = command(async ({ get, set }, name: string, signal: AbortSignal) => {
|
|
345
|
+
const currentUser = await get(currentUser$);
|
|
346
|
+
signal.throwIfAbort();
|
|
347
|
+
|
|
348
|
+
await set(updateUserName$, currentUser.id, name, signal); // Here we pass signal to the next command, so the current command doesn't need to check
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`Computed` doesn't need such checks because Computed is side-effect free.
|
|
353
|
+
|
|
354
|
+
In `watch`, although it cannot produce side effects on the Store, it may produce external side effects. In this case, signal check is also needed.
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
store.watch(
|
|
358
|
+
(get, { signal }) => {
|
|
359
|
+
void (async () => {
|
|
360
|
+
const user = await get(currentUser$);
|
|
361
|
+
signal.throwIfAbort();
|
|
362
|
+
|
|
363
|
+
syncExternalState(user); // this code will not execute when externalAbortSignal abort
|
|
364
|
+
})();
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
signal: externalAbortSignal,
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Rule 2: Do not intercept AbortError in try/catch
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
const isAbortError = (error: unknown): boolean => {
|
|
376
|
+
if ((error instanceof Error || error instanceof DOMException) && error.name === 'AbortError') {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return false;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
function throwIfAbort(e: unknown) {
|
|
384
|
+
if (isAbortError(e)) {
|
|
385
|
+
throw e;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const updateUser$ = command(async ({ get, set }, name: string, signal: AbortSignal) => {
|
|
390
|
+
// ...
|
|
391
|
+
try {
|
|
392
|
+
await set(updateUserName$, currentUser.id, name, signal);
|
|
393
|
+
} catch (error: unknown) {
|
|
394
|
+
throwIfAbort(error); // do not catch any AbortError
|
|
395
|
+
// .. error handling
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
323
400
|
## Using in React
|
|
324
401
|
|
|
325
402
|
[Using in React](docs/react.md)
|
|
@@ -365,12 +442,8 @@ import { createDebugStore, state, computed, command } from 'ccstate';
|
|
|
365
442
|
const base$ = state(1, { debugLabel: 'base$' });
|
|
366
443
|
const derived$ = computed((get) => get(base$) * 2);
|
|
367
444
|
|
|
368
|
-
const store = createDebugStore([base$, 'derived'], ['set'
|
|
445
|
+
const store = createDebugStore([base$, 'derived'], ['set']); // log set actions
|
|
369
446
|
store.set(base$, 1); // console: SET [V0:base$] 1
|
|
370
|
-
store.sub(
|
|
371
|
-
derived$,
|
|
372
|
-
command(() => void 0),
|
|
373
|
-
); // console: SUB [V0:derived$]
|
|
374
447
|
```
|
|
375
448
|
|
|
376
449
|
## Concept behind CCState
|
|
@@ -397,28 +470,9 @@ Like Jotai, CCState is also an Atom State solution. However, unlike Jotai, CCSta
|
|
|
397
470
|
|
|
398
471
|
### Subscription System
|
|
399
472
|
|
|
400
|
-
CCState's subscription system is different from Jotai's. First,
|
|
473
|
+
CCState's subscription system is very different from Jotai's. First, the method name `watch` itself implies this is a read-only operation for observing state changes, not for modifying state.
|
|
401
474
|
|
|
402
|
-
|
|
403
|
-
export const userId$ = state(1);
|
|
404
|
-
|
|
405
|
-
export const userIdChange$ = command(({ get, set }) => {
|
|
406
|
-
const userId = get(userId$);
|
|
407
|
-
// ...
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// ...
|
|
411
|
-
import { userId$, userIdChange$ } from './signals';
|
|
412
|
-
|
|
413
|
-
function setupPage() {
|
|
414
|
-
const store = createStore();
|
|
415
|
-
// ...
|
|
416
|
-
store.sub(userId$, userIdChange$);
|
|
417
|
-
// ...
|
|
418
|
-
}
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
The consideration here is to avoid having callbacks depend on the Store object, which was a key design consideration when creating CCState. In CCState, `sub` is the only API with reactive capabilities, and CCState reduces the complexity of reactive computations by limiting Store usage.
|
|
475
|
+
Secondly, the subscription callback receives a `get` parameter, the consideration here is to avoid having callbacks depend on the Store object, which was a key design consideration when creating CCState. In CCState, `watch` is the only API with reactive capabilities, and CCState reduces the complexity of reactive computations by limiting Store usage.
|
|
422
476
|
|
|
423
477
|
In Jotai, there are no restrictions on writing code that uses sub within a sub callback:
|
|
424
478
|
|
|
@@ -432,27 +486,26 @@ store.sub(targetAtom, () => {
|
|
|
432
486
|
});
|
|
433
487
|
```
|
|
434
488
|
|
|
435
|
-
In CCState, we can prevent this situation by moving the `
|
|
489
|
+
In CCState, we can prevent this situation by moving the `Watcher` definition to a separate file and protecting the Store.
|
|
436
490
|
|
|
437
491
|
```typescript
|
|
438
492
|
// main.ts
|
|
439
|
-
import {
|
|
440
|
-
import { foo$ } from './states
|
|
493
|
+
import { fooWatcher } from './watchers';
|
|
441
494
|
|
|
442
495
|
function initApp() {
|
|
443
|
-
const store = createStore()
|
|
444
|
-
store.
|
|
496
|
+
const store = createStore();
|
|
497
|
+
store.watch(fooWatcher);
|
|
445
498
|
// do not expose store to outside
|
|
446
499
|
}
|
|
447
500
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
501
|
+
// watchers.ts
|
|
502
|
+
export const fooWatcher = (get) => {
|
|
503
|
+
// there is no way to use store watch here
|
|
504
|
+
get(foo$);
|
|
505
|
+
};
|
|
453
506
|
```
|
|
454
507
|
|
|
455
|
-
Therefore, in CCState, the capability of `
|
|
508
|
+
Therefore, in CCState, the capability of `watcher` is intentionally limited. CCState encourages developers to handle data consistency updates within `Computed` and `Watcher`, rather than relying on subscription capabilities for reactive data updates. In fact, in a React application, CCState's `watch` is likely only used in conjunction with `useSyncExternalStore` to update views, or other external state system out of CCState.
|
|
456
509
|
|
|
457
510
|
### Avoid `useEffect` in React
|
|
458
511
|
|
|
@@ -579,12 +632,11 @@ function App() {
|
|
|
579
632
|
|
|
580
633
|
// A more recommended approach is to enable side effects outside of React
|
|
581
634
|
// main.ts
|
|
582
|
-
store.
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}),
|
|
635
|
+
store.watch(
|
|
636
|
+
// watch is always effect-less to any State
|
|
637
|
+
(get) => {
|
|
638
|
+
get(count$); // ... onCount
|
|
639
|
+
},
|
|
588
640
|
);
|
|
589
641
|
store.set(setupTimer$); // must setup effect explicitly
|
|
590
642
|
|
|
@@ -734,13 +786,10 @@ At this point, the dependency array of `derived$` is `[branch$]`, because `deriv
|
|
|
734
786
|
store.set(branch$, 'D'); // will not trigger derived$'s read, until next get(derived$)
|
|
735
787
|
```
|
|
736
788
|
|
|
737
|
-
Once we mount `derived$` by `
|
|
789
|
+
Once we mount `derived$` by `watch`, all its direct and indirect dependencies will enter the _mounted_ state.
|
|
738
790
|
|
|
739
791
|
```typescript
|
|
740
|
-
store.
|
|
741
|
-
derived$,
|
|
742
|
-
command(() => void 0),
|
|
743
|
-
);
|
|
792
|
+
store.watch((get) => get(derived$));
|
|
744
793
|
```
|
|
745
794
|
|
|
746
795
|
The mount graph in CCState is `[derived$, [branch$]]`. When `branch$` is reset, `derived$` will be re-evaluated immediately, and all subscribers will be notified.
|