nanoeffects 0.28.0 → 0.29.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/dist/{action-creator-CcAEvUnw.d.ts → action-creator-jeEqIKgK.d.ts} +34 -32
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-0uYn_kRj.js +1 -0
- package/dist/context-DtUnzC-1.js +1 -0
- package/dist/context-types-Bme1mzkn.d.ts +31 -0
- package/dist/context.d.ts +8 -3
- package/dist/context.js +1 -1
- package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +3 -3
- package/dist/match.js +1 -1
- package/dist/{module-C_oEXw1n.d.ts → module-BWMW83qk.d.ts} +38 -38
- package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-xpQ_I2h8.d.ts} +34 -32
- package/dist/notrace/actions.d.ts +52 -52
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-lJjjd-SN.d.ts → plugin-6LhNx0th.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +43 -1
- package/dist/observable.js +1 -1
- package/dist/onAbort-BgMfspK4.js +1 -0
- package/dist/oop.d.ts +3 -3
- package/dist/oop.js +1 -1
- package/dist/{plugin-CuvIgjnX.d.ts → plugin-BaErevLy.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +9 -9
- package/dist/signal-field-key-CWT-3OXC.js +1 -0
- package/dist/signal.d.ts +34 -6
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +20 -5
- package/dist/trace.js +1 -1
- package/dist/{types-FV8fuvI9.d.ts → types-D5xsR37j.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-BZVRBUOm.d.ts} +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-C6BIiPIi.js} +1 -1
- package/docs/README.md +13 -1
- package/docs/actions.md +27 -1
- package/docs/chat-example.md +12 -0
- package/docs/chat-widget.md +7 -7
- package/docs/context.md +33 -1
- package/docs/effects.md +20 -9
- package/docs/guide.md +20 -2
- package/docs/migrating-from-effector.md +11 -0
- package/docs/observable.md +43 -0
- package/docs/oop.md +17 -0
- package/docs/package.md +22 -13
- package/docs/plugins.md +69 -11
- package/docs/query-libraries.md +273 -5
- package/docs/react.md +12 -2
- package/docs/scope.md +14 -2
- package/docs/semantics.md +10 -0
- package/docs/server.md +3 -3
- package/docs/stability.md +2 -2
- package/docs/tracing.md +74 -6
- package/docs/typescript.md +1 -1
- package/llms-full.txt +671 -66
- package/llms.txt +7 -7
- package/package.json +4 -4
- package/skills/nanoeffects/SKILL.md +2 -2
- package/dist/bus-DlpEluzd.js +0 -1
- package/dist/context-UcmrAIhP.js +0 -1
- package/dist/context-types-hCKgU8sC.d.ts +0 -14
package/llms-full.txt
CHANGED
|
@@ -10,7 +10,7 @@ Rules that generated code often gets wrong:
|
|
|
10
10
|
- Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
|
|
11
11
|
- Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
|
|
12
12
|
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/svelte` (`sveltePlugin`, adds `bus.eventStore`; needs Svelte), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
13
|
-
- `bus.emit(
|
|
13
|
+
- `bus.emit(action(payload))` runs the action's handler and returns its result. Plain events return nothing.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
Source: docs/guide.md
|
|
@@ -28,6 +28,23 @@ Climb one step at a time: small bus → policies → effects → handlers and mo
|
|
|
28
28
|
|
|
29
29
|
Runnable code for every step: [`examples/guide.ts`](../examples/guide.ts).
|
|
30
30
|
|
|
31
|
+
## On this page
|
|
32
|
+
|
|
33
|
+
- [1. Create a bus](#1-create-a-bus)
|
|
34
|
+
- [Listen to every event](#listen-to-every-event)
|
|
35
|
+
- [Pick events with a function](#pick-events-with-a-function)
|
|
36
|
+
- [2. Use the handler context](#2-use-the-handler-context)
|
|
37
|
+
- [3. Abort the previous run with `latest`](#3-abort-the-previous-run-with-latest)
|
|
38
|
+
- [4. Pick another policy when overlap differs](#4-pick-another-policy-when-overlap-differs)
|
|
39
|
+
- [5. Add retry and timeout](#5-add-retry-and-timeout)
|
|
40
|
+
- [6. Run work in parallel or wait for an event](#6-run-work-in-parallel-or-wait-for-an-event)
|
|
41
|
+
- [7. Name events three ways](#7-name-events-three-ways)
|
|
42
|
+
- [8. Bind a handler to an action later](#8-bind-a-handler-to-an-action-later)
|
|
43
|
+
- [9. Mount a domain as a module](#9-mount-a-domain-as-a-module)
|
|
44
|
+
- [10. Add types when you want checking (optional)](#10-add-types-when-you-want-checking-optional)
|
|
45
|
+
- [11. Isolate errors and warn on typos](#11-isolate-errors-and-warn-on-typos)
|
|
46
|
+
- [Where next](#where-next)
|
|
47
|
+
|
|
31
48
|
## 1. Create a bus
|
|
32
49
|
|
|
33
50
|
`on` subscribes. `emit` calls the subscribers. `on` returns the unsubscribe function.
|
|
@@ -289,10 +306,11 @@ import { all, delay, race, take } from "nanoeffects/effects";
|
|
|
289
306
|
const bus = createBus();
|
|
290
307
|
|
|
291
308
|
// all — run together; one failure cancels the rest
|
|
309
|
+
// child — the context of that starter: it has the `signal`, so the request stops with the group
|
|
292
310
|
bus.on("search", async (id, ctx) => {
|
|
293
311
|
const { user, posts } = await all(ctx, {
|
|
294
|
-
user: (
|
|
295
|
-
posts: (
|
|
312
|
+
user: (child) => api.user(id, child),
|
|
313
|
+
posts: (child) => api.posts(id, child),
|
|
296
314
|
});
|
|
297
315
|
ctx.emit("loaded", { user, posts });
|
|
298
316
|
});
|
|
@@ -696,24 +714,33 @@ Source: docs/package.md
|
|
|
696
714
|
|
|
697
715
|
Imports are split so you only load what you use. `createBus` stays small; the rest is another entry.
|
|
698
716
|
|
|
717
|
+
## Start with these three
|
|
718
|
+
|
|
699
719
|
| Import | What is in it |
|
|
700
720
|
| ------ | ------------- |
|
|
701
721
|
| `nanoeffects` | `createBus`, the types |
|
|
702
722
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
703
723
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
|
710
|
-
|
|
|
711
|
-
| `nanoeffects/
|
|
712
|
-
| `nanoeffects/
|
|
713
|
-
| `nanoeffects/
|
|
714
|
-
| `nanoeffects/
|
|
715
|
-
| `nanoeffects/
|
|
716
|
-
| `nanoeffects/
|
|
724
|
+
|
|
725
|
+
## Add the rest when you need it
|
|
726
|
+
|
|
727
|
+
Each row is optional. The "when" column says what need brings you to it.
|
|
728
|
+
|
|
729
|
+
| Import | What is in it | When |
|
|
730
|
+
| ------ | ------------- | ---- |
|
|
731
|
+
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` | `emit` must return a result, or domain logic leaves the UI |
|
|
732
|
+
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events | `on` must take a function that picks events |
|
|
733
|
+
| `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value | a store or client must be on every task |
|
|
734
|
+
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` | an outside `AbortSignal` (request, React Query) must stop tasks |
|
|
735
|
+
| `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` | you write classes, not functions |
|
|
736
|
+
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) | one action or module needs its own fields |
|
|
737
|
+
| `nanoeffects/redux` | `createEffectsMiddleware` | you already use Redux |
|
|
738
|
+
| `nanoeffects/observable` | `createObserve`, `Subject` | you need an Observable (RxJS) |
|
|
739
|
+
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) | React components |
|
|
740
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) | Svelte components |
|
|
741
|
+
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) | Vue components |
|
|
742
|
+
| `nanoeffects/dev` | `devPlugin` (no-op in production) | you look for leaks in development |
|
|
743
|
+
| `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
717
744
|
|
|
718
745
|
The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
|
|
719
746
|
|
|
@@ -804,8 +831,8 @@ import { race, retry } from "nanoeffects/effects";
|
|
|
804
831
|
const bus = createBus();
|
|
805
832
|
|
|
806
833
|
bus.on("open", async (id, ctx) => {
|
|
807
|
-
// with ctx: you pass the context, the function
|
|
808
|
-
const user = await retry(ctx, (
|
|
834
|
+
// with ctx: you pass the context, and the function takes the signal from it
|
|
835
|
+
const user = await retry(ctx, () => fetchUser(id, { signal: ctx.signal }), { times: 3 });
|
|
809
836
|
|
|
810
837
|
// as a starter: a group (race, all, allSettled, timeout) calls it with its child
|
|
811
838
|
const result = await race(ctx, {
|
|
@@ -828,12 +855,52 @@ Source: docs/plugins.md
|
|
|
828
855
|
|
|
829
856
|
Optional. A plugin adds one thing to a bus: fields on `ctx`, a method, a registry of handlers, diagnostics. Read this page when you want to know which plugins exist, what each one gives you, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
|
|
830
857
|
|
|
858
|
+
## On this page
|
|
859
|
+
|
|
860
|
+
- [Use a plugin](#use-a-plugin)
|
|
861
|
+
- [Pick a plugin by task](#pick-a-plugin-by-task)
|
|
862
|
+
- [Main plugins](#main-plugins)
|
|
863
|
+
- [Put a store or a client on every handler: `contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin)
|
|
864
|
+
- [Use `race` and `delay` without `ctx`: `effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin)
|
|
865
|
+
- [Bind or replace a handler: `actionsPlugin`](#bind-or-replace-a-handler-actionsplugin)
|
|
866
|
+
- [Mount a domain as one unit: `modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin)
|
|
867
|
+
- [Extra plugins](#extra-plugins)
|
|
868
|
+
- [Give one action its own fields: `scopePlugin`](#give-one-action-its-own-fields-scopeplugin)
|
|
869
|
+
- [Tie an emit or a listener to an outside signal: `signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin)
|
|
870
|
+
- [Name the origin and the flow of a trace: `tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin)
|
|
871
|
+
- [Listen to a group of events: `matchPlugin`](#listen-to-a-group-of-events-matchplugin)
|
|
872
|
+
- [Hand the bus to a state manager: `emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin)
|
|
873
|
+
- [Tell the bus when a React value changes: `reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin)
|
|
874
|
+
- [Find leaks while you develop: `devPlugin`](#find-leaks-while-you-develop-devplugin)
|
|
875
|
+
- [Put them together](#put-them-together)
|
|
876
|
+
- [Use them in any order](#use-them-in-any-order)
|
|
877
|
+
- [See what is not a plugin](#see-what-is-not-a-plugin)
|
|
878
|
+
- [Write your own plugin](#write-your-own-plugin)
|
|
879
|
+
|
|
831
880
|
## Use a plugin
|
|
832
881
|
|
|
833
882
|
A plugin is a function of the bus. `bus.use(plugin)` calls it with the bus and returns whatever the plugin returns. Most plugins return the same bus, so you can chain them: `createBus().use(a).use(b)`. A few return something of their own (a registry, an app, a `disconnect` function): keep it.
|
|
834
883
|
|
|
835
884
|
In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type of the bus with its new fields is the type they return (`const bus = createBus().use(contextPlugin({ store }))`).
|
|
836
885
|
|
|
886
|
+
## Pick a plugin by task
|
|
887
|
+
|
|
888
|
+
Find your need in the left column. No row fits: you need no plugin.
|
|
889
|
+
|
|
890
|
+
| I need | Use |
|
|
891
|
+
| ------ | --- |
|
|
892
|
+
| A store, client or logger on every handler | [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) |
|
|
893
|
+
| `race`, `all` or `delay` without passing `ctx` | [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) |
|
|
894
|
+
| A test that replaces a handler | [`actionsPlugin`](#bind-or-replace-a-handler-actionsplugin) |
|
|
895
|
+
| One domain mounted and unmounted as a unit | [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) |
|
|
896
|
+
| Fields for one action or module only | [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) |
|
|
897
|
+
| Tasks that stop when a request, socket or query is cancelled | [`signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin) |
|
|
898
|
+
| One listener for a group of events | [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) |
|
|
899
|
+
| A callback for a state manager that emits | [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) |
|
|
900
|
+
| A React component that sends its value to the bus | [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) |
|
|
901
|
+
| Names in the trace | [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) |
|
|
902
|
+
| Warnings about leaks while you develop | [`devPlugin`](#find-leaks-while-you-develop-devplugin) |
|
|
903
|
+
|
|
837
904
|
## Main plugins
|
|
838
905
|
|
|
839
906
|
These four cover what most apps need: shared dependencies, effects, handlers and modules. Start here.
|
|
@@ -859,6 +926,8 @@ const bus = createBus().use(contextPlugin({ store }));
|
|
|
859
926
|
bus.on("save", (draft, { store }) => store.put(draft));
|
|
860
927
|
```
|
|
861
928
|
|
|
929
|
+
A client that must stop with the task, such as an `axios` instance, goes in as `fromSignal((signal) => axios.create({ signal }))` from `nanoeffects/signal`: it is built from the signal of each task, when the task reads it. More in [Build a field from the signal of each task](context.md#build-a-field-from-the-signal-of-each-task).
|
|
930
|
+
|
|
862
931
|
### Use `race` and `delay` without `ctx`: `effectsPlugin`
|
|
863
932
|
|
|
864
933
|
Effects such as `race`, `all`, `take` and `delay` are bound to the task, so a handler calls `race({ … })` and not `race(ctx, { … })`. Your own functions that take `{ signal }` first work too. More in [Effects](effects.md#put-selected-effects-on-ctx).
|
|
@@ -874,7 +943,7 @@ const bus = createBus().use(effects);
|
|
|
874
943
|
|
|
875
944
|
bus.on("load", async (id, { race, delay }) => {
|
|
876
945
|
const result = await race({
|
|
877
|
-
data: (
|
|
946
|
+
data: (child) => api.get(id, child),
|
|
878
947
|
timeout: () => delay(5000),
|
|
879
948
|
});
|
|
880
949
|
});
|
|
@@ -911,7 +980,7 @@ const bus = createBus();
|
|
|
911
980
|
// modulePlugin — mounts userModule at the end of this block, or at the first use if that is earlier, and returns the app
|
|
912
981
|
const app = bus.use(modulePlugin([userModule]));
|
|
913
982
|
|
|
914
|
-
app.unmount(userModule); // its listeners, bound
|
|
983
|
+
app.unmount(userModule); // its listeners, bound actions and tasks are gone
|
|
915
984
|
```
|
|
916
985
|
|
|
917
986
|
## Extra plugins
|
|
@@ -921,7 +990,7 @@ Add one of these when you meet its need. None of them is required.
|
|
|
921
990
|
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
922
991
|
| ------ | ----------------- | ------ | ----------------- |
|
|
923
992
|
| [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) | Its own fields for one action, job or module, and a way to replace them in one `emit`. | `nanoeffects/scope` | The same bus |
|
|
924
|
-
| [`signalPlugin`](#tie-an-emit-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
|
|
993
|
+
| [`signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks, and `on(event, handler, { signal })` that removes the listener, when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
|
|
925
994
|
| [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) | `on` that listens to a group of events which a function picks. | `nanoeffects/match` | The same bus, typed with the new overloads |
|
|
926
995
|
| [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) | A callback that emits an action, to give to a state manager. | `nanoeffects/actions` | The same bus, with `emitter` |
|
|
927
996
|
| [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) | `traceId` and `traceGroupId` for a trace: the root of an emitter, a flow, a named listener. | `nanoeffects/trace` | The same bus |
|
|
@@ -937,10 +1006,10 @@ import { createBus } from "nanoeffects";
|
|
|
937
1006
|
import { createAction } from "nanoeffects/actions";
|
|
938
1007
|
import { scopePlugin } from "nanoeffects/scope";
|
|
939
1008
|
|
|
940
|
-
// scopePlugin — makes the scope of
|
|
1009
|
+
// scopePlugin — makes the scope of an action work on this bus
|
|
941
1010
|
const bus = createBus().use(scopePlugin());
|
|
942
1011
|
|
|
943
|
-
// the last argument: fields of this
|
|
1012
|
+
// the last argument: fields of this action only
|
|
944
1013
|
const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
|
|
945
1014
|
api,
|
|
946
1015
|
});
|
|
@@ -949,25 +1018,41 @@ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
|
949
1018
|
bus.emit(getUser("1"), { api: fakeApi }); // a fake api for this call only
|
|
950
1019
|
```
|
|
951
1020
|
|
|
952
|
-
### Tie an emit to an outside signal: `signalPlugin`
|
|
1021
|
+
### Tie an emit or a listener to an outside signal: `signalPlugin`
|
|
953
1022
|
|
|
954
|
-
A library such as React Query owns the request and gives you an `AbortSignal`. `emit` takes it as `{ signal }` and aborts the tasks it starts when the signal aborts, so `timeout`, `retry` and the request stop too. More in [Use nanoeffects with React Query and
|
|
1023
|
+
A library such as React Query owns the request and gives you an `AbortSignal`. `emit` takes it as `{ signal }` and aborts the tasks it starts when the signal aborts, so `timeout`, `retry` and the request stop too. More in [Use nanoeffects with React Query, SWR and RTK Query](query-libraries.md).
|
|
955
1024
|
|
|
956
1025
|
```js
|
|
957
1026
|
import { createBus } from "nanoeffects";
|
|
958
1027
|
import { createAction } from "nanoeffects/actions";
|
|
959
1028
|
import { signalPlugin } from "nanoeffects/signal";
|
|
960
1029
|
|
|
961
|
-
// signalPlugin — emit takes { signal } for
|
|
1030
|
+
// signalPlugin — emit takes { signal } for an action, a job or an action
|
|
962
1031
|
const bus = createBus().use(signalPlugin());
|
|
963
1032
|
|
|
964
1033
|
const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
|
|
965
1034
|
|
|
966
1035
|
const controller = new AbortController();
|
|
967
|
-
bus.emit(getUser("1"), { signal: controller.signal }); // runs the
|
|
1036
|
+
bus.emit(getUser("1"), { signal: controller.signal }); // runs the action
|
|
968
1037
|
controller.abort(); // ctx.signal of the handler aborts with it
|
|
969
1038
|
```
|
|
970
1039
|
|
|
1040
|
+
The same signal can end a listener. `on` and `once` take `{ signal }` as their options, as `addEventListener` does, so code that has a signal keeps no `unbind` function. The listener is removed when the signal aborts; a handler that already runs is not aborted. A signal that is aborted already adds no listener.
|
|
1041
|
+
|
|
1042
|
+
```js
|
|
1043
|
+
import { createBus } from "nanoeffects";
|
|
1044
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
1045
|
+
|
|
1046
|
+
// signalPlugin — on and once take { signal } in their options
|
|
1047
|
+
const bus = createBus().use(signalPlugin());
|
|
1048
|
+
|
|
1049
|
+
const controller = new AbortController();
|
|
1050
|
+
bus.on("saved", (draft) => console.log(draft), { signal: controller.signal }); // listens until the abort
|
|
1051
|
+
controller.abort(); // the listener is gone: bus.get("saved") is empty
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
Without the plugin the `signal` option is ignored and the listener stays. `off(event, handler)` removes a listener but does not end its link to the signal; call the function that `on` returned for that.
|
|
1055
|
+
|
|
971
1056
|
### Name the origin and the flow of a trace: `tracePlugin`
|
|
972
1057
|
|
|
973
1058
|
With a tracer on the bus, the plugin lets a trace say which subscription began a chain (`traceId` of `useEmitter` and `bus.emitter`), follow one flow by a `traceGroupId`, and show which listener ran (`bus.on(event, handler, { traceId })`). Without the plugin the options are ignored. More in [Tracing](tracing.md#show-which-subscription-began-a-chain).
|
|
@@ -1080,7 +1165,7 @@ import { scopePlugin } from "nanoeffects/scope";
|
|
|
1080
1165
|
export const bus = createBus()
|
|
1081
1166
|
.use(contextPlugin({ store })) // ctx.store on every task
|
|
1082
1167
|
.use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
|
|
1083
|
-
.use(scopePlugin()) // scopes for
|
|
1168
|
+
.use(scopePlugin()) // scopes for actions, jobs, modules and emit
|
|
1084
1169
|
.use(matchPlugin()) // bus.on takes a function that picks the events
|
|
1085
1170
|
.use(emitterPlugin()); // bus.emitter(creator)
|
|
1086
1171
|
|
|
@@ -1121,7 +1206,7 @@ These connect the bus to something, but are not used with `bus.use`:
|
|
|
1121
1206
|
- `createEffectsMiddleware(bus, { filter })` from `nanoeffects/redux` is a Redux middleware for a bus that exists. See [Stores](stores.md#wire-redux-toolkit-to-a-bus).
|
|
1122
1207
|
- `createBus({ trace })` takes a tracer from `nanoeffects/trace`. See [Tracing](tracing.md).
|
|
1123
1208
|
- `extendBus(bus, Base)` from `nanoeffects/oop` gives a class the methods of a bus, with the subscriptions of each instance ended by `dispose()`. It reads the bus as it is, so use the plugins first. See [Use the bus from a class](oop.md).
|
|
1124
|
-
- `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable. See [Observable](observable.md).
|
|
1209
|
+
- `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable, and `events(bus, event)` into a `for await` loop. See [Observable](observable.md).
|
|
1125
1210
|
|
|
1126
1211
|
## Write your own plugin
|
|
1127
1212
|
|
|
@@ -1700,6 +1785,18 @@ Use this page for retry, timeouts, waiting, or parallel work — not as the firs
|
|
|
1700
1785
|
|
|
1701
1786
|
An abort only raises a flag. Your code decides to stop. Start with `retry` / `timeout` on a handler (see [Guide §5](guide.md#5-add-retry-and-timeout)).
|
|
1702
1787
|
|
|
1788
|
+
## On this page
|
|
1789
|
+
|
|
1790
|
+
- [Wrap a handler with retry and timeout](#wrap-a-handler-with-retry-and-timeout)
|
|
1791
|
+
- [Options for retry, timeout, and poll](#options-for-retry-timeout-and-poll)
|
|
1792
|
+
- [Back off between retries](#back-off-between-retries)
|
|
1793
|
+
- [Put selected effects on `ctx`](#put-selected-effects-on-ctx)
|
|
1794
|
+
- [Tie a promise to the task signal](#tie-a-promise-to-the-task-signal)
|
|
1795
|
+
- [Run cleanup on abort](#run-cleanup-on-abort)
|
|
1796
|
+
- [Wait for the next event with `take`](#wait-for-the-next-event-with-take)
|
|
1797
|
+
- [Run several starters together](#run-several-starters-together)
|
|
1798
|
+
- [Pass `ctx` outside a task](#pass-ctx-outside-a-task)
|
|
1799
|
+
|
|
1703
1800
|
## Wrap a handler with retry and timeout
|
|
1704
1801
|
|
|
1705
1802
|
`retry`, `timeout`, and `poll` have two forms:
|
|
@@ -1745,9 +1842,7 @@ bus.on("load", async (id, ctx) => {
|
|
|
1745
1842
|
() => fetchUser(id, { signal: ctx.signal }),
|
|
1746
1843
|
{ times: 4 },
|
|
1747
1844
|
);
|
|
1748
|
-
const data = await timeout(ctx, 5000, (
|
|
1749
|
-
return fetchData(id, { signal });
|
|
1750
|
-
});
|
|
1845
|
+
const data = await timeout(ctx, 5000, (child) => fetchData(id, child));
|
|
1751
1846
|
await poll(ctx, () => refresh(), 10_000);
|
|
1752
1847
|
ctx.emit("ready", { user, data });
|
|
1753
1848
|
});
|
|
@@ -1856,7 +1951,8 @@ const bus = createBus().use(effects);
|
|
|
1856
1951
|
bus.on("load", async (id, { race, all, delay }) => {
|
|
1857
1952
|
// race — same as race(ctx, …)
|
|
1858
1953
|
const result = await race({
|
|
1859
|
-
|
|
1954
|
+
// child — the context of this starter; it has the `signal`, so the request stops with the race
|
|
1955
|
+
data: (child) => fetchData(id, child),
|
|
1860
1956
|
// a starter gets its own child with the same bound effects, so delay(5000) stops with the race
|
|
1861
1957
|
timeout: ({ delay }) => delay(5000),
|
|
1862
1958
|
});
|
|
@@ -1864,8 +1960,8 @@ bus.on("load", async (id, { race, all, delay }) => {
|
|
|
1864
1960
|
|
|
1865
1961
|
// all — same as all(ctx, …)
|
|
1866
1962
|
const { user, posts } = await all({
|
|
1867
|
-
user: (
|
|
1868
|
-
posts: (
|
|
1963
|
+
user: (child) => fetchUser(id, child),
|
|
1964
|
+
posts: (child) => fetchPosts(id, child),
|
|
1869
1965
|
});
|
|
1870
1966
|
});
|
|
1871
1967
|
```
|
|
@@ -1988,8 +2084,8 @@ const bus = createBus();
|
|
|
1988
2084
|
|
|
1989
2085
|
bus.on("open", async (id, ctx) => {
|
|
1990
2086
|
const { user, posts } = await allSettled(ctx, {
|
|
1991
|
-
user: (
|
|
1992
|
-
posts: (
|
|
2087
|
+
user: (child) => fetchUser(id, child),
|
|
2088
|
+
posts: (child) => fetchPosts(id, child),
|
|
1993
2089
|
});
|
|
1994
2090
|
|
|
1995
2091
|
if (user.status === "fulfilled") {
|
|
@@ -2095,7 +2191,7 @@ bus.on("sync", async (ids, ctx) => {
|
|
|
2095
2191
|
The result has the same order and shape as without the option. If one starter of `all` fails, the running ones are cancelled and the waiting ones are never called. `allSettled` goes on after a failure. When `ctx` aborts, both stop and start nothing more. A value that is not a positive integer (`0`, `-1`, `NaN`, `1.5`) throws a `RangeError` before anything starts; `Infinity` means no limit. `race` has no `concurrency`: it ends at the first result, so a starter that waited for a free slot would never run. The form without `ctx` takes no options, because a second argument would look like the form with `ctx`, so a group starter inside another group takes none; write `(child) => all(child, effects, { concurrency: 5 })` there.
|
|
2096
2192
|
|
|
2097
2193
|
|
|
2098
|
-
**A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options
|
|
2194
|
+
**A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options, and `child` has a `signal`, so pass it as the options: `(child) => fetchUser(id, child)` (`fetch` reads only `signal` from it). The same holds for `all`, `allSettled` and `timeout`.
|
|
2099
2195
|
|
|
2100
2196
|
If `ctx` is `{}` or a bare `{ signal }` (outside a task), `child` has only `signal` and `abort`, so it fits `delay` and `poll` but not `take`, which needs a bus.
|
|
2101
2197
|
|
|
@@ -2160,6 +2256,32 @@ A chat with two entities and three features, jobs under policies and effects, an
|
|
|
2160
2256
|
|
|
2161
2257
|
A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
|
|
2162
2258
|
|
|
2259
|
+
## Pick the word: event, action, job, module
|
|
2260
|
+
|
|
2261
|
+
Four names, one idea each. Take the first row that fits.
|
|
2262
|
+
|
|
2263
|
+
| Word | What it is | Use it when |
|
|
2264
|
+
| ---- | ---------- | ----------- |
|
|
2265
|
+
| Event | A name and a payload; `createAction("x")` with no handler. | The UI or a handler tells that something happened. |
|
|
2266
|
+
| Action with a handler | The same, plus work: `createAction("x", handler)`. `emit` returns the result. | The caller needs a value back. |
|
|
2267
|
+
| Job | An action with a handler that also emits `pending`, `success`, `fail` and `cancel`. | The UI shows a loading state. |
|
|
2268
|
+
| Module | A group of listeners, handlers and tasks, mounted and unmounted with one call. | One domain must start and stop as a unit. |
|
|
2269
|
+
|
|
2270
|
+
Start with events. Add a handler when you need a result, a job when you need a status, a module when you need a unit.
|
|
2271
|
+
|
|
2272
|
+
## On this page
|
|
2273
|
+
|
|
2274
|
+
- [Pick the word: event, action, job, module](#pick-the-word-event-action-job-module)
|
|
2275
|
+
- [Return a value from `emit` with a handler](#return-a-value-from-emit-with-a-handler)
|
|
2276
|
+
- [Describe events, handlers and jobs](#describe-events-handlers-and-jobs)
|
|
2277
|
+
- [Actions](#actions)
|
|
2278
|
+
- [Bind a handler later](#bind-a-handler-later)
|
|
2279
|
+
- [Say what a bound handler returns](#say-what-a-bound-handler-returns)
|
|
2280
|
+
- [Jobs](#jobs)
|
|
2281
|
+
- [Show a loading state with a job](#show-a-loading-state-with-a-job)
|
|
2282
|
+
- [Mount domains with modules](#mount-domains-with-modules)
|
|
2283
|
+
- [Give an action or a module its own fields](#give-an-action-or-a-module-its-own-fields)
|
|
2284
|
+
|
|
2163
2285
|
## Return a value from `emit` with a handler
|
|
2164
2286
|
|
|
2165
2287
|
Give `createAction` a handler, and `emit` runs it and returns its result. There is one creator for an event and for an action with work behind it; the handler is the only difference.
|
|
@@ -2314,7 +2436,7 @@ On a bus without an event map you can say it where you call: `const id = bus.emi
|
|
|
2314
2436
|
- `pending` / `success` / `fail` / `cancel` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/pending`, `<type>/success`, `<type>/fail` or `<type>/cancel`.
|
|
2315
2437
|
- `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, give `createAction` the handler.
|
|
2316
2438
|
- Outcome actions are emitted as the task that ran the job (also for a handler bound with `actions.handle`). `success` and `fail` are not sent after abort; `cancel` is, at the moment of the abort.
|
|
2317
|
-
- A job without a handler is not an outcome: `emit` throws `No handler for
|
|
2439
|
+
- A job without a handler is not an outcome: `emit` throws `No handler for action "..."` and nothing is sent. An action without a handler is an event and does not throw.
|
|
2318
2440
|
- Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
|
|
2319
2441
|
- The outcomes are events like any other: with `onUnhandled` set on the bus, it is called for a `pending` or a `cancel` that nobody listens to.
|
|
2320
2442
|
|
|
@@ -3198,11 +3320,24 @@ A handler gets the store from `ctx.store` and calls a mutation, whatever it hand
|
|
|
3198
3320
|
---
|
|
3199
3321
|
Source: docs/query-libraries.md
|
|
3200
3322
|
|
|
3201
|
-
# Use nanoeffects with React Query and
|
|
3323
|
+
# Use nanoeffects with React Query, SWR and RTK Query
|
|
3324
|
+
|
|
3325
|
+
Read this page if you fetch data with [React Query](https://tanstack.com/query) (TanStack Query), [SWR](https://swr.vercel.app) or [RTK Query](https://redux-toolkit.js.org/rtk-query/overview) and want `retry`, `timeout` and the abort of nanoeffects inside the request. It shows one action that the libraries call, then three recipes for the parts around it: a client that stops with the request, a live stream into the cache, and a refetch on a bus event.
|
|
3202
3326
|
|
|
3203
|
-
|
|
3327
|
+
You need a React project, `nanoeffects`, and one of the libraries. `api` below is your own client: replace it with your requests.
|
|
3328
|
+
|
|
3329
|
+
## On this page
|
|
3204
3330
|
|
|
3205
|
-
|
|
3331
|
+
- [Decide who owns what](#decide-who-owns-what)
|
|
3332
|
+
- [Make the action](#make-the-action)
|
|
3333
|
+
- [Call it from React Query](#call-it-from-react-query)
|
|
3334
|
+
- [Call it from SWR](#call-it-from-swr)
|
|
3335
|
+
- [Let the bus know the result](#let-the-bus-know-the-result)
|
|
3336
|
+
- [Call it from RTK Query](#call-it-from-rtk-query)
|
|
3337
|
+
- [Give the client its signal once](#give-the-client-its-signal-once)
|
|
3338
|
+
- [Push a live stream into the cache](#push-a-live-stream-into-the-cache)
|
|
3339
|
+
- [Refetch queries when the bus says so](#refetch-queries-when-the-bus-says-so)
|
|
3340
|
+
- [Know the limits](#know-the-limits)
|
|
3206
3341
|
|
|
3207
3342
|
## Decide who owns what
|
|
3208
3343
|
|
|
@@ -3263,7 +3398,7 @@ import { getUser } from "./user";
|
|
|
3263
3398
|
export function Profile({ id }) {
|
|
3264
3399
|
const { isLoading, data, error } = useQuery({
|
|
3265
3400
|
queryKey: ["user", id],
|
|
3266
|
-
// emit — runs the
|
|
3401
|
+
// emit — runs the action; { signal } ties its task to the signal of React Query
|
|
3267
3402
|
queryFn: ({ signal }) => bus.emit(getUser(id), { signal }),
|
|
3268
3403
|
});
|
|
3269
3404
|
|
|
@@ -3315,9 +3450,264 @@ export const userLoaded = createAction("user/loaded");
|
|
|
3315
3450
|
|
|
3316
3451
|
Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the action with `createJob` here: a job handles the error itself and resolves with `undefined`, so React Query would never see the failure. Use a job when nothing waits for the result and the app shows a loading state from the bus, see [Show a loading state with a job](actions.md#show-a-loading-state-with-a-job).
|
|
3317
3452
|
|
|
3453
|
+
## Call it from RTK Query
|
|
3454
|
+
|
|
3455
|
+
<!-- docs-check: new -->
|
|
3456
|
+
|
|
3457
|
+
RTK Query gives the `queryFn` of an endpoint a `signal` in its second argument, so the call is the same as with React Query: pass it to `emit`. This page does not set up the store; see [Stores](stores.md) for that.
|
|
3458
|
+
|
|
3459
|
+
```js
|
|
3460
|
+
// bus.js
|
|
3461
|
+
import { createBus } from "nanoeffects";
|
|
3462
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
3463
|
+
|
|
3464
|
+
// signalPlugin — lets emit take { signal }
|
|
3465
|
+
export const bus = createBus().use(signalPlugin());
|
|
3466
|
+
```
|
|
3467
|
+
|
|
3468
|
+
```js
|
|
3469
|
+
// user.js
|
|
3470
|
+
import { createAction } from "nanoeffects/actions";
|
|
3471
|
+
|
|
3472
|
+
// createAction with a handler — emit returns what the handler returns
|
|
3473
|
+
export const getUser = createAction("user/get", (id, { signal }) =>
|
|
3474
|
+
api.getUser(id, { signal }),
|
|
3475
|
+
);
|
|
3476
|
+
```
|
|
3477
|
+
|
|
3478
|
+
```js
|
|
3479
|
+
// api-slice.js
|
|
3480
|
+
import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react";
|
|
3481
|
+
import { bus } from "./bus";
|
|
3482
|
+
import { getUser } from "./user";
|
|
3483
|
+
|
|
3484
|
+
export const userApi = createApi({
|
|
3485
|
+
reducerPath: "userApi",
|
|
3486
|
+
baseQuery: fakeBaseQuery(), // RTK Query — every endpoint brings its own queryFn
|
|
3487
|
+
endpoints: (build) => ({
|
|
3488
|
+
getUser: build.query({
|
|
3489
|
+
// emit — runs the action; { signal } ties its task to the request of RTK Query
|
|
3490
|
+
queryFn: async (id, { signal }) => {
|
|
3491
|
+
const user = await bus.emit(getUser(id), { signal });
|
|
3492
|
+
return { data: user };
|
|
3493
|
+
},
|
|
3494
|
+
}),
|
|
3495
|
+
}),
|
|
3496
|
+
});
|
|
3497
|
+
```
|
|
3498
|
+
|
|
3499
|
+
## Give the client its signal once
|
|
3500
|
+
|
|
3501
|
+
<!-- docs-check: new -->
|
|
3502
|
+
|
|
3503
|
+
Problem: the action passes `signal` to every call of `api`, and one forgotten `{ signal }` leaves a request running after the query is gone. `fromSignal` builds the client from the signal of each task, so the handler cannot forget it. Use it when many actions share one client. A single action is simpler as it is in [Make the action](#make-the-action).
|
|
3504
|
+
|
|
3505
|
+
`fromSignal` marks a field of `contextPlugin`. The client is made when a handler first reads `ctx.http`, from the `ctx.signal` of that task. More in [Context fields](context.md#build-a-field-from-the-signal-of-each-task).
|
|
3506
|
+
|
|
3507
|
+
```js
|
|
3508
|
+
// bus.js
|
|
3509
|
+
import axios from "axios";
|
|
3510
|
+
import { createBus } from "nanoeffects";
|
|
3511
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
3512
|
+
import { fromSignal, signalPlugin } from "nanoeffects/signal";
|
|
3513
|
+
|
|
3514
|
+
// fromSignal — http is a new axios client for each task, bound to the signal of that task
|
|
3515
|
+
const http = fromSignal((signal) => axios.create({ baseURL: "/api", signal }));
|
|
3516
|
+
|
|
3517
|
+
// contextPlugin — every handler gets ctx.http
|
|
3518
|
+
const context = contextPlugin({ http });
|
|
3519
|
+
|
|
3520
|
+
// signalPlugin — emit takes { signal }, so the signal of the query reaches the task
|
|
3521
|
+
export const bus = createBus().use(signalPlugin()).use(context);
|
|
3522
|
+
```
|
|
3523
|
+
|
|
3524
|
+
```js
|
|
3525
|
+
// user.js
|
|
3526
|
+
import { createAction } from "nanoeffects/actions";
|
|
3527
|
+
|
|
3528
|
+
// the handler never sees a signal: ctx.http stops when the task aborts
|
|
3529
|
+
export const getUser = createAction("user/get", async (id, { http }) => {
|
|
3530
|
+
const response = await http.get(`/users/${id}`);
|
|
3531
|
+
return response.data;
|
|
3532
|
+
});
|
|
3533
|
+
```
|
|
3534
|
+
|
|
3535
|
+
```jsx
|
|
3536
|
+
// Profile.jsx
|
|
3537
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3538
|
+
import { bus } from "./bus";
|
|
3539
|
+
import { getUser } from "./user";
|
|
3540
|
+
|
|
3541
|
+
export function Profile({ id }) {
|
|
3542
|
+
const { data } = useQuery({
|
|
3543
|
+
queryKey: ["user", id],
|
|
3544
|
+
// emit — the signal of React Query aborts the task, and with it the request of ctx.http
|
|
3545
|
+
queryFn: ({ signal }) => bus.emit(getUser(id), { signal }),
|
|
3546
|
+
});
|
|
3547
|
+
|
|
3548
|
+
return <p>{data?.name}</p>;
|
|
3549
|
+
}
|
|
3550
|
+
```
|
|
3551
|
+
|
|
3552
|
+
## Push a live stream into the cache
|
|
3553
|
+
|
|
3554
|
+
<!-- docs-check: new -->
|
|
3555
|
+
|
|
3556
|
+
Problem: prices arrive from a socket, and the component should show the newest one without a refetch. The socket code emits an event on the bus; the component reads those events with `events` and writes each into the cache. Both libraries have an entry point for pushed data: `setQueryData` in React Query and `useSWRSubscription` in SWR.
|
|
3557
|
+
|
|
3558
|
+
`events` gives the payloads to a `for await` loop. `buffer: 1` keeps only the newest price while a write is running. The abort in the cleanup ends the loop and removes the listener. More in [Read an event with a `for await` loop](observable.md#read-an-event-with-a-for-await-loop).
|
|
3559
|
+
|
|
3560
|
+
```js
|
|
3561
|
+
// bus.js
|
|
3562
|
+
import { createBus } from "nanoeffects";
|
|
3563
|
+
|
|
3564
|
+
// createBus — the bus the socket code and the components share
|
|
3565
|
+
export const bus = createBus();
|
|
3566
|
+
```
|
|
3567
|
+
|
|
3568
|
+
```js
|
|
3569
|
+
// prices.js
|
|
3570
|
+
import { createAction } from "nanoeffects/actions";
|
|
3571
|
+
|
|
3572
|
+
// createAction — the event the socket code emits for every new price
|
|
3573
|
+
export const priceChanged = createAction("price/changed");
|
|
3574
|
+
```
|
|
3575
|
+
|
|
3576
|
+
```jsx
|
|
3577
|
+
// Price.jsx
|
|
3578
|
+
import { useEffect } from "react";
|
|
3579
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3580
|
+
import { events } from "nanoeffects/observable";
|
|
3581
|
+
import { bus } from "./bus";
|
|
3582
|
+
import { priceChanged } from "./prices";
|
|
3583
|
+
|
|
3584
|
+
export function Price({ symbol }) {
|
|
3585
|
+
const queryClient = useQueryClient();
|
|
3586
|
+
const { data } = useQuery({
|
|
3587
|
+
queryKey: ["price", symbol],
|
|
3588
|
+
queryFn: () => api.getPrice(symbol), // the first value; the stream keeps it fresh
|
|
3589
|
+
});
|
|
3590
|
+
|
|
3591
|
+
useEffect(() => {
|
|
3592
|
+
const controller = new AbortController();
|
|
3593
|
+
// events — the newest price event, one at a time
|
|
3594
|
+
const prices = events(bus, priceChanged, {
|
|
3595
|
+
signal: controller.signal,
|
|
3596
|
+
buffer: 1,
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
async function follow() {
|
|
3600
|
+
for await (const price of prices) {
|
|
3601
|
+
if (price.symbol === symbol) {
|
|
3602
|
+
queryClient.setQueryData(["price", symbol], price.value); // write into the cache
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
follow().catch((error) => {
|
|
3608
|
+
if (!controller.signal.aborted) throw error; // the abort ends the loop; anything else is a bug
|
|
3609
|
+
});
|
|
3610
|
+
|
|
3611
|
+
return () => controller.abort(); // the component left: the loop ends, the listener goes
|
|
3612
|
+
}, [queryClient, symbol]);
|
|
3613
|
+
|
|
3614
|
+
return <p>{data}</p>;
|
|
3615
|
+
}
|
|
3616
|
+
```
|
|
3617
|
+
|
|
3618
|
+
SWR does the same with `useSWRSubscription` from `swr/subscription`. Its subscribe function gets `next` to push a value and returns the cleanup:
|
|
3619
|
+
|
|
3620
|
+
```jsx
|
|
3621
|
+
// Price.jsx
|
|
3622
|
+
import useSWRSubscription from "swr/subscription";
|
|
3623
|
+
import { events } from "nanoeffects/observable";
|
|
3624
|
+
import { bus } from "./bus";
|
|
3625
|
+
import { priceChanged } from "./prices";
|
|
3626
|
+
|
|
3627
|
+
export function Price({ symbol }) {
|
|
3628
|
+
const { data } = useSWRSubscription(["price", symbol], (_key, { next }) => {
|
|
3629
|
+
const controller = new AbortController();
|
|
3630
|
+
// events — the newest price event, one at a time
|
|
3631
|
+
const prices = events(bus, priceChanged, {
|
|
3632
|
+
signal: controller.signal,
|
|
3633
|
+
buffer: 1,
|
|
3634
|
+
});
|
|
3635
|
+
|
|
3636
|
+
async function follow() {
|
|
3637
|
+
for await (const price of prices) {
|
|
3638
|
+
if (price.symbol === symbol) {
|
|
3639
|
+
next(null, price.value); // SWR — push the value to the hook
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
follow().catch((error) => {
|
|
3645
|
+
if (!controller.signal.aborted) next(error);
|
|
3646
|
+
});
|
|
3647
|
+
|
|
3648
|
+
return () => controller.abort(); // SWR calls this when the last component of the key leaves
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3651
|
+
return <p>{data}</p>;
|
|
3652
|
+
}
|
|
3653
|
+
```
|
|
3654
|
+
|
|
3655
|
+
## Refetch queries when the bus says so
|
|
3656
|
+
|
|
3657
|
+
<!-- docs-check: new -->
|
|
3658
|
+
|
|
3659
|
+
Problem: a save in one part of the app should refresh the lists in another. The save emits an event, and the component that owns the queries listens for it. One `AbortController` ends every listener of the component, instead of one `unbind` for each. For a single listener, `return bus.on(…)` from the effect is shorter.
|
|
3660
|
+
|
|
3661
|
+
This needs `signalPlugin`, because `{ signal }` in the options of `on` is read by the plugin. Without it the listeners stay after the component leaves.
|
|
3662
|
+
|
|
3663
|
+
```js
|
|
3664
|
+
// bus.js
|
|
3665
|
+
import { createBus } from "nanoeffects";
|
|
3666
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
3667
|
+
|
|
3668
|
+
// signalPlugin — on takes { signal } and removes the listener when it aborts
|
|
3669
|
+
export const bus = createBus().use(signalPlugin());
|
|
3670
|
+
```
|
|
3671
|
+
|
|
3672
|
+
```js
|
|
3673
|
+
// user-events.js
|
|
3674
|
+
import { createAction } from "nanoeffects/actions";
|
|
3675
|
+
|
|
3676
|
+
// createAction — events the save code emits
|
|
3677
|
+
export const userSaved = createAction("user/saved");
|
|
3678
|
+
export const userDeleted = createAction("user/deleted");
|
|
3679
|
+
```
|
|
3680
|
+
|
|
3681
|
+
```jsx
|
|
3682
|
+
// UserList.jsx
|
|
3683
|
+
import { useEffect } from "react";
|
|
3684
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3685
|
+
import { bus } from "./bus";
|
|
3686
|
+
import { userDeleted, userSaved } from "./user-events";
|
|
3687
|
+
|
|
3688
|
+
export function UserList() {
|
|
3689
|
+
const queryClient = useQueryClient();
|
|
3690
|
+
const { data } = useQuery({ queryKey: ["users"], queryFn: () => api.listUsers() });
|
|
3691
|
+
|
|
3692
|
+
useEffect(() => {
|
|
3693
|
+
const controller = new AbortController();
|
|
3694
|
+
const { signal } = controller;
|
|
3695
|
+
const refresh = () => queryClient.invalidateQueries({ queryKey: ["users"] });
|
|
3696
|
+
|
|
3697
|
+
// on with { signal } — both listeners go when the component leaves
|
|
3698
|
+
bus.on(userSaved, refresh, { signal });
|
|
3699
|
+
bus.on(userDeleted, refresh, { signal });
|
|
3700
|
+
|
|
3701
|
+
return () => controller.abort();
|
|
3702
|
+
}, [queryClient]);
|
|
3703
|
+
|
|
3704
|
+
return <ul>{data?.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;
|
|
3705
|
+
}
|
|
3706
|
+
```
|
|
3707
|
+
|
|
3318
3708
|
## Know the limits
|
|
3319
3709
|
|
|
3320
|
-
- **Without the plugin, `{ signal }` is ignored
|
|
3710
|
+
- **Without the plugin, `{ signal }` is ignored,** in `emit` and in the options of `on`. The action runs, but the abort does not reach it. Call `bus.use(signalPlugin())` once, where the bus is made.
|
|
3321
3711
|
- **Only the call forms take it.** `bus.emit(getUser(id), { signal })` works; `bus.emit("user/get", id, { signal })` does not.
|
|
3322
3712
|
- **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it. Actions that fetch are async, so this only matters for a sync handler that sets up a callback.
|
|
3323
3713
|
- **Give each request its own signal for an event.** For an action with a handler or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
|
|
@@ -3367,7 +3757,7 @@ The action loads a user. It tries the request up to three times, and gives up on
|
|
|
3367
3757
|
import { createAction } from "nanoeffects/actions";
|
|
3368
3758
|
import { retry, timeout } from "nanoeffects/effects";
|
|
3369
3759
|
|
|
3370
|
-
// createAction —
|
|
3760
|
+
// createAction — an action with a handler: emit returns what the handler returns
|
|
3371
3761
|
export const getUser = createAction("user/get", async (id, ctx) => {
|
|
3372
3762
|
// one try: the request, with the signal of timeout so it can stop it
|
|
3373
3763
|
const load = ({ signal }) => api.getUser(id, { signal });
|
|
@@ -3408,7 +3798,7 @@ app.get("/users/:id", async (request, reply) => {
|
|
|
3408
3798
|
});
|
|
3409
3799
|
|
|
3410
3800
|
try {
|
|
3411
|
-
// emit with { signal } — aborting the controller aborts the
|
|
3801
|
+
// emit with { signal } — aborting the controller aborts the action: timeout, retry and the request
|
|
3412
3802
|
const call = getUser(request.params.id);
|
|
3413
3803
|
return await bus.emit(call, { signal: controller.signal });
|
|
3414
3804
|
} catch (error) {
|
|
@@ -3461,7 +3851,7 @@ import { createAction } from "nanoeffects/actions";
|
|
|
3461
3851
|
import { poll } from "nanoeffects/effects";
|
|
3462
3852
|
import { latest } from "nanoeffects/policies";
|
|
3463
3853
|
|
|
3464
|
-
// createAction —
|
|
3854
|
+
// createAction — an action with a handler
|
|
3465
3855
|
// latest — a new subscription stops the old one; key — but only the old one of the same socket
|
|
3466
3856
|
export const watchPrices = createAction(
|
|
3467
3857
|
"prices/watch",
|
|
@@ -3540,6 +3930,16 @@ Use this page when a React screen starts requests on user input and you need the
|
|
|
3540
3930
|
|
|
3541
3931
|
You need a React project and `nanoeffects` installed. The form talks to your own client, `api` below: replace it with your requests.
|
|
3542
3932
|
|
|
3933
|
+
## On this page
|
|
3934
|
+
|
|
3935
|
+
- [See what goes wrong without a bus](#see-what-goes-wrong-without-a-bus)
|
|
3936
|
+
- [Wire the bus once](#wire-the-bus-once)
|
|
3937
|
+
- [Connect the form](#connect-the-form)
|
|
3938
|
+
- [Stop running work when the form closes](#stop-running-work-when-the-form-closes)
|
|
3939
|
+
- [Show the last value of an event as state](#show-the-last-value-of-an-event-as-state)
|
|
3940
|
+
- [Keep a debounced handler between renders](#keep-a-debounced-handler-between-renders)
|
|
3941
|
+
- [Where to go next](#where-to-go-next)
|
|
3942
|
+
|
|
3543
3943
|
## See what goes wrong without a bus
|
|
3544
3944
|
|
|
3545
3945
|
A registration form has three problems that `useEffect` and `fetch` leave to you:
|
|
@@ -3611,7 +4011,7 @@ The handlers know nothing about React. A test can emit `username/typed` and read
|
|
|
3611
4011
|
|
|
3612
4012
|
<!-- docs-check: continue -->
|
|
3613
4013
|
|
|
3614
|
-
The component keeps what it shows in `useState`. It subscribes in `useEffect` and emits events from its handlers. `bus.on` returns the function that unsubscribes; returning it from the effect removes the listener on unmount.
|
|
4014
|
+
The component keeps what it shows in `useState`. It subscribes in `useEffect` and emits events from its handlers. `bus.on` returns the function that unsubscribes; returning it from the effect removes the listener on unmount. If the component already holds an `AbortSignal`, `bus.on(event, handler, { signal })` ends the listener with it; see [`signalPlugin`](plugins.md#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin).
|
|
3615
4015
|
|
|
3616
4016
|
```jsx
|
|
3617
4017
|
// SignUpForm.jsx
|
|
@@ -3812,7 +4212,7 @@ export const Search = ({ wait }) => {
|
|
|
3812
4212
|
- A policy as a plain function without a bus: [Use a policy without a bus](policies.md#use-a-policy-without-a-bus).
|
|
3813
4213
|
- `retry`, `timeout` and the other effects: [Effects](effects.md).
|
|
3814
4214
|
- A state library instead of `useState`: [Stores](stores.md).
|
|
3815
|
-
- React Query or SWR instead of your own state: [Use nanoeffects with React Query and
|
|
4215
|
+
- React Query or SWR instead of your own state: [Use nanoeffects with React Query, SWR and RTK Query](query-libraries.md).
|
|
3816
4216
|
|
|
3817
4217
|
---
|
|
3818
4218
|
Source: docs/chat-example.md
|
|
@@ -3833,6 +4233,18 @@ npm run dev
|
|
|
3833
4233
|
|
|
3834
4234
|
`npm run dev` starts the server and Vite together. Start it with `CHAOS=off npm run dev` to turn the failures off.
|
|
3835
4235
|
|
|
4236
|
+
## On this page
|
|
4237
|
+
|
|
4238
|
+
- [Read how the server fails](#read-how-the-server-fails)
|
|
4239
|
+
- [See how the files are split](#see-how-the-files-are-split)
|
|
4240
|
+
- [Load a history that must not race](#load-a-history-that-must-not-race)
|
|
4241
|
+
- [Send a message that can fail](#send-a-message-that-can-fail)
|
|
4242
|
+
- [Search where one room is slow](#search-where-one-room-is-slow)
|
|
4243
|
+
- [Wait for a room before marking a result](#wait-for-a-room-before-marking-a-result)
|
|
4244
|
+
- [Poll for new messages](#poll-for-new-messages)
|
|
4245
|
+
- [Know where each value lives](#know-where-each-value-lives)
|
|
4246
|
+
- [Where to go next](#where-to-go-next)
|
|
4247
|
+
|
|
3836
4248
|
## Read how the server fails
|
|
3837
4249
|
|
|
3838
4250
|
The server keeps its data in memory. These rules are on purpose, and they are the same every run, so you can follow each section below by doing what the table says:
|
|
@@ -4105,7 +4517,7 @@ It is a level D example: the logic lives in one module, and the page only mounts
|
|
|
4105
4517
|
| `src/chat-handlers.ts` | `askBot` (`timeout` and `retry` inside) and `watchOperator` (`poll`): plain `async` functions that know nothing about the page. |
|
|
4106
4518
|
| `src/chat-module.ts` | The list of what is alive while the chat is open. |
|
|
4107
4519
|
| `src/panel.ts` | The view: shows the lines, sends the texts, returns one function that removes it. |
|
|
4108
|
-
| `src/main.ts` | The launcher: the only file that knows both the module and the panel. |
|
|
4520
|
+
| `src/main.ts` | The launcher: the only file that knows both the module and the panel; it listens for `closeRequested` only while the chat is open. |
|
|
4109
4521
|
|
|
4110
4522
|
## Bind everything in one module
|
|
4111
4523
|
|
|
@@ -4155,7 +4567,7 @@ Three details that make the cleanup complete:
|
|
|
4155
4567
|
|
|
4156
4568
|
## Open and close from the launcher
|
|
4157
4569
|
|
|
4158
|
-
`app.mount` returns the function that unmounts the module. The launcher puts
|
|
4570
|
+
`app.mount` returns the function that unmounts the module. `bus.on` returns the one that unsubscribes. The launcher puts them together with the one that removes the panel into a single `close`.
|
|
4159
4571
|
|
|
4160
4572
|
<!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
|
|
4161
4573
|
```js
|
|
@@ -4171,7 +4583,10 @@ let close = null; // set while the chat is open
|
|
|
4171
4583
|
const open = () => {
|
|
4172
4584
|
const stopModule = app.mount(chatModule); // subscriptions, polling and Escape start
|
|
4173
4585
|
const stopPanel = renderPanel(document.body);
|
|
4586
|
+
// the module emits closeRequested on Escape; nothing does while the chat is closed
|
|
4587
|
+
const stopEscape = bus.on(closeRequested, () => close?.());
|
|
4174
4588
|
close = () => {
|
|
4589
|
+
stopEscape();
|
|
4175
4590
|
stopPanel();
|
|
4176
4591
|
stopModule(); // everything the module started ends, a running request included
|
|
4177
4592
|
close = null;
|
|
@@ -4185,18 +4600,15 @@ button.addEventListener("click", () => {
|
|
|
4185
4600
|
open();
|
|
4186
4601
|
}
|
|
4187
4602
|
});
|
|
4188
|
-
|
|
4189
|
-
// the launcher stays subscribed: it must hear Escape to close the chat
|
|
4190
|
-
bus.on(closeRequested, () => close?.());
|
|
4191
4603
|
```
|
|
4192
4604
|
|
|
4193
|
-
|
|
4605
|
+
While the chat is closed the launcher holds no subscription at all: it only has the click listener of the button, which is a DOM listener, not a listener of the bus.
|
|
4194
4606
|
|
|
4195
4607
|
## Check that closing cleans up
|
|
4196
4608
|
|
|
4197
4609
|
1. Open the chat and send a text. The bot answers after about a second. Every third question fails, and the retry asks again.
|
|
4198
4610
|
2. Send a text and close the chat before the answer arrives. No line appears, also when you open the chat again.
|
|
4199
|
-
3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed:
|
|
4611
|
+
3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed: every event shows `0`, and so does "all events". Open: each of the four has one listener, and "all events" is `4`. That is the proof that nothing is left subscribed.
|
|
4200
4612
|
4. Wait 10 s with the chat closed. The operator is not polled: no line from the operator appears.
|
|
4201
4613
|
5. Press `Escape` with the chat closed. Nothing happens. Open the chat and press it again: it closes.
|
|
4202
4614
|
6. Open the chat again. The old lines are back, and the polling runs.
|
|
@@ -4420,7 +4832,7 @@ Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may
|
|
|
4420
4832
|
|
|
4421
4833
|
To get `race`, `all`, `take` and the like on every task without passing `ctx`, see [`effectsPlugin`](effects.md#put-selected-effects-on-ctx).
|
|
4422
4834
|
|
|
4423
|
-
To give fields to **one**
|
|
4835
|
+
To give fields to **one** action, job or module, or to replace them for one `emit`, see [scopes](scope.md).
|
|
4424
4836
|
|
|
4425
4837
|
## Type the fields (optional)
|
|
4426
4838
|
|
|
@@ -4444,6 +4856,38 @@ bus.on("save", (draft, { store }) => store.put(draft));
|
|
|
4444
4856
|
- A name that the bus already has is a type error. The fields of the library (`signal`, `emit`, `run`, …) win at runtime, and a field that the context already has is not replaced.
|
|
4445
4857
|
- The plugin returns the same bus, so keep the result of the chain: `const bus = createBus().use(contextPlugin({ store }))`.
|
|
4446
4858
|
|
|
4859
|
+
## Build a field from the signal of each task
|
|
4860
|
+
|
|
4861
|
+
Problem: an HTTP client or a database handle should stop when the task is aborted. Without help, every handler passes `ctx.signal` to it by hand, and one forgotten call leaves a request running.
|
|
4862
|
+
|
|
4863
|
+
`fromSignal(make)` from `nanoeffects/signal` marks a field of `contextPlugin` as a value that is built from the signal of the task. It does not run `make` for the bus: it runs it for each task that reads the field, once, with the `ctx.signal` of that task.
|
|
4864
|
+
|
|
4865
|
+
```js
|
|
4866
|
+
import { createBus } from "nanoeffects";
|
|
4867
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
4868
|
+
import { fromSignal } from "nanoeffects/signal";
|
|
4869
|
+
import axios from "axios";
|
|
4870
|
+
|
|
4871
|
+
// contextPlugin — every task gets these fields
|
|
4872
|
+
// fromSignal — http is made from the signal of the task that reads ctx.http
|
|
4873
|
+
const bus = createBus().use(
|
|
4874
|
+
contextPlugin({
|
|
4875
|
+
logger: console, // a plain field: the same object for every task
|
|
4876
|
+
http: fromSignal((signal) => axios.create({ signal })),
|
|
4877
|
+
}),
|
|
4878
|
+
);
|
|
4879
|
+
|
|
4880
|
+
bus.on("user/open", async (id, ctx) => {
|
|
4881
|
+
const response = await ctx.http.get(`/users/${id}`); // aborts with the task
|
|
4882
|
+
ctx.logger.log(response.data);
|
|
4883
|
+
});
|
|
4884
|
+
```
|
|
4885
|
+
|
|
4886
|
+
- `make` runs at the first read, so a handler that never uses `ctx.http` pays nothing for it.
|
|
4887
|
+
- A task that `ctx.run` starts builds its own value from its own signal, so aborting the child stops only the child's requests.
|
|
4888
|
+
- The field is not copied by `{ ...ctx }` and `Object.keys(ctx)` does not list it. Read `ctx.http` first if you copy a context.
|
|
4889
|
+
- Without `contextPlugin` a `fromSignal` marker is only an object.
|
|
4890
|
+
|
|
4447
4891
|
### Name the fields in the event map instead
|
|
4448
4892
|
|
|
4449
4893
|
`createBus<Events>({ context: { store } })` also types the fields, but through the event map: `WithContext` is checker-only and says which fields the handlers of this bus have. Use it when you cannot call `use`, for example for the bus that `createEffectsMiddleware` makes.
|
|
@@ -4468,6 +4912,18 @@ A **scope** is a plain object. Every key goes on `ctx` as it is (`cache`, `api`,
|
|
|
4468
4912
|
|
|
4469
4913
|
You give a scope to the thing that uses it. A call can replace it. Nothing else changes: handlers still get `(payload, ctx)`.
|
|
4470
4914
|
|
|
4915
|
+
## On this page
|
|
4916
|
+
|
|
4917
|
+
- [Turn scopes on](#turn-scopes-on)
|
|
4918
|
+
- [Give an action its own fields](#give-an-action-its-own-fields)
|
|
4919
|
+
- [Bind an effect to the task](#bind-an-effect-to-the-task)
|
|
4920
|
+
- [Replace a field for one call](#replace-a-field-for-one-call)
|
|
4921
|
+
- [Give an action, a job or a module its own fields](#give-an-action-a-job-or-a-module-its-own-fields)
|
|
4922
|
+
- [Which value wins](#which-value-wins)
|
|
4923
|
+
- [Keep a generic effect generic](#keep-a-generic-effect-generic)
|
|
4924
|
+
- [Type the fields (optional)](#type-the-fields-optional)
|
|
4925
|
+
- [Limits](#limits)
|
|
4926
|
+
|
|
4471
4927
|
## Turn scopes on
|
|
4472
4928
|
|
|
4473
4929
|
Scopes are a plugin, so a bus that does not use them pays nothing.
|
|
@@ -4498,7 +4954,7 @@ import { createAction } from "nanoeffects/actions";
|
|
|
4498
4954
|
export const getUser = createAction(
|
|
4499
4955
|
"user/get",
|
|
4500
4956
|
(id, { api, cache }) => {
|
|
4501
|
-
// api and cache are on ctx for this
|
|
4957
|
+
// api and cache are on ctx for this action only
|
|
4502
4958
|
return cache.get(id) ?? api.getUser(id);
|
|
4503
4959
|
},
|
|
4504
4960
|
{ api, cache: new Map() },
|
|
@@ -4647,7 +5103,7 @@ When several places set the same field, the strongest one wins:
|
|
|
4647
5103
|
| Strength | Where it is set |
|
|
4648
5104
|
| -------- | --------------- |
|
|
4649
5105
|
| 1 (strongest) | the second argument of `emit` |
|
|
4650
|
-
| 2 | the scope of the action
|
|
5106
|
+
| 2 | the scope of the action or job |
|
|
4651
5107
|
| 3 | the scope of the module that owns the handler |
|
|
4652
5108
|
| 4 (weakest) | the bus: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
|
|
4653
5109
|
|
|
@@ -4741,6 +5197,23 @@ Use this page when **your domain logic lives in classes**: a service, a store or
|
|
|
4741
5197
|
|
|
4742
5198
|
If your logic is plain functions, stay with [modules](actions.md#mount-domains-with-modules): a module is the same idea without a class.
|
|
4743
5199
|
|
|
5200
|
+
## On this page
|
|
5201
|
+
|
|
5202
|
+
- [The problem without it](#the-problem-without-it)
|
|
5203
|
+
- [Make an independent emitter with `Emitter`](#make-an-independent-emitter-with-emitter)
|
|
5204
|
+
- [Type the events (optional)](#type-the-events-optional)
|
|
5205
|
+
- [Give a class the methods of a shared bus](#give-a-class-the-methods-of-a-shared-bus)
|
|
5206
|
+
- [Extend the result with no class of your own](#extend-the-result-with-no-class-of-your-own)
|
|
5207
|
+
- [Let the class make its bus](#let-the-class-make-its-bus)
|
|
5208
|
+
- [End the subscriptions with `dispose`](#end-the-subscriptions-with-dispose)
|
|
5209
|
+
- [Stream an event with a `Subject`](#stream-an-event-with-a-subject)
|
|
5210
|
+
- [Keep a method of your own](#keep-a-method-of-your-own)
|
|
5211
|
+
- [Subscribe a method with `@On` and `@Once`](#subscribe-a-method-with-on-and-once)
|
|
5212
|
+
- [Decorate the class with `@WithBus`](#decorate-the-class-with-withbus)
|
|
5213
|
+
- [Pass a method by hand](#pass-a-method-by-hand)
|
|
5214
|
+
- [Type the instances (optional)](#type-the-instances-optional)
|
|
5215
|
+
- [What is not done](#what-is-not-done)
|
|
5216
|
+
|
|
4744
5217
|
## The problem without it
|
|
4745
5218
|
|
|
4746
5219
|
A class that listens to the bus keeps a reference to it, and keeps every unsubscribe function in a list to call when the object goes away. Forget one and the old object keeps handling events. `extendBus` does both for you.
|
|
@@ -5171,7 +5644,7 @@ export const getUser = createAction(
|
|
|
5171
5644
|
},
|
|
5172
5645
|
);
|
|
5173
5646
|
|
|
5174
|
-
// createModule: m.use binds the handler the
|
|
5647
|
+
// createModule: m.use binds the handler the action carries, while the module is mounted
|
|
5175
5648
|
export const userModule = createModule("user", (m) => {
|
|
5176
5649
|
m.use(getUser);
|
|
5177
5650
|
});
|
|
@@ -5421,6 +5894,17 @@ Source: docs/migrating-from-effector.md
|
|
|
5421
5894
|
|
|
5422
5895
|
Read this page if your project uses Effector and you want to know where nanoeffects fits. They overlap in one place: events that start effects. Effector also holds state; nanoeffects does not. It maps the names, shows one effect both ways, and lists what has no equivalent.
|
|
5423
5896
|
|
|
5897
|
+
## On this page
|
|
5898
|
+
|
|
5899
|
+
- [Split the work](#split-the-work)
|
|
5900
|
+
- [Find the equivalent](#find-the-equivalent)
|
|
5901
|
+
- [Rewrite an effect](#rewrite-an-effect)
|
|
5902
|
+
- [Keep the state in Zustand](#keep-the-state-in-zustand)
|
|
5903
|
+
- [Rewrite a form](#rewrite-a-form)
|
|
5904
|
+
- [Replace the log with a request](#replace-the-log-with-a-request)
|
|
5905
|
+
- [Use both in one app](#use-both-in-one-app)
|
|
5906
|
+
- [What is different](#what-is-different)
|
|
5907
|
+
|
|
5424
5908
|
## Split the work
|
|
5425
5909
|
|
|
5426
5910
|
| Job | Use |
|
|
@@ -6051,6 +6535,16 @@ Source: docs/semantics.md
|
|
|
6051
6535
|
|
|
6052
6536
|
Guarantees and sharp edges. Read when something surprises you; not required to start.
|
|
6053
6537
|
|
|
6538
|
+
## Five rules to know first
|
|
6539
|
+
|
|
6540
|
+
1. A handler is `(payload, ctx)`. Use `ctx.signal`, `ctx.emit` and `ctx.on`; the rest of `ctx` is optional.
|
|
6541
|
+
2. Pass `ctx.signal` to what the handler calls (`fetch`, a client), so an abort stops the work.
|
|
6542
|
+
3. An aborted task delivers no result and emits nothing.
|
|
6543
|
+
4. `emit` is synchronous: the first handler error is thrown, unless `onError` is set.
|
|
6544
|
+
5. Rejections of async handlers are not handled without `onError`.
|
|
6545
|
+
|
|
6546
|
+
Details of each rule and the rest:
|
|
6547
|
+
|
|
6054
6548
|
- **An aborted task delivers no result.** If the task aborts before a promise settles, the promise rejects with the abort reason, even if the value was just produced in the same tick. `latest`, `debounce`, `all`, `race`, `take`, and the rest follow this rule.
|
|
6055
6549
|
- **`emit` is a plain, synchronous loop** (see [Errors](errors.md)). A `ctx.emit` inside a handler is the same call: without `onError`, an error of a nested handler is thrown into the handler that emitted; with `onError` it goes there and `ctx.emit` does not throw.
|
|
6056
6550
|
- **Rejections of async handlers** are not handled for you. Without `onError` they are unhandled rejections, which end a Node process by default. Use `onError` for async handlers.
|
|
@@ -6066,6 +6560,22 @@ Find out what your app did and why: which events were emitted, which calls a pol
|
|
|
6066
6560
|
|
|
6067
6561
|
Tracing is optional. A bus works the same without it, and it costs almost nothing until you turn it on. If you only want to see what happens while you develop, jump to [Show every emit](#show-every-emit) and [Configure Redux DevTools](#configure-redux-devtools); the rest of the page is for apps that need to follow a flow or turn tracing off in a build.
|
|
6068
6562
|
|
|
6563
|
+
## On this page
|
|
6564
|
+
|
|
6565
|
+
- [Attach a tracer](#attach-a-tracer)
|
|
6566
|
+
- [Show every emit](#show-every-emit)
|
|
6567
|
+
- [Show which subscription began a chain](#show-which-subscription-began-a-chain)
|
|
6568
|
+
- [Follow one flow with a traceGroupId](#follow-one-flow-with-a-tracegroupid)
|
|
6569
|
+
- [Name a listener](#name-a-listener)
|
|
6570
|
+
- [Keep the chain: `ctx.emit`, not `bus.emit`](#keep-the-chain-ctxemit-not-busemit)
|
|
6571
|
+
- [Turn the names off](#turn-the-names-off)
|
|
6572
|
+
- [Use built-in loggers](#use-built-in-loggers)
|
|
6573
|
+
- [Cut volume in production](#cut-volume-in-production)
|
|
6574
|
+
- [Keep the last events for an incident](#keep-the-last-events-for-an-incident)
|
|
6575
|
+
- [Configure Redux DevTools](#configure-redux-devtools)
|
|
6576
|
+
- [Write your own logger](#write-your-own-logger)
|
|
6577
|
+
- [Drop tracing from a build](#drop-tracing-from-a-build)
|
|
6578
|
+
|
|
6069
6579
|
## Attach a tracer
|
|
6070
6580
|
|
|
6071
6581
|
Pass `trace` to `createBus`. Make it with `createTracer` from `nanoeffects/trace`, which takes a logger (a function) or an array of them. The tracing code lives in that entry, so a bus without `trace` does not carry it.
|
|
@@ -6148,7 +6658,7 @@ The event changes nothing else: the other tasks keep their `parentId`, `rootId`
|
|
|
6148
6658
|
|
|
6149
6659
|
## Show which subscription began a chain
|
|
6150
6660
|
|
|
6151
|
-
**Problem.** `useEmitter` and `bus.emitter` put an event on the bus from outside any task, such as a render or the callback of a store. In the trace that event has no sign of where it came from. With many such subscriptions it is hard to tell which one started a chain of
|
|
6661
|
+
**Problem.** `useEmitter` and `bus.emitter` put an event on the bus from outside any task, such as a render or the callback of a store. In the trace that event has no sign of where it came from. With many such subscriptions it is hard to tell which one started a chain of actions and retries.
|
|
6152
6662
|
|
|
6153
6663
|
**When.** The bus has a tracer and the project has several `useEmitter` or `bus.emitter` calls. The names come from `tracePlugin`: use it on the bus, or the `traceId` is ignored.
|
|
6154
6664
|
|
|
@@ -6176,8 +6686,8 @@ In a component, the same option goes in the options of the hook: `useEmitter(nam
|
|
|
6176
6686
|
|
|
6177
6687
|
```
|
|
6178
6688
|
[nanoeffects] Profile · run start
|
|
6179
|
-
[nanoeffects] user/save ·
|
|
6180
|
-
[nanoeffects] user/save ·
|
|
6689
|
+
[nanoeffects] user/save · action start
|
|
6690
|
+
[nanoeffects] user/save · action done
|
|
6181
6691
|
[nanoeffects] Profile · run done
|
|
6182
6692
|
```
|
|
6183
6693
|
|
|
@@ -6217,7 +6727,7 @@ const pay = createAction("checkout/pay", async () => {}, {
|
|
|
6217
6727
|
```
|
|
6218
6728
|
|
|
6219
6729
|
```
|
|
6220
|
-
[nanoeffects] [checkout] checkout/pay ·
|
|
6730
|
+
[nanoeffects] [checkout] checkout/pay · action start
|
|
6221
6731
|
```
|
|
6222
6732
|
|
|
6223
6733
|
The emitters take it too: `useEmitter(cartChanged, cart, { traceId: "Cart", traceGroupId: "checkout" })`. `traceGroupId` works without `scopePlugin`, and needs `tracePlugin`.
|
|
@@ -6247,7 +6757,7 @@ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
|
|
|
6247
6757
|
|
|
6248
6758
|
```
|
|
6249
6759
|
[nanoeffects] saveNotifier · run start
|
|
6250
|
-
[nanoeffects] user/save ·
|
|
6760
|
+
[nanoeffects] user/save · action start
|
|
6251
6761
|
[nanoeffects] saveNotifier · run done
|
|
6252
6762
|
```
|
|
6253
6763
|
|
|
@@ -6292,7 +6802,7 @@ const bus = createBus({
|
|
|
6292
6802
|
| `reduxDevtools()` | Sends the emits to Redux DevTools, and with `policies` the stages of the handlers as `type/effect/phase`; state is `{ running }` |
|
|
6293
6803
|
| `consoleLogger()` | Prints one line per phase, indented by task depth |
|
|
6294
6804
|
| `performanceLogger()` | User Timing measures for the browser Performance panel |
|
|
6295
|
-
| `createTraceRecorder()` | Keeps events in `recorder.events
|
|
6805
|
+
| `createTraceRecorder()` | Keeps events in `recorder.events`: all of them for tests, or with `{ capacity }` the last N ([see below](#keep-the-last-events-for-an-incident)) |
|
|
6296
6806
|
|
|
6297
6807
|
### Cut volume in production
|
|
6298
6808
|
|
|
@@ -6308,8 +6818,60 @@ createTracer(logger, {
|
|
|
6308
6818
|
- `sample` is decided once per root task; every task below it follows, so a trace is never cut in the middle. Unsampled tasks cost almost nothing.
|
|
6309
6819
|
- `filter` runs on each event before any logger sees it.
|
|
6310
6820
|
|
|
6821
|
+
### Keep the last events for an incident
|
|
6822
|
+
|
|
6823
|
+
`createTraceRecorder()` with no option keeps every event, so a long session fills memory. That is fine in a test and wrong in a running app. Give it a `capacity` and it keeps only the last N events (a ring buffer): when something goes wrong, you still have what led up to it.
|
|
6824
|
+
|
|
6825
|
+
<!-- docs-check: new -->
|
|
6826
|
+
|
|
6827
|
+
```js
|
|
6828
|
+
// bus.js
|
|
6829
|
+
import { createBus } from "nanoeffects";
|
|
6830
|
+
import { createTraceRecorder, createTracer } from "nanoeffects/trace";
|
|
6831
|
+
|
|
6832
|
+
// A logger that keeps the last 500 events and drops the older ones.
|
|
6833
|
+
export const recorder = createTraceRecorder({ capacity: 500 });
|
|
6834
|
+
|
|
6835
|
+
// Feed the recorder from the tracer.
|
|
6836
|
+
const trace = createTracer(recorder);
|
|
6837
|
+
|
|
6838
|
+
export const bus = createBus({ trace });
|
|
6839
|
+
```
|
|
6840
|
+
|
|
6841
|
+
```js
|
|
6842
|
+
// incident.js
|
|
6843
|
+
import { recorder } from "./bus";
|
|
6844
|
+
|
|
6845
|
+
// Keep what is safe to send. An event also has `payload` and `error`.
|
|
6846
|
+
const withoutPayload = ({ id, parentId, type, effect, phase }) => ({
|
|
6847
|
+
id,
|
|
6848
|
+
parentId,
|
|
6849
|
+
type,
|
|
6850
|
+
effect,
|
|
6851
|
+
phase,
|
|
6852
|
+
});
|
|
6853
|
+
|
|
6854
|
+
window.addEventListener("error", () => {
|
|
6855
|
+
const events = recorder.events; // a snapshot, oldest event first
|
|
6856
|
+
const safe = events.map(withoutPayload);
|
|
6857
|
+
const body = JSON.stringify(safe);
|
|
6858
|
+
navigator.sendBeacon("/api/incident", body);
|
|
6859
|
+
});
|
|
6860
|
+
```
|
|
6861
|
+
|
|
6862
|
+
What to know before you send the events:
|
|
6863
|
+
|
|
6864
|
+
- **`payload` and `error` can hold private data**: what a user typed, tokens, personal text. The recorder keeps them as they are and does not redact. Choose what to send, as `withoutPayload` does.
|
|
6865
|
+
- **There is no time on an event.** You get the order, not the duration. To add a time, wrap the recorder in a logger that stamps `performance.now()` on a copy of the event.
|
|
6866
|
+
- **An old `parentId` can point at an event that was pushed out.** When you build a tree from the events, treat an unknown `parentId` as a root.
|
|
6867
|
+
- **`recorder.events` is a snapshot with a `capacity`.** Each read builds a new array, so read it once at the moment you need it. Without a `capacity` it is one live array.
|
|
6868
|
+
- **A low `sample` thins the buffer.** `createTracer(recorder, { sample: 0.05 })` records 5% of root tasks, so the buffer may hold little when the incident comes. See [Cut volume in production](#cut-volume-in-production).
|
|
6869
|
+
- **A `capacity` that is not a positive integer** (`0`, a negative number, `NaN`) means no limit.
|
|
6870
|
+
|
|
6311
6871
|
### Configure Redux DevTools
|
|
6312
6872
|
|
|
6873
|
+
<!-- docs-check: new -->
|
|
6874
|
+
|
|
6313
6875
|
Read-only (the bus owns no state, so there is no time travel). Does nothing when the extension is not installed. Has its own instance in the DevTools picker, apart from your store.
|
|
6314
6876
|
|
|
6315
6877
|
Quiet by default, so you see what happened before you see how:
|
|
@@ -6409,6 +6971,8 @@ Source: docs/observable.md
|
|
|
6409
6971
|
|
|
6410
6972
|
Use this page when your app already works with Observables or RxJS and you want a bus event as a stream. If you start from scratch, prefer `bus.on` with policies: they cancel work and show up in traces.
|
|
6411
6973
|
|
|
6974
|
+
The same entry has `events`, which needs no stream library: it gives an event to a `for await` loop. See [Read an event with a `for await` loop](#read-an-event-with-a-for-await-loop).
|
|
6975
|
+
|
|
6412
6976
|
`createObserve(bus)` from `nanoeffects/observable` turns a bus event into an Observable that follows the TC39 proposal (`subscribe`, `unsubscribe`, `Symbol.observable`). RxJS and other stream libraries take it as it is. Nothing here depends on them.
|
|
6413
6977
|
|
|
6414
6978
|
## Turn an event into a stream
|
|
@@ -6552,6 +7116,47 @@ bus.on(
|
|
|
6552
7116
|
|
|
6553
7117
|
`switchMap` drops the old inner stream. `latest` aborts the old task through `signal`, so a `fetch` that got the signal really stops.
|
|
6554
7118
|
|
|
7119
|
+
## Read an event with a `for await` loop
|
|
7120
|
+
|
|
7121
|
+
Problem: a task wants to handle the payloads of one event one after another, in order, and stop when the screen closes. `take` waits for one payload, and a listener has no place for a loop.
|
|
7122
|
+
|
|
7123
|
+
`events(source, event, options)` from `nanoeffects/observable` returns an async iterator of the payloads. It listens from the call, so a payload that comes before the first read is kept. `source` is a bus or a task context; with a context the loop also ends when the task aborts.
|
|
7124
|
+
|
|
7125
|
+
```js
|
|
7126
|
+
import { createBus } from "nanoeffects";
|
|
7127
|
+
import { events } from "nanoeffects/observable";
|
|
7128
|
+
|
|
7129
|
+
const bus = createBus();
|
|
7130
|
+
const controller = new AbortController();
|
|
7131
|
+
|
|
7132
|
+
// events — the "query/changed" payloads one by one; keeps the last 3 while the loop is busy
|
|
7133
|
+
const queries = events(bus, "query/changed", {
|
|
7134
|
+
signal: controller.signal,
|
|
7135
|
+
buffer: 3,
|
|
7136
|
+
});
|
|
7137
|
+
|
|
7138
|
+
async function watchQueries() {
|
|
7139
|
+
for await (const query of queries) {
|
|
7140
|
+
console.log("search for", query); // runs once per payload, in order
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
7143
|
+
|
|
7144
|
+
watchQueries().catch((reason) => console.log("stopped:", reason)); // the abort rejects the loop
|
|
7145
|
+
bus.emit("query/changed", "cat");
|
|
7146
|
+
controller.abort("screen closed"); // the loop ends with this reason
|
|
7147
|
+
```
|
|
7148
|
+
|
|
7149
|
+
| Option | What it does | Default |
|
|
7150
|
+
| ------ | ------------ | ------- |
|
|
7151
|
+
| `signal` | Ends the loop. The waiting read rejects with the abort reason. | none |
|
|
7152
|
+
| `buffer` | The most payloads kept while the loop is busy. | `Infinity` |
|
|
7153
|
+
| `overflow` | Which payload goes when the buffer is full: `"drop-oldest"` or `"drop-newest"`. | `"drop-oldest"` |
|
|
7154
|
+
|
|
7155
|
+
- `break`, `return` and a throw inside the loop remove the listener.
|
|
7156
|
+
- With no `buffer`, a loop that never reads keeps every payload. Set `buffer` for a slow loop.
|
|
7157
|
+
- An event of a bus does not end, so the loop ends only by `break`, the `signal` or the task.
|
|
7158
|
+
- Inside a task, `events(ctx, "query/changed")` needs no `signal`: the task ending is the end.
|
|
7159
|
+
|
|
6555
7160
|
## Convert back to a stream or a promise
|
|
6556
7161
|
|
|
6557
7162
|
| You need | Write |
|