ccstate 4.13.0 → 5.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 +125 -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 +1 -1
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ const otherStore = createStore(); // another new Map()
|
|
|
136
136
|
otherStore.get(count$); // anotherMap[$count] ?? $count.init, returns 0
|
|
137
137
|
```
|
|
138
138
|
|
|
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
|
|
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 another signal type. Next, let's introduce `Computed`, CCState's reactive computation unit.
|
|
140
140
|
|
|
141
141
|
### Computed
|
|
142
142
|
|
|
@@ -191,11 +191,10 @@ Since `Command` can call the `set` method, it produces side effects on the `Stor
|
|
|
191
191
|
|
|
192
192
|
- Calling a `Command` through `store.set`
|
|
193
193
|
- Being called by the `set` method within other `Command`s
|
|
194
|
-
- Being triggered by subscription relationships established through `store.sub`
|
|
195
194
|
|
|
196
|
-
###
|
|
195
|
+
### Watching to Changes
|
|
197
196
|
|
|
198
|
-
CCState provides a `
|
|
197
|
+
CCState provides a `watch` method on the store to observe state change.
|
|
199
198
|
|
|
200
199
|
```typescript
|
|
201
200
|
import { createStore, state, computed, command } from 'ccstate';
|
|
@@ -204,35 +203,40 @@ const base$ = state(0);
|
|
|
204
203
|
const double$ = computed((get) => get(base$) * 2);
|
|
205
204
|
|
|
206
205
|
const store = createStore();
|
|
207
|
-
store.
|
|
208
|
-
double
|
|
209
|
-
|
|
210
|
-
console.log('double', get(double$));
|
|
211
|
-
}),
|
|
212
|
-
);
|
|
206
|
+
store.watch((get) => {
|
|
207
|
+
console.log('double', get(double$));
|
|
208
|
+
});
|
|
213
209
|
|
|
214
210
|
store.set(base$, 10); // will log to console 'double 20'
|
|
215
211
|
```
|
|
216
212
|
|
|
217
|
-
|
|
213
|
+
Using an AbortSignal to cancel any watcher.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const ctrl = new AbortController();
|
|
217
|
+
store.watch(
|
|
218
|
+
(get) => {
|
|
219
|
+
console.log('double', get(double$));
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
signal: ctrl.signal,
|
|
223
|
+
},
|
|
224
|
+
);
|
|
218
225
|
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
ctrl.abort(); // will cancel watch
|
|
227
|
+
```
|
|
221
228
|
|
|
222
|
-
|
|
229
|
+
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
230
|
|
|
224
231
|
```typescript
|
|
225
|
-
// 🙅 use
|
|
232
|
+
// 🙅 use watch & store.set
|
|
226
233
|
const user$ = state(undefined);
|
|
227
234
|
const userId$ = state(0);
|
|
228
|
-
store.
|
|
229
|
-
userId
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
set(user$, user);
|
|
234
|
-
}),
|
|
235
|
-
);
|
|
235
|
+
store.watch((get) => {
|
|
236
|
+
const userId = get(userId$);
|
|
237
|
+
const user = fetch('/api/users/' + userId).then((resp) => resp.json());
|
|
238
|
+
store.set(user$, user);
|
|
239
|
+
});
|
|
236
240
|
|
|
237
241
|
// ✅ use computed
|
|
238
242
|
const userId$ = state(0);
|
|
@@ -252,11 +256,11 @@ Here's a simple rule of thumb:
|
|
|
252
256
|
|
|
253
257
|
### Comprasion
|
|
254
258
|
|
|
255
|
-
| Type | get | set |
|
|
256
|
-
| -------- | --- | --- |
|
|
257
|
-
| State | ✅ | ✅ |
|
|
258
|
-
| Computed | ✅ | ❌ |
|
|
259
|
-
| Command | ❌ | ✅ |
|
|
259
|
+
| Type | get | set |
|
|
260
|
+
| -------- | --- | --- |
|
|
261
|
+
| State | ✅ | ✅ |
|
|
262
|
+
| Computed | ✅ | ❌ |
|
|
263
|
+
| Command | ❌ | ✅ |
|
|
260
264
|
|
|
261
265
|
That's it! Next, you can learn how to use CCState in React.
|
|
262
266
|
|
|
@@ -313,13 +317,85 @@ It is easy to see that `userId` passed by the function parameters do not have re
|
|
|
313
317
|
The `Command` has the ability to receive parameters, so we can encapsulate the above example into a `Command`:
|
|
314
318
|
|
|
315
319
|
```javascript
|
|
316
|
-
const getUser$ = command(({ set, get }, userId,
|
|
320
|
+
const getUser$ = command(({ set, get }, userId, signal) => {
|
|
317
321
|
return fetch(`/api/users/${userId}`, { signal });
|
|
318
322
|
});
|
|
319
323
|
```
|
|
320
324
|
|
|
321
325
|
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
326
|
|
|
327
|
+
### Robust Async Flow Cancellation
|
|
328
|
+
|
|
329
|
+
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.
|
|
330
|
+
|
|
331
|
+
#### Rule 1: await in commands must perform abort signal check
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const updateUser$ = command(async ({ get }, signal: AbortSignal) => {
|
|
335
|
+
const currentUser = await get(currentUser$); // Here we assume currentUser$ returns a Promise<User>
|
|
336
|
+
signal.throwIfAbort(); // Must perform throwIfAbort check on the line after await
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
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.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const updateUser$ = command(async ({ get, set }, name: string, signal: AbortSignal) => {
|
|
344
|
+
const currentUser = await get(currentUser$);
|
|
345
|
+
signal.throwIfAbort();
|
|
346
|
+
|
|
347
|
+
await set(updateUserName$, currentUser.id, name, signal); // Here we pass signal to the next command, so the current command doesn't need to check
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`Computed` doesn't need such checks because Computed is side-effect free.
|
|
352
|
+
|
|
353
|
+
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.
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
store.watch(
|
|
357
|
+
(get, { signal }) => {
|
|
358
|
+
void (async () => {
|
|
359
|
+
const user = await get(currentUser$);
|
|
360
|
+
signal.throwIfAbort();
|
|
361
|
+
|
|
362
|
+
syncExternalState(user); // this code will not execute when externalAbortSignal abort
|
|
363
|
+
})();
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
signal: externalAbortSignal,
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Rule 2: Do not intercept AbortError in try/catch
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
const isAbortError = (error: unknown): boolean => {
|
|
375
|
+
if ((error instanceof Error || error instanceof DOMException) && error.name === 'AbortError') {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return false;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
function throwIfAbort(e: unknown) {
|
|
383
|
+
if (isAbortError(e)) {
|
|
384
|
+
throw e;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const updateUser$ = command(async ({ get, set }, name: string, signal: AbortSignal) => {
|
|
389
|
+
// ...
|
|
390
|
+
try {
|
|
391
|
+
await set(updateUserName$, currentUser.id, name, signal);
|
|
392
|
+
} catch (error: unknown) {
|
|
393
|
+
throwIfAbort(error); // do not catch any AbortError
|
|
394
|
+
// .. error handling
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
323
399
|
## Using in React
|
|
324
400
|
|
|
325
401
|
[Using in React](docs/react.md)
|
|
@@ -365,12 +441,8 @@ import { createDebugStore, state, computed, command } from 'ccstate';
|
|
|
365
441
|
const base$ = state(1, { debugLabel: 'base$' });
|
|
366
442
|
const derived$ = computed((get) => get(base$) * 2);
|
|
367
443
|
|
|
368
|
-
const store = createDebugStore([base$, 'derived'], ['set'
|
|
444
|
+
const store = createDebugStore([base$, 'derived'], ['set']); // log set actions
|
|
369
445
|
store.set(base$, 1); // console: SET [V0:base$] 1
|
|
370
|
-
store.sub(
|
|
371
|
-
derived$,
|
|
372
|
-
command(() => void 0),
|
|
373
|
-
); // console: SUB [V0:derived$]
|
|
374
446
|
```
|
|
375
447
|
|
|
376
448
|
## Concept behind CCState
|
|
@@ -397,28 +469,9 @@ Like Jotai, CCState is also an Atom State solution. However, unlike Jotai, CCSta
|
|
|
397
469
|
|
|
398
470
|
### Subscription System
|
|
399
471
|
|
|
400
|
-
CCState's subscription system is different from Jotai's. First,
|
|
472
|
+
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
473
|
|
|
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.
|
|
474
|
+
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
475
|
|
|
423
476
|
In Jotai, there are no restrictions on writing code that uses sub within a sub callback:
|
|
424
477
|
|
|
@@ -432,27 +485,26 @@ store.sub(targetAtom, () => {
|
|
|
432
485
|
});
|
|
433
486
|
```
|
|
434
487
|
|
|
435
|
-
In CCState, we can prevent this situation by moving the `
|
|
488
|
+
In CCState, we can prevent this situation by moving the `Watcher` definition to a separate file and protecting the Store.
|
|
436
489
|
|
|
437
490
|
```typescript
|
|
438
491
|
// main.ts
|
|
439
|
-
import {
|
|
440
|
-
import { foo$ } from './states
|
|
492
|
+
import { fooWatcher } from './watchers';
|
|
441
493
|
|
|
442
494
|
function initApp() {
|
|
443
|
-
const store = createStore()
|
|
444
|
-
store.
|
|
495
|
+
const store = createStore();
|
|
496
|
+
store.watch(fooWatcher);
|
|
445
497
|
// do not expose store to outside
|
|
446
498
|
}
|
|
447
499
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
500
|
+
// watchers.ts
|
|
501
|
+
export const fooWatcher = (get) => {
|
|
502
|
+
// there is no way to use store watch here
|
|
503
|
+
get(foo$);
|
|
504
|
+
};
|
|
453
505
|
```
|
|
454
506
|
|
|
455
|
-
Therefore, in CCState, the capability of `
|
|
507
|
+
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
508
|
|
|
457
509
|
### Avoid `useEffect` in React
|
|
458
510
|
|
|
@@ -579,12 +631,11 @@ function App() {
|
|
|
579
631
|
|
|
580
632
|
// A more recommended approach is to enable side effects outside of React
|
|
581
633
|
// main.ts
|
|
582
|
-
store.
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}),
|
|
634
|
+
store.watch(
|
|
635
|
+
// watch is always effect-less to any State
|
|
636
|
+
(get) => {
|
|
637
|
+
get(count$); // ... onCount
|
|
638
|
+
},
|
|
588
639
|
);
|
|
589
640
|
store.set(setupTimer$); // must setup effect explicitly
|
|
590
641
|
|
|
@@ -734,13 +785,10 @@ At this point, the dependency array of `derived$` is `[branch$]`, because `deriv
|
|
|
734
785
|
store.set(branch$, 'D'); // will not trigger derived$'s read, until next get(derived$)
|
|
735
786
|
```
|
|
736
787
|
|
|
737
|
-
Once we mount `derived$` by `
|
|
788
|
+
Once we mount `derived$` by `watch`, all its direct and indirect dependencies will enter the _mounted_ state.
|
|
738
789
|
|
|
739
790
|
```typescript
|
|
740
|
-
store.
|
|
741
|
-
derived$,
|
|
742
|
-
command(() => void 0),
|
|
743
|
-
);
|
|
791
|
+
store.watch((get) => get(derived$));
|
|
744
792
|
```
|
|
745
793
|
|
|
746
794
|
The mount graph in CCState is `[derived$, [branch$]]`. When `branch$` is reset, `derived$` will be re-evaluated immediately, and all subscribers will be notified.
|