nanoeffects 0.28.0 → 0.30.1
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 +6 -5
- package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-DFlmNJ6h.js +1 -0
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/context-types-DKNc5mkS.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 +44 -3
- package/dist/dev.js +2 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/emit-key-C6FL7Ewn.js +1 -0
- package/dist/guard.d.ts +59 -0
- package/dist/guard.js +1 -0
- 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-G39MaVOt.d.ts} +38 -38
- package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -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-7GUL2EW5.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-BWNP23hr.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/production/dev.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/site-DLvUoXs3.js +1 -0
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +170 -6
- package/dist/trace.js +3 -1
- package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +65 -22
- package/docs/actions.md +56 -13
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +15 -1
- package/docs/chat-widget.md +9 -7
- package/docs/context.md +352 -8
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +283 -0
- package/docs/effects.md +259 -131
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +57 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +15 -2
- package/docs/migrating-from-mitt.md +6 -4
- package/docs/migrating-from-rxjs.md +2 -0
- package/docs/migrating-from-saga.md +2 -0
- package/docs/motivation.md +14 -21
- package/docs/observable.md +45 -0
- package/docs/oop.md +20 -1
- package/docs/package.md +25 -13
- package/docs/pass-the-signal.md +281 -0
- package/docs/plugins.md +90 -15
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +276 -6
- package/docs/react.md +50 -11
- package/docs/recipes.md +528 -0
- package/docs/ru/README.md +62 -0
- package/docs/ru/actions.md +412 -0
- package/docs/ru/cancel-stale-requests.md +106 -0
- package/docs/ru/chat-example.md +259 -0
- package/docs/ru/chat-widget.md +131 -0
- package/docs/ru/context.md +401 -0
- package/docs/ru/debounce-and-abort.md +105 -0
- package/docs/ru/debugging-loops.md +283 -0
- package/docs/ru/effects.md +538 -0
- package/docs/ru/errors.md +36 -0
- package/docs/ru/examples.md +60 -0
- package/docs/ru/guide.md +530 -0
- package/docs/ru/hmr.md +83 -0
- package/docs/ru/loop-guard.md +109 -0
- package/docs/ru/migrating-from-effector.md +339 -0
- package/docs/ru/migrating-from-mitt.md +99 -0
- package/docs/ru/migrating-from-rxjs.md +197 -0
- package/docs/ru/migrating-from-saga.md +97 -0
- package/docs/ru/motivation.md +47 -0
- package/docs/ru/observable.md +233 -0
- package/docs/ru/oop.md +431 -0
- package/docs/ru/package.md +42 -0
- package/docs/ru/pass-the-signal.md +281 -0
- package/docs/ru/plugins.md +398 -0
- package/docs/ru/policies.md +201 -0
- package/docs/ru/prevent-double-submit.md +122 -0
- package/docs/ru/query-libraries.md +392 -0
- package/docs/ru/react.md +309 -0
- package/docs/ru/recipes.md +528 -0
- package/docs/ru/semantics.md +19 -0
- package/docs/ru/server.md +204 -0
- package/docs/ru/starter.md +118 -0
- package/docs/ru/stores.md +673 -0
- package/docs/ru/svelte.md +78 -0
- package/docs/ru/tracing.md +466 -0
- package/docs/ru/typescript.md +170 -0
- package/docs/ru/vue.md +92 -0
- package/docs/semantics.md +12 -0
- package/docs/server.md +5 -3
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +132 -7
- package/docs/typescript.md +3 -1
- package/docs/vue.md +3 -1
- package/llms-full.txt +2718 -545
- package/llms.txt +17 -14
- package/package.json +25 -8
- 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/docs/scope.md +0 -266
- package/docs/stability.md +0 -90
package/docs/examples.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
> **Levels A to F.** Read first: [README](README.md#the-ladder-from-a-to-f).
|
|
4
|
+
|
|
5
|
+
Examples to run in the browser and to read. Every folder lives in `examples/` of the repository and is type-checked with the project. Two of them are explained step by step: [Build a chat](chat-example.md) and [Build a chat widget](chat-widget.md).
|
|
6
|
+
|
|
7
|
+
## Try it in the browser
|
|
8
|
+
|
|
9
|
+
All three are the same search box: type fast and only one request starts (`debounce`); type again while a request runs and the older one is aborted (`latest`). StackBlitz runs them with no install.
|
|
10
|
+
|
|
11
|
+
| Folder | What is in it |
|
|
12
|
+
| ------ | ------------- |
|
|
13
|
+
| [`playground`](../examples/playground) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground) | Plain DOM and one file: the smallest setup. The page lists the requests, so the abort is visible. |
|
|
14
|
+
| [`playground-react`](../examples/playground-react) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground-react) | The same search in a React component, with the bus outside React. |
|
|
15
|
+
| [`playground-devtools`](../examples/playground-devtools) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground-devtools) | The same search with its emits and stopped calls sent to Redux DevTools. |
|
|
16
|
+
|
|
17
|
+
## Bigger projects
|
|
18
|
+
|
|
19
|
+
React and TypeScript projects that show where logic goes when an app has several features.
|
|
20
|
+
|
|
21
|
+
| Folder | What is in it |
|
|
22
|
+
| ------ | ------------- |
|
|
23
|
+
| [`chat`](../examples/chat) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat) | Rooms, history under `latest`, ordered sends under `queue` and `retry`, search under `allSettled`, polling with `poll`. Explained in [Build a chat](chat-example.md). |
|
|
24
|
+
| [`pokedex`](../examples/pokedex) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/pokedex) | Real requests to PokeAPI: a search with `debounce`, `latest`, `timeout` and `retry`, abilities with `allSettled`. |
|
|
25
|
+
| [`spells`](../examples/spells) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/spells) | The smallest example: a search in one file, one action that carries its handler. |
|
|
26
|
+
| [`chat-widget`](../examples/chat-widget) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat-widget) | A chat behind a launcher button: opening mounts one module, closing unmounts it. Explained in [Build a chat widget](chat-widget.md). |
|
|
27
|
+
| [`counter`](../examples/counter) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/counter) | The counter of the redux-saga tutorial, without a store and with Redux Toolkit. |
|
|
28
|
+
| [`rickandmorty`](../examples/rickandmorty) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/rickandmorty) | Characters with React Query: requests are actions that get its abort signal. |
|
|
29
|
+
| [`trades`](../examples/trades) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/trades) | A live stream over a WebSocket: `retry`, `latest` and `throttle`. |
|
|
30
|
+
|
|
31
|
+
## Backend examples
|
|
32
|
+
|
|
33
|
+
Small [Fastify](https://fastify.dev) apps with no database. They need Node 18 or newer: run `npm install` and `npm start` in the folder. More in [Stop work on a server](server.md).
|
|
34
|
+
|
|
35
|
+
| Folder | What is in it |
|
|
36
|
+
| ------ | ------------- |
|
|
37
|
+
| [`api-server`](../examples/api-server) | A server over PokeAPI: `timeout`, `retry`, `allSettled`, and an abort when the client leaves. |
|
|
38
|
+
| [`job-worker`](../examples/job-worker) | A queue worker: `queue`, `retry`, `timeout`, counts from the outcomes of a job. |
|
|
39
|
+
| [`ws-server`](../examples/ws-server) | A WebSocket server: one controller per socket, `latest` with a `key`. |
|
|
40
|
+
|
|
41
|
+
## Recipes
|
|
42
|
+
|
|
43
|
+
Each file is one job, written as a short program with a comment on every nanoeffects call.
|
|
44
|
+
|
|
45
|
+
| File | Job | Docs |
|
|
46
|
+
| ---- | --- | ---- |
|
|
47
|
+
| [`search.ts`](../examples/search.ts) | A search box that waits for quiet and aborts the older request. | [Policies](policies.md) |
|
|
48
|
+
| [`backoff.ts`](../examples/backoff.ts) | Retry a flaky request with a growing pause. | [Effects](effects.md#backoff) |
|
|
49
|
+
| [`registration-form.ts`](../examples/registration-form.ts) | A sign-up form: a username check that does not race. | [React form](react.md) |
|
|
50
|
+
| [`scope.ts`](../examples/scope.ts) | An action with its own effects and a test that replaces one. | [Working with context](context.md) |
|
|
51
|
+
| [`modules.ts`](../examples/modules.ts) | Domain logic with no UI, mounted and unmounted as one unit. | [Actions](actions.md) |
|
|
52
|
+
| [`redux.ts`](../examples/redux.ts) | Redux Toolkit actions that also reach the bus. | [State managers](stores.md) |
|
|
53
|
+
| [`guide.ts`](../examples/guide.ts) | The code of every step of the guide. | [Guide](guide.md) |
|
|
54
|
+
|
|
55
|
+
## TypeScript projects
|
|
56
|
+
|
|
57
|
+
| Folder | What is in it |
|
|
58
|
+
| ------ | ------------- |
|
|
59
|
+
| [`typescript`](../examples/typescript) | Name the event map once and use it in modules and actions. |
|
|
60
|
+
| [`typescript-context`](../examples/typescript-context) | Give `ctx` the fields of your bus and type them from one place. |
|
package/docs/guide.md
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
# Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Levels A to D.** Read first: [README](../README.md) (install and the first example).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
5
|
+
Climb one step at a time: small bus → policies → effects → handlers and modules. Stop when your project has what it needs. Install the package first: `npm install nanoeffects` (details in the [README](../README.md#install)). The levels A to D are described in the [map of the docs](README.md#the-ladder-from-a-to-f).
|
|
6
|
+
|
|
7
|
+
| Level | Your project | Stop after |
|
|
8
|
+
| ----- | ------------ | ---------- |
|
|
9
|
+
| A | Widget / prototype | §1–2 |
|
|
10
|
+
| B | SPA with async UI | §3–4 |
|
|
11
|
+
| C | Unreliable network / long flows | §5–6 |
|
|
12
|
+
| D | Domain logic off the UI | §7–9 |
|
|
11
13
|
|
|
12
14
|
Runnable code for every step: [`examples/guide.ts`](../examples/guide.ts).
|
|
13
15
|
|
|
16
|
+
## On this page
|
|
17
|
+
|
|
18
|
+
- [1. Create a bus](#1-create-a-bus)
|
|
19
|
+
- [Listen to every event](#listen-to-every-event)
|
|
20
|
+
- [Pick events with a function](#pick-events-with-a-function)
|
|
21
|
+
- [2. Use the handler context](#2-use-the-handler-context)
|
|
22
|
+
- [3. Abort the previous run with `latest`](#3-abort-the-previous-run-with-latest)
|
|
23
|
+
- [4. Pick another policy when overlap differs](#4-pick-another-policy-when-overlap-differs)
|
|
24
|
+
- [5. Add retry and timeout](#5-add-retry-and-timeout)
|
|
25
|
+
- [6. Run work in parallel or wait for an event](#6-run-work-in-parallel-or-wait-for-an-event)
|
|
26
|
+
- [7. Name events three ways](#7-name-events-three-ways)
|
|
27
|
+
- [8. Replace a handler on the fly](#8-replace-a-handler-on-the-fly)
|
|
28
|
+
- [9. Mount a domain as a module](#9-mount-a-domain-as-a-module)
|
|
29
|
+
- [10. Add types when you want checking (optional)](#10-add-types-when-you-want-checking-optional)
|
|
30
|
+
- [11. Isolate errors and warn on typos](#11-isolate-errors-and-warn-on-typos)
|
|
31
|
+
- [Where next](#where-next)
|
|
32
|
+
|
|
14
33
|
## 1. Create a bus
|
|
15
34
|
|
|
16
35
|
`on` subscribes. `emit` calls the subscribers. `on` returns the unsubscribe function.
|
|
@@ -31,7 +50,12 @@ bus.emit("greet", "Bob"); // nobody listens
|
|
|
31
50
|
|
|
32
51
|
Handlers run at once, in subscription order.
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
Four more calls cover most of what you need from the bus itself:
|
|
54
|
+
|
|
55
|
+
- `bus.once(event, handler)` is like `on`, but the handler runs at most once (it is removed before it is invoked).
|
|
56
|
+
- `bus.off("greet")` clears every listener of that event.
|
|
57
|
+
- `bus.off()` with no arguments clears **every** listener on the bus, `"*"` included. `ctx.off` is the same `off` as the bus's, so `ctx.off()` also clears the whole bus.
|
|
58
|
+
- `bus.get("greet")` gives the listeners that event has right now, so `bus.get("greet").length === 0` tells that nobody listens any more. `"*"` gives the any-event listeners, which a named event does not include; `bus.get()` with no name gives every listener of every event. It takes the event name as a string: for an action creator use `creator.type`.
|
|
35
59
|
|
|
36
60
|
```js
|
|
37
61
|
import { createBus } from "nanoeffects";
|
|
@@ -49,6 +73,8 @@ bus.off(); // clear every subscription
|
|
|
49
73
|
|
|
50
74
|
### Listen to every event
|
|
51
75
|
|
|
76
|
+
You can skip this and the next section on a first read.
|
|
77
|
+
|
|
52
78
|
`bus.on("*", fn)` runs for every event of the bus. It is for logging, debugging, or forwarding events elsewhere. The handler gets the payload and the task context as usual, and `ctx.type` is the name of the event.
|
|
53
79
|
|
|
54
80
|
```js
|
|
@@ -71,7 +97,7 @@ unbind();
|
|
|
71
97
|
|
|
72
98
|
### Pick events with a function
|
|
73
99
|
|
|
74
|
-
To listen to a group of events, such as every `user/*` event, `"*"` and a check of `ctx.type` do the job but leave the check to you. `matchPlugin` lets `on` take a function that picks the events. The function gets `{ type, payload }` of each event, and the handler runs when it returns true.
|
|
100
|
+
This is for level B and later. To listen to a group of events, such as every `user/*` event, `"*"` and a check of `ctx.type` do the job but leave the check to you. `matchPlugin` lets `on` take a function that picks the events. The function gets `{ type, payload }` of each event, and the handler runs when it returns true.
|
|
75
101
|
|
|
76
102
|
<!-- docs-check: new -->
|
|
77
103
|
|
|
@@ -217,7 +243,7 @@ const searchLatest = latest(async (query, { signal }) => {
|
|
|
217
243
|
bus.on("search", debounce(300, searchLatest));
|
|
218
244
|
```
|
|
219
245
|
|
|
220
|
-
Full options: [Policies](policies.md). No bus? A policy also works as a plain function: [
|
|
246
|
+
Full options: [Policies](policies.md). No bus? A policy also works as a plain function: [`withPolicy`: call a policy outside a bus](policies.md#withpolicy-call-a-policy-outside-a-bus).
|
|
221
247
|
|
|
222
248
|
## 5. Add retry and timeout
|
|
223
249
|
|
|
@@ -272,10 +298,11 @@ import { all, delay, race, take } from "nanoeffects/effects";
|
|
|
272
298
|
const bus = createBus();
|
|
273
299
|
|
|
274
300
|
// all — run together; one failure cancels the rest
|
|
301
|
+
// child — the context of that request: it has the `signal`, so the request stops with the group
|
|
275
302
|
bus.on("search", async (id, ctx) => {
|
|
276
303
|
const { user, posts } = await all(ctx, {
|
|
277
|
-
user: (
|
|
278
|
-
posts: (
|
|
304
|
+
user: (child) => api.user(id, child),
|
|
305
|
+
posts: (child) => api.posts(id, child),
|
|
279
306
|
});
|
|
280
307
|
ctx.emit("loaded", { user, posts });
|
|
281
308
|
});
|
|
@@ -346,9 +373,11 @@ bus.emit(double(21)); // 42
|
|
|
346
373
|
|
|
347
374
|
Use strings for the plain case. Use objects when other code already builds actions. Give `createAction` a handler when you need a result from `emit`.
|
|
348
375
|
|
|
349
|
-
## 8.
|
|
376
|
+
## 8. Replace a handler on the fly
|
|
350
377
|
|
|
351
|
-
|
|
378
|
+
**Problem.** The handler of an action is in the file of the feature, but a test needs a fake, or another layer owns the work. If the handler sits in `createAction`, the event file is tied to the work.
|
|
379
|
+
|
|
380
|
+
**What to do.** Create the action without a handler: it is an event. Bind the real handler with `setHandler` when the bus is made. Later, `actions.handle` binds a newer handler that wins until you remove it.
|
|
352
381
|
|
|
353
382
|
```js
|
|
354
383
|
// user.js
|
|
@@ -360,26 +389,32 @@ export const getUser = createAction("user/get"); // no handler yet: an event
|
|
|
360
389
|
```js
|
|
361
390
|
// bus.js
|
|
362
391
|
import { createBus } from "nanoeffects";
|
|
363
|
-
import { actionsPlugin } from "nanoeffects/actions";
|
|
392
|
+
import { actionsPlugin, setHandler } from "nanoeffects/actions";
|
|
393
|
+
import { getUser } from "./user";
|
|
364
394
|
|
|
365
395
|
export const bus = createBus();
|
|
366
|
-
|
|
367
|
-
|
|
396
|
+
|
|
397
|
+
// setHandler — binds the real handler to the action
|
|
398
|
+
const realUser = setHandler(getUser, (id) => api.user(id));
|
|
399
|
+
|
|
400
|
+
// actionsPlugin — the registry of bound handlers of this bus
|
|
401
|
+
export const actions = bus.use(actionsPlugin([realUser]));
|
|
368
402
|
```
|
|
369
403
|
|
|
370
404
|
```js
|
|
371
405
|
import { bus, actions } from "./bus";
|
|
372
406
|
import { getUser } from "./user";
|
|
373
407
|
|
|
374
|
-
bus.emit(getUser("1")); //
|
|
408
|
+
const user = await bus.emit(getUser("1")); // the real handler answers
|
|
375
409
|
|
|
376
|
-
|
|
377
|
-
const
|
|
410
|
+
// actions.handle — a newer binding wins, for example a fake in a test
|
|
411
|
+
const restore = actions.handle(getUser, (id) => ({ id, name: "test" }));
|
|
412
|
+
const fake = await bus.emit(getUser("1")); // the fake answers
|
|
378
413
|
|
|
379
|
-
|
|
414
|
+
restore(); // the real handler answers again
|
|
380
415
|
```
|
|
381
416
|
|
|
382
|
-
Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can
|
|
417
|
+
Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can bind handlers and remove them on unmount. More in [Actions](actions.md#bind-a-handler-later).
|
|
383
418
|
|
|
384
419
|
## 9. Mount a domain as a module
|
|
385
420
|
|
package/docs/hmr.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Hot reload and development diagnostics
|
|
2
2
|
|
|
3
|
+
> **Level F.** Read first: [Actions, jobs and modules](actions.md).
|
|
4
|
+
|
|
3
5
|
Use this page in development with modules and hot reload. Skip until you mount domains with `modulePlugin`.
|
|
4
6
|
|
|
5
7
|
When a file is edited, the bundler loads its new version and runs it again, while the page and the rest of the code stay alive. For a bus that can go wrong in three ways:
|
|
@@ -37,11 +39,43 @@ bus.use(devPlugin());
|
|
|
37
39
|
|
|
38
40
|
- **Listeners that pile up.** When more than `maxListeners` (default `10`) listeners of one event were added and not unsubscribed, one warning names the event and the count. The count follows the function that `on` returns; `bus.off(event, fn)` is not seen.
|
|
39
41
|
- **Modules.** For the app of `modulePlugin` (or the `app` option) it logs `module "user" mounted`, `replaced` when a module of the same name was mounted, and `unmounted`. Only what goes through `app.mount` and `app.unmount` is reported, not sub-modules.
|
|
40
|
-
- **
|
|
42
|
+
- **Where a named listener was made.** For a listener with a `traceId`, it notes the file and the line of the `bus.on` call, while the listener is subscribed. [`graphLogger`](debugging-loops.md#read-a-report) shows it in a loop report, so you can open the handler that closes the loop.
|
|
43
|
+
- **Handlers that ignore the signal.** When a handler emits after its task was aborted (a newer call of `latest`, an unmounted module), the bus drops that emit. One warning then names the handler and the event: the handler did not stop when it was cancelled, usually because `ctx.signal` never reached the request. [Details](#find-a-handler-that-ignores-the-signal)
|
|
44
|
+
- **Options.** `abandoned: false`, `leaks: false` and `modules: false` switch a diagnostic off; `maxListeners` sets the limit; `log` receives the messages instead of the console.
|
|
41
45
|
- **Disconnect.** `bus.use(devPlugin(...))` returns the function that stops the diagnostics and puts `bus.on` and `app.mount` back.
|
|
42
46
|
|
|
43
47
|
It changes no behavior: it wraps `bus.on`, `app.mount`, and `app.unmount` and adds messages.
|
|
44
48
|
|
|
49
|
+
### Find a handler that ignores the signal
|
|
50
|
+
|
|
51
|
+
A policy such as `latest` cancels the older call by aborting its `ctx.signal`. A handler that does not pass the signal to its request keeps running, and its result arrives after the newer one. The policy drops that result, so nothing looks wrong until the handler does something else with it.
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { createBus } from "nanoeffects";
|
|
55
|
+
import { latest } from "nanoeffects/policies";
|
|
56
|
+
|
|
57
|
+
const bus = createBus();
|
|
58
|
+
|
|
59
|
+
bus.on(
|
|
60
|
+
"search",
|
|
61
|
+
latest(async (query, ctx) => {
|
|
62
|
+
// the signal is not passed, so an older request is not cancelled
|
|
63
|
+
const items = await fetch(`/api?q=${query}`).then((response) => response.json());
|
|
64
|
+
ctx.emit("results", items);
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
With `devPlugin`, the second `search` aborts the first call. When the first call then emits `results`, the bus drops the emit and the plugin warns once:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
[nanoeffects] the handler of "search" emitted "results" after its task was aborted, so the emit was dropped. The handler kept running after it was cancelled: pass ctx.signal to the request (fetch(url, { signal: ctx.signal })) or give ctx to an effect (retry(ctx, ...)).
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The check lives in the plugin only: the policies and the bus carry no code for it, and in production `nanoeffects/dev` is empty. It sees the handlers that emit or subscribe with `ctx.on` after the cancel; a handler that never does is not seen.
|
|
76
|
+
|
|
77
|
+
A linter finds the same mistake before the code runs, and much more of it: a handler with no `ctx`, a step that does not get the signal while another does, the calls of `Promise.all`, the callbacks of `retry` and `timeout`, a pause, a stream, `bus.emit` in a handler. [Keep the signal through a handler](pass-the-signal.md) lists every situation, what catches it and how to fix it, and shows the setup for ESLint, oxlint and Biome.
|
|
78
|
+
|
|
45
79
|
### Keep the import in production
|
|
46
80
|
|
|
47
81
|
`nanoeffects/dev` is a no-op in a production build. `package.json` maps it to an empty build under the `production` export condition (Vite and webpack set that in production mode), so `bus.use(devPlugin())` can stay without a guard: it touches nothing and costs a few bytes.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Cut a loop with `loopGuardPlugin`
|
|
2
|
+
|
|
3
|
+
> **Level F.** Read first: [Plugins](plugins.md#use-a-plugin). To find a loop before you cut it, see [Find and cut event loops](debugging-loops.md).
|
|
4
|
+
|
|
5
|
+
Two handlers that emit each other's events never stop. A loop made of `await` and timers burns CPU and network. A loop made of microtasks freezes the page, because the browser never gets a turn to draw. A report cannot stop any of them. `loopGuardPlugin` can: it follows the chain of emits and refuses the emit that would pass a limit.
|
|
6
|
+
|
|
7
|
+
Use it as a safety net, also in production. It needs no tracer and changes nothing while a chain stays under the limits. It is opt-in: a bus that does not use it behaves as before.
|
|
8
|
+
|
|
9
|
+
## On this page
|
|
10
|
+
|
|
11
|
+
- [Cut a loop](#cut-a-loop)
|
|
12
|
+
- [Choose what happens at the limit](#choose-what-happens-at-the-limit)
|
|
13
|
+
- [Limit the emits that come from outside](#limit-the-emits-that-come-from-outside)
|
|
14
|
+
- [Leave a recursion out](#leave-a-recursion-out)
|
|
15
|
+
- [Know what it does not do](#know-what-it-does-not-do)
|
|
16
|
+
|
|
17
|
+
## Cut a loop
|
|
18
|
+
|
|
19
|
+
The plugin counts how many times the same event is in the chain of emits that led to an emit. The chain follows `ctx.emit`, also across `await`, timers and microtasks. An emit that would pass a limit is refused, and the loop ends there. Other chains go on, and the next emit from outside starts a chain at zero.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// bus.js
|
|
23
|
+
import { createBus } from "nanoeffects";
|
|
24
|
+
import { loopGuardPlugin } from "nanoeffects/guard";
|
|
25
|
+
|
|
26
|
+
// loopGuardPlugin — refuses the emit that would pass a limit of a chain
|
|
27
|
+
export const bus = createBus().use(
|
|
28
|
+
loopGuardPlugin({
|
|
29
|
+
maxRepeats: 50, // one event may be in a chain 50 times
|
|
30
|
+
maxDepth: 1000, // a chain may be 1000 emits long, whatever the names are
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Default | Cuts |
|
|
36
|
+
| ------ | ------- | ---- |
|
|
37
|
+
| `maxRepeats` | 50 | The same event in one chain too often: a loop. |
|
|
38
|
+
| `maxDepth` | 1000 | A chain that is too long, also when every event has a new name (`step/${n}`). |
|
|
39
|
+
| `maxPerSecond` | none | Emits of one event from outside any handler that come too fast ([below](#limit-the-emits-that-come-from-outside)). |
|
|
40
|
+
| `ignore` | none | Event names that the repeat count skips ([below](#leave-a-recursion-out)). |
|
|
41
|
+
|
|
42
|
+
## Choose what happens at the limit
|
|
43
|
+
|
|
44
|
+
`onLimit` says what the refused emit does. Either way the plugin reports the finding once for each path. The report has the path of the loop and never the payload.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// bus.js
|
|
48
|
+
import { createBus } from "nanoeffects";
|
|
49
|
+
import { LoopError, loopGuardPlugin } from "nanoeffects/guard";
|
|
50
|
+
|
|
51
|
+
export const bus = createBus().use(
|
|
52
|
+
loopGuardPlugin({
|
|
53
|
+
onLimit: "drop", // "drop" (default): the emit is not sent; "throw": it throws a LoopError
|
|
54
|
+
// report — replaces the default console.error
|
|
55
|
+
report: (finding) => console.error(finding.limit, finding.path.join(" → ")),
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// with onLimit: "throw", the emit that was refused throws, and the error names the path
|
|
60
|
+
try {
|
|
61
|
+
bus.emit("cart/changed");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof LoopError) {
|
|
64
|
+
console.log(error.limit, error.path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- **`"drop"`** is the default. The emit returns as if nobody listened, so no new error appears in an async handler.
|
|
70
|
+
- **`"throw"`** shows the loop with a stack. In an async handler the error becomes a rejection: handle it with `onError`.
|
|
71
|
+
|
|
72
|
+
A loop that is cut can leave the state of the app half done: the handlers that ran have done their part, the ones after the cut have not. Fix the loop, and keep the guard as a net.
|
|
73
|
+
|
|
74
|
+
## Limit the emits that come from outside
|
|
75
|
+
|
|
76
|
+
A component that emits on every render has no chain: each emit comes from outside any handler. `maxPerSecond` counts them for each event.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
// bus.js
|
|
80
|
+
import { createBus } from "nanoeffects";
|
|
81
|
+
import { loopGuardPlugin } from "nanoeffects/guard";
|
|
82
|
+
|
|
83
|
+
// maxPerSecond — over 100 emits of one event in a second from outside are refused
|
|
84
|
+
export const bus = createBus().use(loopGuardPlugin({ maxPerSecond: 100 }));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
It is off by default, because a burst of many emits can be right (a stream of messages).
|
|
88
|
+
|
|
89
|
+
## Leave a recursion out
|
|
90
|
+
|
|
91
|
+
A chain that repeats on purpose is cut too. A handler that emits `tree/visit` for each level of a tree, or one that emits `tick` again after a timer for hours, passes `maxRepeats`. List its event in `ignore`. `maxDepth` still applies to it.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
// bus.js
|
|
95
|
+
import { createBus } from "nanoeffects";
|
|
96
|
+
import { loopGuardPlugin } from "nanoeffects/guard";
|
|
97
|
+
|
|
98
|
+
export const bus = createBus().use(
|
|
99
|
+
loopGuardPlugin({
|
|
100
|
+
ignore: ["tree/visit"], // names, or a function of the name
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Know what it does not do
|
|
106
|
+
|
|
107
|
+
- **It does not abort the chain.** It refuses one emit, at the edge where the limit is passed. The handlers that already run, run to the end.
|
|
108
|
+
- **It follows `ctx.emit`.** A `bus.emit` called from inside a handler is not part of the chain: it starts a new one with the counts at zero. Use `ctx.emit` inside handlers.
|
|
109
|
+
- **It does not find a loop.** To see the path of a loop, and loops that go through components, use `graphLogger` ([Find and cut event loops](debugging-loops.md)).
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# Coming from Effector
|
|
2
2
|
|
|
3
|
+
> **Level E, for readers who know Effector.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
|
|
4
|
+
|
|
3
5
|
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.
|
|
4
6
|
|
|
7
|
+
## On this page
|
|
8
|
+
|
|
9
|
+
- [Split the work](#split-the-work)
|
|
10
|
+
- [Find the equivalent](#find-the-equivalent)
|
|
11
|
+
- [Rewrite an effect](#rewrite-an-effect)
|
|
12
|
+
- [Keep the state in Zustand](#keep-the-state-in-zustand)
|
|
13
|
+
- [Rewrite a form](#rewrite-a-form)
|
|
14
|
+
- [Replace the log with a request](#replace-the-log-with-a-request)
|
|
15
|
+
- [Use both in one app](#use-both-in-one-app)
|
|
16
|
+
- [What is different](#what-is-different)
|
|
17
|
+
|
|
5
18
|
## Split the work
|
|
6
19
|
|
|
7
20
|
| Job | Use |
|
|
@@ -20,7 +33,7 @@ nanoeffects has no store. `createStore` can be replaced by any store you like, f
|
|
|
20
33
|
| event call `loaded(user)` | `bus.emit("loaded", user)` |
|
|
21
34
|
| `createEffect(handler)` | a handler: `bus.on(event, handler)` |
|
|
22
35
|
| effect with `done` and `fail` | [`createJob`](actions.md): emits the success and fail actions |
|
|
23
|
-
| `effect.pending` | `job.pending`, `job.success`, `job.fail` and `job.cancel`: set and clear the flag in your store ([Show a loading state](actions.md#
|
|
36
|
+
| `effect.pending` | `job.pending`, `job.success`, `job.fail` and `job.cancel`: set and clear the flag in your store ([Show a loading state](actions.md#the-state-of-a-job)) |
|
|
24
37
|
| `sample({ clock, fn, target })` | `bus.on(clock, handler)` and `ctx.emit(target, …)` |
|
|
25
38
|
| `sample({ source: $store, clock })` | read the store from a [context field](context.md) in the handler |
|
|
26
39
|
| `sample({ filter })` | an `if` in the handler |
|
|
@@ -318,7 +331,7 @@ bus.on(
|
|
|
318
331
|
## What is different
|
|
319
332
|
|
|
320
333
|
- **No state.** There is no `createStore`, `combine`, `restore`, `$store.map`. The bus holds listeners only. Take a store such as Zustand for state ([Stores](stores.md)).
|
|
321
|
-
- **`pending` is an action, not a store.** `createJob` sends `pending` when the handler starts, and `success`, `fail` or `cancel` when it ends; you keep the flag in your store ([Show a loading state](actions.md#
|
|
334
|
+
- **`pending` is an action, not a store.** `createJob` sends `pending` when the handler starts, and `success`, `fail` or `cancel` when it ends; you keep the flag in your store ([Show a loading state](actions.md#the-state-of-a-job)). Policies report to the job, so the flag also follows `latest`, `leading`, `debounce`, `throttle` and `queue`; that section says when to count instead. A running task is also visible through [Tracing](tracing.md).
|
|
322
335
|
- **Cancellation is the default tool.** An Effector effect runs every call to the end. Here `latest` and `ctx.signal` stop what is no longer needed.
|
|
323
336
|
- **Events are matched by name, not by identity.** An event is a string or a `createAction` creator, not a unit that you import and call. Two modules agree on the name.
|
|
324
337
|
- **No graph.** `sample` and `forward` build a static graph. A bus subscribes handlers at runtime and the call order is the order of `on`.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# Coming from mitt
|
|
1
|
+
# Coming from mitt and nanoevents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Level A, for readers who know mitt or nanoevents.** Read first: nothing: the bus is the same idea.
|
|
4
|
+
|
|
5
|
+
Read this page if your app uses a small event emitter (`mitt`, `nanoevents`, `tiny-emitter`) and its handlers start to do async work: requests, timers, retries. It maps the calls, shows what you gain, and shows what to check before you switch.
|
|
4
6
|
|
|
5
7
|
## When to switch
|
|
6
8
|
|
|
@@ -15,8 +17,8 @@ If handlers stay synchronous, keep your emitter. nanoeffects costs about 1.4 kB
|
|
|
15
17
|
|
|
16
18
|
## Find the equivalent
|
|
17
19
|
|
|
18
|
-
| mitt /
|
|
19
|
-
|
|
|
20
|
+
| mitt / nanoevents | nanoeffects |
|
|
21
|
+
| ----------------- | ----------- |
|
|
20
22
|
| `emitter.on("a", handler)` | `bus.on("a", handler)` |
|
|
21
23
|
| `emitter.off("a", handler)` | call the function that `bus.on` returned, or `bus.off("a")` for all handlers of `a` |
|
|
22
24
|
| `emitter.emit("a", payload)` | `bus.emit("a", payload)` |
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Use nanoeffects next to RxJS
|
|
2
2
|
|
|
3
|
+
> **Level E, for readers who know RxJS.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
|
|
4
|
+
|
|
3
5
|
Read this page if your project uses RxJS and you wonder what nanoeffects takes over. It does not replace RxJS. The two split the work: RxJS shapes streams of values, nanoeffects runs the side effects that events start. It maps the operators, shows a search box and a polling loop both ways, and lists what stays in RxJS.
|
|
4
6
|
|
|
5
7
|
## Split the work
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Coming from redux-saga
|
|
2
2
|
|
|
3
|
+
> **Level E, for readers who know redux-saga.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
|
|
4
|
+
|
|
3
5
|
Read this page if you know redux-saga and want to find the same job in nanoeffects. It maps the names, shows one saga rewritten, and lists what has no equivalent. It does not teach the library: start with the [Guide](guide.md).
|
|
4
6
|
|
|
5
7
|
## Find the equivalent
|
package/docs/motivation.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# Why nanoeffects exists
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Level A.** Read first: [README](../README.md).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Read this page to see why the library exists. It moves business logic and side effects out of React, Vue or Svelte components into plain `async` functions, with cancellation, retries and a context for dependencies. It does not teach the API: start with the [Guide](guide.md).
|
|
6
|
+
|
|
7
|
+
## Moving logic out of the UI
|
|
6
8
|
|
|
7
9
|
Components that hold requests, timers and races are hard to test and easy to break. The usual cure is services and pure functions outside the UI, and then every service needs the same things: to cancel work that is no longer wanted, to retry, to share dependencies, and to show what ran and why.
|
|
8
10
|
|
|
9
|
-
The common tools ask a lot: redux-saga writes this logic as generators, and RxJS as streams and operators. nanoeffects keeps ordinary `async` functions and adds one idea, the `AbortSignal`.
|
|
11
|
+
The common tools ask a lot: redux-saga writes this logic as generators, and RxJS as streams and operators. nanoeffects keeps ordinary `async` functions and adds one idea, the `AbortSignal`.
|
|
12
|
+
|
|
13
|
+
Another way to take logic out of the UI is a dependency injection (DI) container, such as the one in Angular or in any DI library. nanoeffects has no container, but it can do dependency injection at the level of the contract: a handler takes what it needs from `ctx`, and the bus decides what to give. See [Context fields](context.md).
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
The event bus is the heart of the library but not its value: any emitter can send events (`nanoevents` is about 100 bytes, and there are [dozens more](https://github.com/gajus/sister#similar-libraries)). What nanoeffects adds is cancellable tasks, policies and effects, a context that works as a small container for dependencies, actions and modules to group a feature, and [tracing](tracing.md) with an adapter for Redux DevTools. It also fits what a project already has: recipes and small adapters for [stores](stores.md) (Redux Toolkit, Zustand, MobX, Pinia), an [RxJS](migrating-from-rxjs.md) bridge, and helpers for [React](react.md), [Vue](vue.md) and [Svelte](svelte.md), so nothing has to be rewritten to start.
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
## Micro-frontends
|
|
18
|
+
|
|
19
|
+
In micro-frontends every app can bring its own versions of the same libraries. State and event libraries are the worst case, because they keep **internal state and identity** (a store, a context, a class). Two copies or two versions of them do not merge: they silently stop seeing each other. Different versions and copies also do more than work out of step: they load more code and use more resources than one agreed copy would. So the goal is a project that is as simple as possible, with as few shared dependencies that hold state as possible.
|
|
14
20
|
|
|
15
21
|
Module Federation makes this visible. Each remote can bring its own copy unless every team agrees on a `singleton` and a version range. The [Nx guide on library versions](https://nx.dev/docs/kb/manage-library-versions-with-module-federation) lists the result: libraries with internal state conflict, and a missing method in one version breaks callers written for another.
|
|
16
22
|
|
|
@@ -23,25 +29,12 @@ Real cases from the libraries nanoeffects was compared with:
|
|
|
23
29
|
| Effector | A library that uses `effector-react/scope` and is bundled into another project gets two instances. Its `Provider` is not seen by `useStore` of the app: "no scope found". | [effector#727](https://github.com/effector/effector/issues/727) |
|
|
24
30
|
| RxJS | Two copies give two `Observable` classes. Types of one version do not fit the other, and operators added to one copy are missing on the other. The advice is "one version in `node_modules`". | [RxJS#2984](https://github.com/ReactiveX/rxjs/issues/2984), [neo4j-javascript-driver#978](https://github.com/neo4j/neo4j-javascript-driver/issues/978) |
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Why not the alternatives
|
|
32
|
+
nanoeffects has no dependencies and no module-level state: a bus is an object you create. Two copies, or two versions, of the package in one page are two independent buses. Nothing is registered globally, no `Provider` is needed, and the host app does not have to pick a version for you.
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
| ------ | ------------------ |
|
|
32
|
-
| Redux (with sagas) | A store and a provider shared by every consumer, which is the risk above. `redux-saga` also has no cheap way to get an answer back from an event: effects talk through the store. |
|
|
33
|
-
| Effector | A graph with global identity. A version or scope mismatch between the SDK and the app reaches the whole infrastructure. |
|
|
34
|
-
| Zustand | Good state, but it is a store, not a way to run and cancel work. It is the natural place to hold state next to nanoeffects: see [Stores](stores.md). |
|
|
35
|
-
| RxJS | Flexible, but learning its ecosystem (operators, subjects, schedulers) is a high entry cost for every developer who touches the SDK. The package is also not small: [Bundlephobia](https://bundlephobia.com/package/rxjs) reports 17.3 kB minified and gzipped for 7.8.2. |
|
|
36
|
-
|
|
37
|
-
## What the library does instead
|
|
34
|
+
## What the library does
|
|
38
35
|
|
|
39
36
|
- **No dependencies, no global state.** It is safe to ship inside an SDK next to whatever the host app runs. It does not need a `singleton` entry in the Module Federation config.
|
|
40
37
|
- **Small enough for a strict budget.** SDKs often get a few kilobytes for all third-party code. `createBus` is about 1.4 kB gzip (1.3 kB brotli); policies, effects and actions are separate entries you import only when you need them. See [Package entries](package.md).
|
|
41
|
-
- **
|
|
38
|
+
- **Easy to start.** `on` and `emit` are the whole model. Cancellation, retry and the rest are plain functions on top. The [Guide](guide.md) and [Coming from redux-saga](migrating-from-saga.md) map the familiar names.
|
|
42
39
|
- **`emit` can return an answer.** An [action with a handler](actions.md) returns a result, so one module can ask another and wait for the reply. Events alone cannot do that, and it was the main thing missing from the Redux-based options.
|
|
43
40
|
- **Easy to debug.** Event-driven code is hard to follow: you see an effect and not the event that caused it. Every task knows its parent, so [tracing](tracing.md) shows the chain of events, with loggers for the console, the Performance panel and Redux DevTools.
|
|
44
|
-
|
|
45
|
-
## When this is not the right tool
|
|
46
|
-
|
|
47
|
-
nanoeffects holds no state. If you need a store, keep Redux, Zustand or Effector and let the bus run the async work: [Stores](stores.md). If you already have streams everywhere, keep RxJS: [Use nanoeffects next to RxJS](migrating-from-rxjs.md).
|
package/docs/observable.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# Turn events into Observables
|
|
2
2
|
|
|
3
|
+
> **Level F.** Read first: [Guide](guide.md#1-create-a-bus) step 1.
|
|
4
|
+
|
|
3
5
|
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.
|
|
4
6
|
|
|
7
|
+
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).
|
|
8
|
+
|
|
5
9
|
`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.
|
|
6
10
|
|
|
7
11
|
## Turn an event into a stream
|
|
@@ -145,6 +149,47 @@ bus.on(
|
|
|
145
149
|
|
|
146
150
|
`switchMap` drops the old inner stream. `latest` aborts the old task through `signal`, so a `fetch` that got the signal really stops.
|
|
147
151
|
|
|
152
|
+
## Read an event with a `for await` loop
|
|
153
|
+
|
|
154
|
+
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.
|
|
155
|
+
|
|
156
|
+
`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.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { createBus } from "nanoeffects";
|
|
160
|
+
import { events } from "nanoeffects/observable";
|
|
161
|
+
|
|
162
|
+
const bus = createBus();
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
|
|
165
|
+
// events — the "query/changed" payloads one by one; keeps the last 3 while the loop is busy
|
|
166
|
+
const queries = events(bus, "query/changed", {
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
buffer: 3,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
async function watchQueries() {
|
|
172
|
+
for await (const query of queries) {
|
|
173
|
+
console.log("search for", query); // runs once per payload, in order
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
watchQueries().catch((reason) => console.log("stopped:", reason)); // the abort rejects the loop
|
|
178
|
+
bus.emit("query/changed", "cat");
|
|
179
|
+
controller.abort("screen closed"); // the loop ends with this reason
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
| Option | What it does | Default |
|
|
183
|
+
| ------ | ------------ | ------- |
|
|
184
|
+
| `signal` | Ends the loop. The waiting read rejects with the abort reason. | none |
|
|
185
|
+
| `buffer` | The most payloads kept while the loop is busy. | `Infinity` |
|
|
186
|
+
| `overflow` | Which payload goes when the buffer is full: `"drop-oldest"` or `"drop-newest"`. | `"drop-oldest"` |
|
|
187
|
+
|
|
188
|
+
- `break`, `return` and a throw inside the loop remove the listener.
|
|
189
|
+
- With no `buffer`, a loop that never reads keeps every payload. Set `buffer` for a slow loop.
|
|
190
|
+
- An event of a bus does not end, so the loop ends only by `break`, the `signal` or the task.
|
|
191
|
+
- Inside a task, `events(ctx, "query/changed")` needs no `signal`: the task ending is the end.
|
|
192
|
+
|
|
148
193
|
## Convert back to a stream or a promise
|
|
149
194
|
|
|
150
195
|
| You need | Write |
|
package/docs/oop.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Use the bus from a class
|
|
2
2
|
|
|
3
|
+
> **Level D.** Read first: [Guide](guide.md#1-create-a-bus) step 1.
|
|
4
|
+
|
|
3
5
|
Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods. There are two ways to give a class the methods of a bus, and both remember what each instance subscribed to, so `dispose()` ends it all:
|
|
4
6
|
|
|
5
7
|
| You want | Use |
|
|
@@ -7,7 +9,24 @@ Use this page when **your domain logic lives in classes**: a service, a store or
|
|
|
7
9
|
| every `new Service()` to be an independent emitter | `extends Emitter` |
|
|
8
10
|
| the instances to share a bus, or to use plugins | `extends extendBus(bus)` |
|
|
9
11
|
|
|
10
|
-
If your logic is plain functions, stay with [modules](actions.md#
|
|
12
|
+
If your logic is plain functions, stay with [modules](actions.md#modules): a module is the same idea without a class.
|
|
13
|
+
|
|
14
|
+
## On this page
|
|
15
|
+
|
|
16
|
+
- [The problem without it](#the-problem-without-it)
|
|
17
|
+
- [Make an independent emitter with `Emitter`](#make-an-independent-emitter-with-emitter)
|
|
18
|
+
- [Type the events (optional)](#type-the-events-optional)
|
|
19
|
+
- [Give a class the methods of a shared bus](#give-a-class-the-methods-of-a-shared-bus)
|
|
20
|
+
- [Extend the result with no class of your own](#extend-the-result-with-no-class-of-your-own)
|
|
21
|
+
- [Let the class make its bus](#let-the-class-make-its-bus)
|
|
22
|
+
- [End the subscriptions with `dispose`](#end-the-subscriptions-with-dispose)
|
|
23
|
+
- [Stream an event with a `Subject`](#stream-an-event-with-a-subject)
|
|
24
|
+
- [Keep a method of your own](#keep-a-method-of-your-own)
|
|
25
|
+
- [Subscribe a method with `@On` and `@Once`](#subscribe-a-method-with-on-and-once)
|
|
26
|
+
- [Decorate the class with `@WithBus`](#decorate-the-class-with-withbus)
|
|
27
|
+
- [Pass a method by hand](#pass-a-method-by-hand)
|
|
28
|
+
- [Type the instances (optional)](#type-the-instances-optional)
|
|
29
|
+
- [What is not done](#what-is-not-done)
|
|
11
30
|
|
|
12
31
|
## The problem without it
|
|
13
32
|
|