nanoeffects 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -30
- package/dist/{action-creator-Bpa8BUS9.d.ts → action-creator-CcAEvUnw.d.ts} +90 -23
- package/dist/actions.d.ts +54 -21
- package/dist/actions.js +1 -1
- package/dist/bus-DlpEluzd.js +1 -0
- package/dist/{context-types-BKIH5a8s.d.ts → context-types-hCKgU8sC.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/dev.d.ts +2 -2
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/event-DVcW0mME.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/{module-B6rg5NIb.d.ts → module-C_oEXw1n.d.ts} +40 -57
- package/dist/notrace/{action-creator-D2CotugT.d.ts → action-creator-DxOvVo8I.d.ts} +90 -23
- package/dist/notrace/actions.d.ts +114 -98
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-DmKn4SDD.js +1 -0
- 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-Dz6hq8Jg.d.ts → plugin-lJjjd-SN.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +6 -6
- package/dist/oop.d.ts +2 -2
- package/dist/oop.js +1 -1
- package/dist/{plugin-D-j1DarG.d.ts → plugin-CuvIgjnX.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +2 -2
- package/dist/signal.d.ts +1 -1
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +1 -1
- package/dist/{types-Bkuhnkq6.d.ts → types-FV8fuvI9.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-BeH8wCkX.d.ts → with-policy-3bhFLEXs.d.ts} +1 -1
- package/docs/README.md +5 -4
- package/docs/actions.md +94 -53
- package/docs/chat-example.md +8 -8
- package/docs/chat-widget.md +135 -0
- package/docs/context.md +1 -1
- package/docs/errors.md +1 -1
- package/docs/guide.md +26 -26
- package/docs/hmr.md +1 -1
- package/docs/motivation.md +10 -4
- package/docs/oop.md +1 -1
- package/docs/package.md +2 -2
- package/docs/plugins.md +23 -23
- package/docs/query-libraries.md +14 -14
- package/docs/scope.md +22 -22
- package/docs/server.md +12 -12
- package/docs/stability.md +2 -2
- package/docs/starter.md +3 -3
- package/docs/tracing.md +5 -5
- package/docs/typescript.md +16 -16
- package/llms-full.txt +380 -194
- package/llms.txt +3 -2
- package/package.json +5 -4
- package/dist/bus-hHp-T7JV.js +0 -1
- package/dist/event-0RIBGYqS.js +0 -1
- package/dist/notrace/bus-BdyvumpX.js +0 -1
- package/dist/notrace/promise-CSLpzwSR.js +0 -1
package/llms-full.txt
CHANGED
|
@@ -17,7 +17,7 @@ Source: docs/guide.md
|
|
|
17
17
|
|
|
18
18
|
# Guide
|
|
19
19
|
|
|
20
|
-
Climb one step at a time: small bus → policies → effects →
|
|
20
|
+
Climb one step at a time: small bus → policies → effects → handlers and modules. Stop when your project has what it needs.
|
|
21
21
|
|
|
22
22
|
| Your project | Stop after |
|
|
23
23
|
| ------------ | ---------- |
|
|
@@ -48,7 +48,7 @@ bus.emit("greet", "Bob"); // nobody listens
|
|
|
48
48
|
|
|
49
49
|
Handlers run at once, in subscription order.
|
|
50
50
|
|
|
51
|
-
`bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus.
|
|
51
|
+
`bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus. `bus.get("greet")` gives the listeners that event has right now; `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`.
|
|
52
52
|
|
|
53
53
|
```js
|
|
54
54
|
import { createBus } from "nanoeffects";
|
|
@@ -132,7 +132,7 @@ bus.emit(userFailed("boom")); // user/failed boom
|
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
- `once(fn, handler)` waits for the first event that `fn` accepts. `off(fn)` removes every handler of `fn`, and `off(fn, handler)` removes one.
|
|
135
|
-
- An action creator
|
|
135
|
+
- An action creator still means that one event: only a function with no `type` is a function that picks events.
|
|
136
136
|
- The cost: the handler listens to `"*"`, so each event makes one task for each such subscription, also when the function says no, and an event that only such a subscription listens to counts as heard for `onUnhandled`. For a few known events, `on(creator, handler)` is cheaper.
|
|
137
137
|
- It works in any order with the other plugins, `devPlugin` included.
|
|
138
138
|
|
|
@@ -320,7 +320,7 @@ More in [Effects](effects.md).
|
|
|
320
320
|
|
|
321
321
|
## 7. Name events three ways
|
|
322
322
|
|
|
323
|
-
`emit` and `on` take a string, an object `{ type, payload }` (a Redux action fits), or a function with a `type` property (action creators and
|
|
323
|
+
`emit` and `on` take a string, an object `{ type, payload }` (a Redux action fits), or a function with a `type` property (action creators and actions).
|
|
324
324
|
|
|
325
325
|
From here on the bus is kept in one file and the events in files named after their domain (`move.js`, `math.js`), as in a real app: the events can then be imported anywhere without importing the bus.
|
|
326
326
|
|
|
@@ -341,10 +341,10 @@ export const moved = createAction("moved");
|
|
|
341
341
|
|
|
342
342
|
```js
|
|
343
343
|
// math.js
|
|
344
|
-
import {
|
|
344
|
+
import { createAction } from "nanoeffects/actions";
|
|
345
345
|
|
|
346
|
-
//
|
|
347
|
-
export const double =
|
|
346
|
+
// createAction with a handler — emit runs the handler and returns its result
|
|
347
|
+
export const double = createAction("math/double", (value) => value * 2);
|
|
348
348
|
```
|
|
349
349
|
|
|
350
350
|
```js
|
|
@@ -361,36 +361,36 @@ bus.on(moved, ({ x }) => console.log(x));
|
|
|
361
361
|
bus.emit(double(21)); // 42
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
-
Use strings for the plain case. Use objects when other code already builds actions.
|
|
364
|
+
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`.
|
|
365
365
|
|
|
366
|
-
## 8.
|
|
366
|
+
## 8. Bind a handler to an action later
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
An action may carry its handler, as `double` above, or get it later from a registry — another file, or a fake in a test. An action with no handler is an event until one is bound.
|
|
369
369
|
|
|
370
370
|
```js
|
|
371
371
|
// user.js
|
|
372
|
-
import {
|
|
372
|
+
import { createAction } from "nanoeffects/actions";
|
|
373
373
|
|
|
374
|
-
export const getUser =
|
|
374
|
+
export const getUser = createAction("user/get"); // no handler yet: an event
|
|
375
375
|
```
|
|
376
376
|
|
|
377
377
|
```js
|
|
378
378
|
// bus.js
|
|
379
379
|
import { createBus } from "nanoeffects";
|
|
380
|
-
import {
|
|
380
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
381
381
|
|
|
382
382
|
export const bus = createBus();
|
|
383
|
-
//
|
|
384
|
-
export const
|
|
383
|
+
// actionsPlugin — registry of handlers for this bus
|
|
384
|
+
export const actions = bus.use(actionsPlugin());
|
|
385
385
|
```
|
|
386
386
|
|
|
387
387
|
```js
|
|
388
|
-
import { bus,
|
|
388
|
+
import { bus, actions } from "./bus";
|
|
389
389
|
import { getUser } from "./user";
|
|
390
390
|
|
|
391
|
-
bus.emit(getUser("1")); //
|
|
391
|
+
bus.emit(getUser("1")); // nothing is bound: only an event
|
|
392
392
|
|
|
393
|
-
const unregister =
|
|
393
|
+
const unregister = actions.handle(getUser, (id) => api.user(id));
|
|
394
394
|
const user = await bus.emit(getUser("1"));
|
|
395
395
|
|
|
396
396
|
unregister();
|
|
@@ -400,7 +400,7 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
|
|
|
400
400
|
|
|
401
401
|
## 9. Mount a domain as a module
|
|
402
402
|
|
|
403
|
-
**Problem.** A feature owns several things on the bus: who answers `getUser`, who listens to `greet`, maybe
|
|
403
|
+
**Problem.** A feature owns several things on the bus: who answers `getUser`, who listens to `greet`, maybe an action that already carries its handler. With plain `bus.on` / `actions.handle` you must remember every unsubscribe when the feature goes away. Miss one and listeners pile up.
|
|
404
404
|
|
|
405
405
|
**When.** Domain logic lives on the bus, off the UI, and you need one mount / one unmount for that feature. Skip modules while a few top-level `bus.on` calls are enough.
|
|
406
406
|
|
|
@@ -408,16 +408,16 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
|
|
|
408
408
|
|
|
409
409
|
```js
|
|
410
410
|
// user.js
|
|
411
|
-
import {
|
|
411
|
+
import { createAction } from "nanoeffects/actions";
|
|
412
412
|
|
|
413
|
-
export const getUser =
|
|
413
|
+
export const getUser = createAction("user/get"); // no handler yet
|
|
414
414
|
```
|
|
415
415
|
|
|
416
416
|
```js
|
|
417
417
|
// math.js
|
|
418
|
-
import {
|
|
418
|
+
import { createAction } from "nanoeffects/actions";
|
|
419
419
|
|
|
420
|
-
export const double =
|
|
420
|
+
export const double = createAction("math/double", (value) => value * 2); // carries its handler
|
|
421
421
|
```
|
|
422
422
|
|
|
423
423
|
```js
|
|
@@ -434,7 +434,7 @@ export const greeting = createModule("greeting", (m) => {
|
|
|
434
434
|
// m.handle — this module answers getUser while mounted
|
|
435
435
|
m.handle(getUser, (id) => api.user(id));
|
|
436
436
|
|
|
437
|
-
// m.use — register
|
|
437
|
+
// m.use — register an action that already has a handler, for this module’s life
|
|
438
438
|
m.use(double);
|
|
439
439
|
});
|
|
440
440
|
```
|
|
@@ -459,8 +459,8 @@ app.unmount(greeting); // greet listener and getUser binding are gone
|
|
|
459
459
|
```
|
|
460
460
|
|
|
461
461
|
- **`m.on`** — same job as `bus.on`, scoped to the module’s lifetime.
|
|
462
|
-
- **`m.handle`** — bind a handler to
|
|
463
|
-
- **`m.use`** — bring
|
|
462
|
+
- **`m.handle`** — bind a handler to an action that has none; gone on unmount.
|
|
463
|
+
- **`m.use`** — bring an action that already carries a handler (or a sub-module). Prefer `handle` when the handler must die with the feature.
|
|
464
464
|
|
|
465
465
|
Modules also make [hot reload](hmr.md) work. Full API in [Actions](actions.md); a longer recipe in [`examples/modules.ts`](../examples/modules.ts).
|
|
466
466
|
|
|
@@ -563,7 +563,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
563
563
|
| `react` | The same handler with `useEmitter` and `useEventState`, and `usePolicy` for a debounced save in a component | You build a React app |
|
|
564
564
|
| `vanilla-ts` | The `vanilla` app in TypeScript | You want the compiler to check events and handlers |
|
|
565
565
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
566
|
-
| `domains-ts` |
|
|
566
|
+
| `domains-ts` | Actions with handlers, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
|
|
567
567
|
| `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
|
|
568
568
|
| `svelte-ts` | The `react-ts` app in Svelte 5, with `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
|
|
569
569
|
|
|
@@ -611,13 +611,13 @@ npm create nanoeffects@latest my-app -- --template domains-ts
|
|
|
611
611
|
| ---- | --- |
|
|
612
612
|
| `src/bus.ts` | Creates the bus. It imports no events and no domain. |
|
|
613
613
|
| `src/app.ts` | Mounts the domains with `modulePlugin`. |
|
|
614
|
-
| `src/domains/catalog/` | Searching products: `catalog.ts` (events and a
|
|
614
|
+
| `src/domains/catalog/` | Searching products: `catalog.ts` (events and an action with a handler), `catalog-handlers.ts` (`debounce` and `latest`), `catalog-module.ts`. |
|
|
615
615
|
| `src/domains/checkout/` | Placing an order: `checkout.ts` (a job), `checkout-handlers.ts` (`leading` and `retry`), `checkout-store.ts` (state written from the job's outcomes), `checkout-module.ts`. |
|
|
616
616
|
| `src/ui/` | The components. None of them calls the fake server in `src/api.ts`. |
|
|
617
617
|
|
|
618
618
|
For a bigger example, a chat in which parts react to each other, see [Build a chat with entities, features, modules and a small server](chat-example.md). It has more layers than `domains-ts`: when domains must react to each other, it keeps them from importing each other by splitting them into entities and features.
|
|
619
619
|
|
|
620
|
-
The
|
|
620
|
+
The actions, jobs and modules themselves are explained in [Actions, jobs and modules](actions.md). The template keeps found products in a Context provider with one `useEventState`, and the order state in a store of its own: `useEventState` is [not a store](react.md#show-the-last-value-of-an-event-as-state).
|
|
621
621
|
|
|
622
622
|
## Read the files
|
|
623
623
|
|
|
@@ -643,9 +643,15 @@ Source: docs/motivation.md
|
|
|
643
643
|
|
|
644
644
|
# Why nanoeffects exists
|
|
645
645
|
|
|
646
|
-
Read this page
|
|
646
|
+
Read this page to see why the library exists and when to pick it. The main reason is to take business logic and side effects out of React, Vue or Svelte components and write them as plain `async` functions, with cancellation, retries and a context for dependencies. The second part of the page is for SDKs, core libraries and micro-frontend shells, where a shared dependency is a shared risk. It does not teach the API: start with the [Guide](guide.md).
|
|
647
|
+
|
|
648
|
+
## The main reason: logic outside the UI
|
|
649
|
+
|
|
650
|
+
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.
|
|
651
|
+
|
|
652
|
+
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`. The event bus is its heart 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.
|
|
647
653
|
|
|
648
|
-
##
|
|
654
|
+
## When the constraint is a shared library
|
|
649
655
|
|
|
650
656
|
A shared library (an SDK, a core package, a platform shell) is loaded by many apps. Whatever it depends on, the apps depend on too. 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.
|
|
651
657
|
|
|
@@ -674,9 +680,9 @@ A library that is **not** a shared singleton cannot break this way. nanoeffects
|
|
|
674
680
|
## What the library does instead
|
|
675
681
|
|
|
676
682
|
- **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.
|
|
677
|
-
- **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
|
|
683
|
+
- **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).
|
|
678
684
|
- **Simple first.** `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.
|
|
679
|
-
- **`emit` can return an answer.**
|
|
685
|
+
- **`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.
|
|
680
686
|
- **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.
|
|
681
687
|
|
|
682
688
|
## When this is not the right tool
|
|
@@ -695,12 +701,12 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
695
701
|
| `nanoeffects` | `createBus`, the types |
|
|
696
702
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
697
703
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
698
|
-
| `nanoeffects/actions` | `createAction`, `
|
|
704
|
+
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` |
|
|
699
705
|
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
|
|
700
706
|
| `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
|
|
701
707
|
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
|
|
702
708
|
| `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()` |
|
|
703
|
-
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one
|
|
709
|
+
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) |
|
|
704
710
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
705
711
|
| `nanoeffects/observable` | `createObserve`, `Subject` |
|
|
706
712
|
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
|
|
@@ -737,7 +743,7 @@ A stable entry follows the rules below. An unstable entry can change in any mino
|
|
|
737
743
|
| `nanoeffects` | Stable | `createBus` and the types of a bus |
|
|
738
744
|
| `nanoeffects/effects` | Stable | `retry`, `timeout`, `poll`, `take`, `delay`, `all`, `allSettled`, `race`, `backoff`, `cancellable`, `effectsPlugin` |
|
|
739
745
|
| `nanoeffects/policies` | Stable | `latest`, `leading`, `debounce`, `throttle`, `queue`, `withPolicy` |
|
|
740
|
-
| `nanoeffects/actions` | Stable | `createAction`, `
|
|
746
|
+
| `nanoeffects/actions` | Stable | `createAction`, `createJob`, `createModule` and their plugins |
|
|
741
747
|
| `nanoeffects/trace` | Stable: the contract | `Tracer`, `Trace`, `TraceEvent`, `TraceLogger`, `Phase`, `EffectName` and `tracePlugin` |
|
|
742
748
|
| `nanoeffects/trace` | Unstable: the tools | `createTracer`, `consoleLogger`, `performanceLogger`, `createTraceRecorder`, `reduxDevtools` and their option types |
|
|
743
749
|
| `nanoeffects/scope` | Unstable | `scopePlugin`, `effect` |
|
|
@@ -780,7 +786,7 @@ A deprecated export keeps working and is removed only in a major release. `RaceS
|
|
|
780
786
|
| ---- | --------- |
|
|
781
787
|
| Node.js, the library | 18, 20, 22 and 24 (a smoke test runs the built package on each) |
|
|
782
788
|
| Node.js, the dev tools and the trace in a browser | 22 and newer |
|
|
783
|
-
| TypeScript | The
|
|
789
|
+
| TypeScript | 5.9 and 7. The built declaration files are type-checked with TypeScript 5.9 in CI, and the source with TypeScript 7. Older versions are not tested. |
|
|
784
790
|
| React | 18 or newer (`nanoeffects/react`) |
|
|
785
791
|
| Vue | 3.3 or newer (`nanoeffects/vue`) |
|
|
786
792
|
| Svelte | 5 or newer (`nanoeffects/svelte`) |
|
|
@@ -830,14 +836,14 @@ In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type o
|
|
|
830
836
|
|
|
831
837
|
## Main plugins
|
|
832
838
|
|
|
833
|
-
These four cover what most apps need: shared dependencies, effects,
|
|
839
|
+
These four cover what most apps need: shared dependencies, effects, handlers and modules. Start here.
|
|
834
840
|
|
|
835
841
|
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
836
842
|
| ------ | ----------------- | ------ | ----------------- |
|
|
837
843
|
| [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) | Your own fields (a store, a client, a logger) on `ctx` of every handler. | `nanoeffects` | The same bus, typed with the fields |
|
|
838
844
|
| [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) | `race`, `all`, `delay` and your own effects on `ctx`, called without passing `ctx`. | `nanoeffects/effects` | The same bus, typed with the effects |
|
|
839
|
-
| [`
|
|
840
|
-
| [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) | A module: the listeners, handlers and tasks of one domain, mounted and unmounted with one call. | `nanoeffects/actions` | The app `{ mount, unmount, isMounted, dispose,
|
|
845
|
+
| [`actionsPlugin`](#let-a-test-replace-a-handler-commandsplugin) | The handlers of actions in a registry, so a test can bind or replace one. | `nanoeffects/actions` | The registry `{ run, handle }` |
|
|
846
|
+
| [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) | A module: the listeners, handlers and tasks of one domain, mounted and unmounted with one call. | `nanoeffects/actions` | The app `{ mount, unmount, isMounted, dispose, actions, createModule }` |
|
|
841
847
|
|
|
842
848
|
### Put a store or a client on every handler: `contextPlugin`
|
|
843
849
|
|
|
@@ -874,27 +880,27 @@ bus.on("load", async (id, { race, delay }) => {
|
|
|
874
880
|
});
|
|
875
881
|
```
|
|
876
882
|
|
|
877
|
-
###
|
|
883
|
+
### Bind or replace a handler: `actionsPlugin`
|
|
878
884
|
|
|
879
|
-
|
|
885
|
+
Handlers of actions can live in a registry that belongs to the bus. An action can have no handler, and a test or another layer binds one. More in [Actions](actions.md#bind-a-handler-later).
|
|
880
886
|
|
|
881
887
|
```js
|
|
882
888
|
import { createBus } from "nanoeffects";
|
|
883
|
-
import {
|
|
889
|
+
import { actionsPlugin, createAction } from "nanoeffects/actions";
|
|
884
890
|
|
|
885
|
-
const getUser =
|
|
891
|
+
const getUser = createAction("user/get"); // no handler yet: an event
|
|
886
892
|
const bus = createBus();
|
|
887
893
|
|
|
888
|
-
//
|
|
889
|
-
const
|
|
894
|
+
// actionsPlugin — the registry of handlers of this bus
|
|
895
|
+
const actions = bus.use(actionsPlugin());
|
|
890
896
|
|
|
891
|
-
|
|
897
|
+
actions.handle(getUser, async (id) => ({ id })); // the latest binding wins
|
|
892
898
|
const user = await bus.emit(getUser("1")); // { id: "1" }
|
|
893
899
|
```
|
|
894
900
|
|
|
895
901
|
### Mount a domain as one unit: `modulePlugin`
|
|
896
902
|
|
|
897
|
-
A module groups the listeners, the bound
|
|
903
|
+
A module groups the listeners, the bound handlers and the background tasks of one domain, and ends all of them when it is unmounted. It also makes [hot reload](hmr.md) work. More in [Modules](actions.md#mount-domains-with-modules).
|
|
898
904
|
|
|
899
905
|
```js
|
|
900
906
|
import { createBus } from "nanoeffects";
|
|
@@ -914,7 +920,7 @@ Add one of these when you meet its need. None of them is required.
|
|
|
914
920
|
|
|
915
921
|
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
916
922
|
| ------ | ----------------- | ------ | ----------------- |
|
|
917
|
-
| [`scopePlugin`](#give-one-
|
|
923
|
+
| [`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 |
|
|
918
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` |
|
|
919
925
|
| [`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 |
|
|
920
926
|
| [`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` |
|
|
@@ -922,20 +928,20 @@ Add one of these when you meet its need. None of them is required.
|
|
|
922
928
|
| [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) | A React hook that emits when a value of a component changes. | `nanoeffects/react` | The same bus, with `useEmitter` |
|
|
923
929
|
| [`devPlugin`](#find-leaks-while-you-develop-devplugin) | Warnings about listeners that pile up, and messages about modules, while you develop. | `nanoeffects/dev` | A function that disconnects it |
|
|
924
930
|
|
|
925
|
-
### Give one
|
|
931
|
+
### Give one action its own fields: `scopePlugin`
|
|
926
932
|
|
|
927
|
-
|
|
933
|
+
An action with a handler, an action, a job or a module can carry its own fields and effects, and one `emit` can replace them. A test gives a fake to one call. More in [Scopes](scope.md).
|
|
928
934
|
|
|
929
935
|
```js
|
|
930
936
|
import { createBus } from "nanoeffects";
|
|
931
|
-
import {
|
|
937
|
+
import { createAction } from "nanoeffects/actions";
|
|
932
938
|
import { scopePlugin } from "nanoeffects/scope";
|
|
933
939
|
|
|
934
940
|
// scopePlugin — makes the scope of a command work on this bus
|
|
935
941
|
const bus = createBus().use(scopePlugin());
|
|
936
942
|
|
|
937
943
|
// the last argument: fields of this command only
|
|
938
|
-
const getUser =
|
|
944
|
+
const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
|
|
939
945
|
api,
|
|
940
946
|
});
|
|
941
947
|
|
|
@@ -949,13 +955,13 @@ A library such as React Query owns the request and gives you an `AbortSignal`. `
|
|
|
949
955
|
|
|
950
956
|
```js
|
|
951
957
|
import { createBus } from "nanoeffects";
|
|
952
|
-
import {
|
|
958
|
+
import { createAction } from "nanoeffects/actions";
|
|
953
959
|
import { signalPlugin } from "nanoeffects/signal";
|
|
954
960
|
|
|
955
961
|
// signalPlugin — emit takes { signal } for a command, a job or an action
|
|
956
962
|
const bus = createBus().use(signalPlugin());
|
|
957
963
|
|
|
958
|
-
const getUser =
|
|
964
|
+
const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
|
|
959
965
|
|
|
960
966
|
const controller = new AbortController();
|
|
961
967
|
bus.emit(getUser("1"), { signal: controller.signal }); // runs the command
|
|
@@ -1062,7 +1068,7 @@ One bus with every plugin that does not need React, in a file of its own. The ev
|
|
|
1062
1068
|
import { createBus } from "nanoeffects";
|
|
1063
1069
|
import { contextPlugin } from "nanoeffects/context";
|
|
1064
1070
|
import {
|
|
1065
|
-
|
|
1071
|
+
actionsPlugin,
|
|
1066
1072
|
emitterPlugin,
|
|
1067
1073
|
modulePlugin,
|
|
1068
1074
|
} from "nanoeffects/actions";
|
|
@@ -1078,8 +1084,8 @@ export const bus = createBus()
|
|
|
1078
1084
|
.use(matchPlugin()) // bus.on takes a function that picks the events
|
|
1079
1085
|
.use(emitterPlugin()); // bus.emitter(creator)
|
|
1080
1086
|
|
|
1081
|
-
//
|
|
1082
|
-
export const
|
|
1087
|
+
// actionsPlugin — the registry of action handlers; keep it to bind handlers later
|
|
1088
|
+
export const actions = bus.use(actionsPlugin());
|
|
1083
1089
|
|
|
1084
1090
|
// modulePlugin — mounts userModule at the end of this block; keep app to mount and unmount later
|
|
1085
1091
|
export const app = bus.use(modulePlugin([userModule]));
|
|
@@ -1094,12 +1100,12 @@ The plugins give the same result in any order. Use them as they read best, and p
|
|
|
1094
1100
|
|
|
1095
1101
|
Two things happen because modules are mounted late:
|
|
1096
1102
|
|
|
1097
|
-
- **Modules start at the end of the block.** `modulePlugin([userModule])` mounts `userModule` when the synchronous block that made it has finished, or earlier if the app or the bus is used first: the first `bus.emit`, or the first call of `app.mount`, `app.unmount`, `app.isMounted`, `app.dispose` or the read of `app.
|
|
1103
|
+
- **Modules start at the end of the block.** `modulePlugin([userModule])` mounts `userModule` when the synchronous block that made it has finished, or earlier if the app or the bus is used first: the first `bus.emit`, or the first call of `app.mount`, `app.unmount`, `app.isMounted`, `app.dispose` or the read of `app.actions`. By then every plugin used in the same block is on the bus, so a module and the tasks it spawns have the fields, effects and scopes of the plugins you used after `modulePlugin`, and `m.on(fn, handler)` works with `matchPlugin`. A plugin used after an `await` can come too late: use every plugin before the first `await`. With no module, `modulePlugin()` starts nothing until the first use.
|
|
1098
1104
|
- **Listeners keep their place.** A `bus.on` after `bus.use(modulePlugin(...))` is held until the modules are mounted, then made, in the order you wrote them. The listeners of a module therefore stay ahead of the ones registered after the plugin, and behind the ones registered before it. The function `bus.on` returns works before and after.
|
|
1099
1105
|
|
|
1100
1106
|
Other plugins do not need an order for another reason:
|
|
1101
1107
|
|
|
1102
|
-
- `
|
|
1108
|
+
- `actionsPlugin` binds its list in the registry that the bus has, so it can come before or after `modulePlugin`. A custom `bus.actions = { run }` is replaced.
|
|
1103
1109
|
- `effectsPlugin` and `contextPlugin` do not replace a field that is on `ctx` already, so a scope wins in any order.
|
|
1104
1110
|
- `devPlugin()` finds the app that `modulePlugin` put on the bus (`bus.app`). `devPlugin({ app })` still works.
|
|
1105
1111
|
- `matchPlugin` and `devPlugin` both wrap `bus.on`; `devPlugin` counts a function that picks events under `"*"`, and its `disconnect` leaves a wrapper that was put on top of it.
|
|
@@ -2125,7 +2131,7 @@ const bus = createBus({
|
|
|
2125
2131
|
|
|
2126
2132
|
## Warn when nobody listens
|
|
2127
2133
|
|
|
2128
|
-
A typo in an event name (`emit("serch")`) does nothing and says nothing. `onUnhandled` runs when an event is emitted and nobody listens (a listener of every event, `"*"`, counts). It is not called for
|
|
2134
|
+
A typo in an event name (`emit("serch")`) does nothing and says nothing. `onUnhandled` runs when an event is emitted and nobody listens (a listener of every event, `"*"`, counts). It is not called for actions, which always have a handler to run.
|
|
2129
2135
|
|
|
2130
2136
|
Use it as a warning in development:
|
|
2131
2137
|
|
|
@@ -2142,7 +2148,7 @@ const bus = createBus({
|
|
|
2142
2148
|
---
|
|
2143
2149
|
Source: docs/actions.md
|
|
2144
2150
|
|
|
2145
|
-
# Actions,
|
|
2151
|
+
# Actions, jobs and modules
|
|
2146
2152
|
|
|
2147
2153
|
Use this page when **domain logic lives on the bus**, off the UI: the UI emits and listens; handlers and modules own the work.
|
|
2148
2154
|
|
|
@@ -2154,7 +2160,9 @@ A chat with two entities and three features, jobs under policies and effects, an
|
|
|
2154
2160
|
|
|
2155
2161
|
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)).
|
|
2156
2162
|
|
|
2157
|
-
## Return a value from `emit` with a
|
|
2163
|
+
## Return a value from `emit` with a handler
|
|
2164
|
+
|
|
2165
|
+
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.
|
|
2158
2166
|
|
|
2159
2167
|
The bus lives in one file and its events in another, so any module can import both without a cycle.
|
|
2160
2168
|
|
|
@@ -2168,10 +2176,10 @@ export const bus = createBus();
|
|
|
2168
2176
|
|
|
2169
2177
|
```js
|
|
2170
2178
|
// math.js
|
|
2171
|
-
import {
|
|
2179
|
+
import { createAction } from "nanoeffects/actions";
|
|
2172
2180
|
|
|
2173
|
-
//
|
|
2174
|
-
export const double =
|
|
2181
|
+
// createAction with a handler — emit runs the handler and returns its result
|
|
2182
|
+
export const double = createAction("math/double", (value) => value * 2);
|
|
2175
2183
|
```
|
|
2176
2184
|
|
|
2177
2185
|
```js
|
|
@@ -2181,27 +2189,24 @@ import { double } from "./math";
|
|
|
2181
2189
|
bus.emit(double(21)); // 42
|
|
2182
2190
|
```
|
|
2183
2191
|
|
|
2184
|
-
## Describe events,
|
|
2192
|
+
## Describe events, handlers and jobs
|
|
2185
2193
|
|
|
2186
2194
|
`nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
|
|
2187
2195
|
|
|
2188
2196
|
```js
|
|
2189
2197
|
// user.js
|
|
2190
|
-
import { createAction,
|
|
2198
|
+
import { createAction, createJob } from "nanoeffects/actions";
|
|
2191
2199
|
|
|
2192
|
-
// createAction — event with a payload (like RTK createAction)
|
|
2200
|
+
// createAction — an event with a payload (like RTK createAction)
|
|
2193
2201
|
export const userLoaded = createAction("user/loaded");
|
|
2194
2202
|
export const loadFailed = createAction("user/failed");
|
|
2195
2203
|
|
|
2196
|
-
//
|
|
2197
|
-
export const getUser =
|
|
2204
|
+
// createAction with a handler — emit returns what the handler returns
|
|
2205
|
+
export const getUser = createAction("user/get", (id, { signal }) => {
|
|
2198
2206
|
return fetchUser(id, { signal });
|
|
2199
2207
|
});
|
|
2200
2208
|
|
|
2201
|
-
//
|
|
2202
|
-
export const sendMail = createCommand("mail/send");
|
|
2203
|
-
|
|
2204
|
-
// createJob — command that also emits success / fail actions
|
|
2209
|
+
// createJob — an action with a handler that also emits success / fail actions
|
|
2205
2210
|
export const loadUser = createJob("user/load", fetchUser, {
|
|
2206
2211
|
success: userLoaded,
|
|
2207
2212
|
fail: "user/failed",
|
|
@@ -2217,57 +2222,99 @@ These are the events of the user domain, in a file of their own (`user.js`). The
|
|
|
2217
2222
|
- `userLoaded(user)` → `{ type, payload }`
|
|
2218
2223
|
- `userLoaded.type` — the type string
|
|
2219
2224
|
- `userLoaded.match(action)` — narrows an action
|
|
2220
|
-
- `createAction(type,
|
|
2225
|
+
- `createAction(type, handler)` — a function as the second argument is the handler; `bus.emit(action(payload))` tells the listeners, runs the handler as a task and returns its result
|
|
2226
|
+
- `createAction(type, { prepare })` — build the payload from several arguments
|
|
2227
|
+
- `createAction(type, { handle, prepare, ...scope })` — everything in one object: `handle` and `prepare` are taken out, and every other field is the [scope](scope.md)
|
|
2221
2228
|
- Works as the event of its type: `bus.emit(userLoaded(user))`, `bus.on(userLoaded, fn)`
|
|
2222
2229
|
|
|
2223
|
-
|
|
2230
|
+
A scope goes after a handler as the last argument, `createAction(type, handler, scope)`; `prepare` with a handler goes in that last object: `createAction(type, handler, { prepare, ...scope })`.
|
|
2224
2231
|
|
|
2225
|
-
|
|
2232
|
+
- Wrap the handler in a policy: `createAction("search", latest(fn))`.
|
|
2226
2233
|
|
|
2227
|
-
|
|
2228
|
-
- `command.match(action)` narrows like an action creator’s `match`.
|
|
2234
|
+
An action with no handler is an event. Emitting it does not throw, and `onUnhandled` hears about it when nobody listens.
|
|
2229
2235
|
|
|
2230
|
-
|
|
2236
|
+
### Bind a handler later
|
|
2237
|
+
|
|
2238
|
+
**Problem.** The work belongs to another layer, or a test must replace it. Putting the handler in `createAction` then ties the event file to the work.
|
|
2239
|
+
|
|
2240
|
+
**When.** Use the registry when the handler is not known where the action is made. The action can have no handler at all; a handler bound to its type answers it. Each bus (each test) has its own registry.
|
|
2241
|
+
|
|
2242
|
+
```js
|
|
2243
|
+
// mail.js
|
|
2244
|
+
import { createAction } from "nanoeffects/actions";
|
|
2245
|
+
|
|
2246
|
+
// createAction without a handler — an event; the work is bound elsewhere
|
|
2247
|
+
export const sendMail = createAction("mail/send");
|
|
2248
|
+
```
|
|
2231
2249
|
|
|
2232
2250
|
```js
|
|
2233
2251
|
// bus.js
|
|
2234
2252
|
import { createBus } from "nanoeffects";
|
|
2235
|
-
import {
|
|
2236
|
-
import { sendMail } from "./
|
|
2253
|
+
import { actionsPlugin, setHandler } from "nanoeffects/actions";
|
|
2254
|
+
import { sendMail } from "./mail";
|
|
2237
2255
|
|
|
2238
2256
|
export const bus = createBus();
|
|
2239
2257
|
|
|
2240
|
-
//
|
|
2241
|
-
export const
|
|
2242
|
-
|
|
2258
|
+
// actionsPlugin — the registry of handlers of this bus; setHandler pairs an action with its work
|
|
2259
|
+
export const actions = bus.use(
|
|
2260
|
+
actionsPlugin([setHandler(sendMail, smtpSend)]),
|
|
2243
2261
|
);
|
|
2244
2262
|
```
|
|
2245
2263
|
|
|
2246
2264
|
```js
|
|
2247
|
-
import {
|
|
2265
|
+
import { actions } from "./bus";
|
|
2248
2266
|
import { getUser } from "./user";
|
|
2249
2267
|
|
|
2250
|
-
// later, e.g. in a test — latest binding wins
|
|
2251
|
-
const unbind =
|
|
2268
|
+
// later, e.g. in a test — the latest binding wins, a handler bound here wins over the one in createAction
|
|
2269
|
+
const unbind = actions.handle(getUser, async () => fakeUser);
|
|
2252
2270
|
unbind();
|
|
2253
2271
|
```
|
|
2254
2272
|
|
|
2255
|
-
A
|
|
2273
|
+
A handler bound in the registry makes `bus.emit(sendMail(mail))` run it and return its result, the same as for an action that carries its handler. The listeners of the type are told first. With no handler bound, `sendMail` is an event again.
|
|
2274
|
+
|
|
2275
|
+
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin` binds its list in that registry and returns it. There is no `actions` option in `createBus`; assign a custom runner with `bus.actions = { run }`, and add `bound(type)` to it to answer for actions that have no handler of their own.
|
|
2276
|
+
|
|
2277
|
+
### Say what a bound handler returns
|
|
2278
|
+
|
|
2279
|
+
TypeScript cannot see which handler the registry has, so the type of `emit` for an action without a handler is `void`. Tell it what the bound handler returns with `Call<Payload, Result>`. This is a promise of yours: nothing checks it against the handler.
|
|
2280
|
+
|
|
2281
|
+
```ts
|
|
2282
|
+
// mail.ts
|
|
2283
|
+
import type { Call } from "nanoeffects";
|
|
2284
|
+
import { createAction } from "nanoeffects/actions";
|
|
2285
|
+
|
|
2286
|
+
// Call — payload Mail, a bound handler answers with a boolean (checker only; nothing runs differently)
|
|
2287
|
+
type Mail = { to: string };
|
|
2288
|
+
export const sendMail = createAction<Call<Mail, boolean>>("mail/send");
|
|
2289
|
+
```
|
|
2290
|
+
|
|
2291
|
+
In an event map the same type does the job for string events, and `emit` returns `boolean`:
|
|
2256
2292
|
|
|
2257
|
-
|
|
2293
|
+
```ts
|
|
2294
|
+
// events.ts
|
|
2295
|
+
import type { Call } from "nanoeffects";
|
|
2296
|
+
|
|
2297
|
+
// Call in the map — the payload is checked, emit returns the result (checker only)
|
|
2298
|
+
export type Events = {
|
|
2299
|
+
"user/getId": Call<void, string>;
|
|
2300
|
+
"user/loaded": { id: string };
|
|
2301
|
+
};
|
|
2302
|
+
```
|
|
2303
|
+
|
|
2304
|
+
On a bus without an event map you can say it where you call: `const id = bus.emit<string>("user/getId")`. A bus with a map takes no such hint, so its names and payloads stay checked.
|
|
2258
2305
|
|
|
2259
2306
|
### Jobs
|
|
2260
2307
|
|
|
2261
|
-
`createJob(type, handler, { pending, success, fail, cancel })` makes a
|
|
2308
|
+
`createJob(type, handler, { pending, success, fail, cancel })` makes an action with a handler that also sends its outcome as actions, when nobody waits for the result. It returns the action with `loadUser.pending`, `loadUser.success`, `loadUser.fail` and `loadUser.cancel` on it.
|
|
2262
2309
|
|
|
2263
2310
|
- When the handler starts, emits `loadUser.pending(payload)`.
|
|
2264
2311
|
- On success, emits `loadUser.success(result)`.
|
|
2265
2312
|
- On failure, emits `loadUser.fail(error)`.
|
|
2266
2313
|
- When the task is aborted before the handler settles, emits `loadUser.cancel(reason)` instead of `success` or `fail`. After `pending`, exactly one of the three follows.
|
|
2267
2314
|
- `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`.
|
|
2268
|
-
- `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,
|
|
2269
|
-
- Outcome actions are emitted as the task that ran the job (also for a handler bound with `
|
|
2270
|
-
- A
|
|
2315
|
+
- `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
|
+
- 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 command "..."` and nothing is sent. An action without a handler is an event and does not throw.
|
|
2271
2318
|
- Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
|
|
2272
2319
|
- 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.
|
|
2273
2320
|
|
|
@@ -2291,7 +2338,7 @@ export const bus = createBus();
|
|
|
2291
2338
|
// user.js
|
|
2292
2339
|
import { createJob } from "nanoeffects/actions";
|
|
2293
2340
|
|
|
2294
|
-
// createJob — a
|
|
2341
|
+
// createJob — an action with a handler that also sends pending, success, fail and cancel
|
|
2295
2342
|
export const loadUser = createJob("user/load", fetchUser);
|
|
2296
2343
|
```
|
|
2297
2344
|
|
|
@@ -2346,18 +2393,18 @@ A flag is exact when calls do not overlap, or when `latest` replaces them. If se
|
|
|
2346
2393
|
|
|
2347
2394
|
## Mount domains with modules
|
|
2348
2395
|
|
|
2349
|
-
**Problem.** A domain needs several subscriptions and
|
|
2396
|
+
**Problem.** A domain needs several subscriptions and handler bindings. With plain `bus.on` / `actions.handle` you must remember every unsubscribe.
|
|
2350
2397
|
|
|
2351
2398
|
**When.** You want one mount / one unmount for that domain. Skip modules while a few top-level `bus.on` calls are enough.
|
|
2352
2399
|
|
|
2353
|
-
`bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `
|
|
2400
|
+
`bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `actions`, and `createModule`: the same as `createModule` below, with the context fields of the bus typed (see [TypeScript](typescript.md)).
|
|
2354
2401
|
|
|
2355
2402
|
```js
|
|
2356
2403
|
// user.js
|
|
2357
|
-
import {
|
|
2404
|
+
import { createAction } from "nanoeffects/actions";
|
|
2358
2405
|
|
|
2359
|
-
export const getUser =
|
|
2360
|
-
export const saveUser =
|
|
2406
|
+
export const getUser = createAction("user/get");
|
|
2407
|
+
export const saveUser = createAction("user/save", persistUser);
|
|
2361
2408
|
```
|
|
2362
2409
|
|
|
2363
2410
|
```js
|
|
@@ -2394,7 +2441,7 @@ import { app } from "./bus";
|
|
|
2394
2441
|
import { getUser } from "./user";
|
|
2395
2442
|
|
|
2396
2443
|
const unmount = app.mount(orderModule); // later, e.g. after a lazy import
|
|
2397
|
-
app.
|
|
2444
|
+
app.actions.handle(getUser, fakeUser); // in a test
|
|
2398
2445
|
app.dispose(); // unmounts every module
|
|
2399
2446
|
```
|
|
2400
2447
|
|
|
@@ -2425,37 +2472,37 @@ app.unmount(...modules); // also in reverse order
|
|
|
2425
2472
|
|
|
2426
2473
|
If the `setup` of one module throws, the modules of that call that were already mounted are unmounted again and the error is thrown, so a call mounts all of its modules or none. A module that is mounted already is skipped.
|
|
2427
2474
|
|
|
2428
|
-
- **Unmounting undoes everything** the module did: subscriptions, bound
|
|
2475
|
+
- **Unmounting undoes everything** the module did: subscriptions, bound handlers, sub-modules, and background tasks (aborted).
|
|
2429
2476
|
- **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
|
|
2430
|
-
- **Registry.** If `bus.use(
|
|
2477
|
+
- **Registry.** If `bus.use(actionsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.actions` and `app.actions`.
|
|
2431
2478
|
- A disposed app cannot mount modules.
|
|
2432
|
-
- **The given modules are mounted at the end of the block that made the app**, or at the first use of the app or the bus if that comes first (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `
|
|
2479
|
+
- **The given modules are mounted at the end of the block that made the app**, or at the first use of the app or the bus if that comes first (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `actions`). Nothing needs to use the app: `bus.use(modulePlugin([a, b]))` is enough to start them. The plugins that you use after `modulePlugin` in the same block still reach them, because the modules wait until that block has finished. A plugin used after an `await` can come too late: use every plugin before the first `await`. The `bus.on` calls made in between are held and made right after, so the listeners of a module stay ahead of them. See [using plugins in any order](plugins.md#use-them-in-any-order).
|
|
2433
2480
|
- **To mount by hand instead**, pass no modules: `modulePlugin()` starts nothing, and `app.mount(...)` starts the modules when you want.
|
|
2434
2481
|
- Module context = task context plus `handle`, `use`, and `spawn`:
|
|
2435
|
-
- `handle` — give
|
|
2436
|
-
- `use` — bring sub-modules and
|
|
2482
|
+
- `handle` — give an action a handler (one that has none, or an override).
|
|
2483
|
+
- `use` — bring sub-modules and actions that carry a handler; the returned function undoes that call.
|
|
2437
2484
|
- `spawn` — background task, aborted on unmount.
|
|
2438
|
-
-
|
|
2485
|
+
- An action that carries a handler keeps answering after unmount (`use` only tracks registration). Use `handle` for a handler that must live only as long as the module.
|
|
2439
2486
|
|
|
2440
|
-
## Give
|
|
2487
|
+
## Give an action or a module its own fields
|
|
2441
2488
|
|
|
2442
|
-
|
|
2489
|
+
An action with a handler can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createAction`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
|
|
2443
2490
|
|
|
2444
2491
|
```js
|
|
2445
2492
|
// bus.js
|
|
2446
2493
|
import { createBus } from "nanoeffects";
|
|
2447
2494
|
import { scopePlugin } from "nanoeffects/scope";
|
|
2448
2495
|
|
|
2449
|
-
// scopePlugin — makes the scope of
|
|
2496
|
+
// scopePlugin — makes the scope of an action work on this bus
|
|
2450
2497
|
export const bus = createBus().use(scopePlugin());
|
|
2451
2498
|
```
|
|
2452
2499
|
|
|
2453
2500
|
```js
|
|
2454
2501
|
// user.js
|
|
2455
|
-
import {
|
|
2502
|
+
import { createAction } from "nanoeffects/actions";
|
|
2456
2503
|
|
|
2457
|
-
// the third argument — fields of this
|
|
2458
|
-
export const getUser =
|
|
2504
|
+
// the third argument — fields of this action only
|
|
2505
|
+
export const getUser = createAction(
|
|
2459
2506
|
"user/get",
|
|
2460
2507
|
(id, { api }) => api.getUser(id),
|
|
2461
2508
|
{ api },
|
|
@@ -3153,7 +3200,7 @@ Source: docs/query-libraries.md
|
|
|
3153
3200
|
|
|
3154
3201
|
# Use nanoeffects with React Query and SWR
|
|
3155
3202
|
|
|
3156
|
-
Read this page if you fetch data with [React Query](https://tanstack.com/query) (TanStack Query) or [SWR](https://swr.vercel.app) and want `retry`, `timeout` and the abort of nanoeffects inside the request. It shows one
|
|
3203
|
+
Read this page if you fetch data with [React Query](https://tanstack.com/query) (TanStack Query) or [SWR](https://swr.vercel.app) and want `retry`, `timeout` and the abort of nanoeffects inside the request. It shows one action that both libraries call.
|
|
3157
3204
|
|
|
3158
3205
|
You need a React project, `nanoeffects`, and one of the two libraries. `api` below is your own client: replace it with your requests.
|
|
3159
3206
|
|
|
@@ -3169,9 +3216,9 @@ The libraries and nanoeffects do different jobs, so each keeps its own.
|
|
|
3169
3216
|
|
|
3170
3217
|
`isLoading` and `data` come from the library, not from `emit`. `await bus.emit(...)` resolves once, with the result, so it cannot report that the request is still running.
|
|
3171
3218
|
|
|
3172
|
-
## Make the
|
|
3219
|
+
## Make the action
|
|
3173
3220
|
|
|
3174
|
-
|
|
3221
|
+
An action with a handler is the request itself. It lives in files that know nothing about React: the bus, the action with its handler, and the component.
|
|
3175
3222
|
|
|
3176
3223
|
```js
|
|
3177
3224
|
// bus.js
|
|
@@ -3187,11 +3234,11 @@ The handler tries the request up to three times and gives up after five seconds
|
|
|
3187
3234
|
|
|
3188
3235
|
```js
|
|
3189
3236
|
// user.js
|
|
3190
|
-
import {
|
|
3237
|
+
import { createAction } from "nanoeffects/actions";
|
|
3191
3238
|
import { retry, timeout } from "nanoeffects/effects";
|
|
3192
3239
|
|
|
3193
|
-
//
|
|
3194
|
-
export const getUser =
|
|
3240
|
+
// createAction with a handler — emit returns what the handler returns
|
|
3241
|
+
export const getUser = createAction("user/get", async (id, ctx) => {
|
|
3195
3242
|
// one try: the request, with the signal of timeout so it can stop it
|
|
3196
3243
|
const load = ({ signal }) => api.getUser(id, { signal });
|
|
3197
3244
|
// timeout — fail the try after 5 s
|
|
@@ -3226,13 +3273,13 @@ export function Profile({ id }) {
|
|
|
3226
3273
|
}
|
|
3227
3274
|
```
|
|
3228
3275
|
|
|
3229
|
-
The
|
|
3276
|
+
The action rejects when all tries fail, so React Query shows the `error`. React Query counts its own `retry` too: set `retry: false` in `useQuery` when the handler already retries, or the tries multiply.
|
|
3230
3277
|
|
|
3231
3278
|
## Call it from SWR
|
|
3232
3279
|
|
|
3233
3280
|
<!-- docs-check: continue -->
|
|
3234
3281
|
|
|
3235
|
-
SWR does not pass a signal to the fetcher, so there is nothing to give to `emit`. The
|
|
3282
|
+
SWR does not pass a signal to the fetcher, so there is nothing to give to `emit`. The action still brings `retry` and `timeout`.
|
|
3236
3283
|
|
|
3237
3284
|
```jsx
|
|
3238
3285
|
// Profile.jsx
|
|
@@ -3256,7 +3303,7 @@ export function Profile({ id }) {
|
|
|
3256
3303
|
|
|
3257
3304
|
<!-- docs-check: continue -->
|
|
3258
3305
|
|
|
3259
|
-
|
|
3306
|
+
An action with a handler gives its result only to the caller. If a store or another module must react to a loaded user, the handler can say so:
|
|
3260
3307
|
|
|
3261
3308
|
```js
|
|
3262
3309
|
// user-events.js
|
|
@@ -3266,15 +3313,15 @@ import { createAction } from "nanoeffects/actions";
|
|
|
3266
3313
|
export const userLoaded = createAction("user/loaded");
|
|
3267
3314
|
```
|
|
3268
3315
|
|
|
3269
|
-
Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the
|
|
3316
|
+
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).
|
|
3270
3317
|
|
|
3271
3318
|
## Know the limits
|
|
3272
3319
|
|
|
3273
|
-
- **Without the plugin, `{ signal }` is ignored.** The
|
|
3320
|
+
- **Without the plugin, `{ signal }` is ignored.** The action runs, but the abort does not reach it. Call `bus.use(signalPlugin())` once, where the bus is made.
|
|
3274
3321
|
- **Only the call forms take it.** `bus.emit(getUser(id), { signal })` works; `bus.emit("user/get", id, { signal })` does not.
|
|
3275
|
-
- **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it.
|
|
3276
|
-
- **Give each request its own signal for an event.** For a
|
|
3277
|
-
- **The signal reaches the tasks that this `emit` starts**, with the listeners of the
|
|
3322
|
+
- **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
|
+
- **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.
|
|
3324
|
+
- **The signal reaches the tasks that this `emit` starts**, with the listeners of the action's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
|
|
3278
3325
|
|
|
3279
3326
|
A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
|
|
3280
3327
|
|
|
@@ -3299,9 +3346,9 @@ The fix is one `AbortController` for each unit of work that the client owns, and
|
|
|
3299
3346
|
| A WebSocket | the socket connects | the socket closes |
|
|
3300
3347
|
| A queued job | the job is accepted | the server shuts down |
|
|
3301
3348
|
|
|
3302
|
-
## Make the bus and the
|
|
3349
|
+
## Make the bus and the action
|
|
3303
3350
|
|
|
3304
|
-
The bus and the
|
|
3351
|
+
The bus and the action live in files that know nothing about HTTP, so the same action can run in a route, a test or a worker.
|
|
3305
3352
|
|
|
3306
3353
|
```js
|
|
3307
3354
|
// bus.js
|
|
@@ -3313,15 +3360,15 @@ import { signalPlugin } from "nanoeffects/signal";
|
|
|
3313
3360
|
export const bus = createBus().use(signalPlugin());
|
|
3314
3361
|
```
|
|
3315
3362
|
|
|
3316
|
-
The
|
|
3363
|
+
The action loads a user. It tries the request up to three times, and gives up on each try after five seconds. `ctx.signal` is the signal of the task: it aborts when the controller of the request aborts.
|
|
3317
3364
|
|
|
3318
3365
|
```js
|
|
3319
3366
|
// user.js
|
|
3320
|
-
import {
|
|
3367
|
+
import { createAction } from "nanoeffects/actions";
|
|
3321
3368
|
import { retry, timeout } from "nanoeffects/effects";
|
|
3322
3369
|
|
|
3323
|
-
//
|
|
3324
|
-
export const getUser =
|
|
3370
|
+
// createAction — a command with a handler: emit returns what the handler returns
|
|
3371
|
+
export const getUser = createAction("user/get", async (id, ctx) => {
|
|
3325
3372
|
// one try: the request, with the signal of timeout so it can stop it
|
|
3326
3373
|
const load = ({ signal }) => api.getUser(id, { signal });
|
|
3327
3374
|
// timeout — fail the try after 5 s
|
|
@@ -3374,7 +3421,7 @@ app.get("/users/:id", async (request, reply) => {
|
|
|
3374
3421
|
});
|
|
3375
3422
|
```
|
|
3376
3423
|
|
|
3377
|
-
After an abort the
|
|
3424
|
+
After an abort the action rejects with the abort reason. The `catch` above ignores it when the connection is gone, because there is nobody to answer.
|
|
3378
3425
|
|
|
3379
3426
|
## Stop the server
|
|
3380
3427
|
|
|
@@ -3406,17 +3453,17 @@ The route above fills `inFlight` and empties it in the `close` listener. The [ap
|
|
|
3406
3453
|
|
|
3407
3454
|
A socket lasts minutes and carries several tasks (a subscription, a heartbeat), so the controller belongs to the socket and every `emit` for it gets the same signal. Closing the socket stops all of them.
|
|
3408
3455
|
|
|
3409
|
-
First the
|
|
3456
|
+
First the action. It sends a price now and then every second, until its task is aborted.
|
|
3410
3457
|
|
|
3411
3458
|
```js
|
|
3412
3459
|
// prices.js
|
|
3413
|
-
import {
|
|
3460
|
+
import { createAction } from "nanoeffects/actions";
|
|
3414
3461
|
import { poll } from "nanoeffects/effects";
|
|
3415
3462
|
import { latest } from "nanoeffects/policies";
|
|
3416
3463
|
|
|
3417
|
-
//
|
|
3464
|
+
// createAction — a command with a handler
|
|
3418
3465
|
// latest — a new subscription stops the old one; key — but only the old one of the same socket
|
|
3419
|
-
export const watchPrices =
|
|
3466
|
+
export const watchPrices = createAction(
|
|
3420
3467
|
"prices/watch",
|
|
3421
3468
|
latest(
|
|
3422
3469
|
async (call, ctx) => {
|
|
@@ -3430,7 +3477,7 @@ export const watchPrices = createCommand(
|
|
|
3430
3477
|
);
|
|
3431
3478
|
```
|
|
3432
3479
|
|
|
3433
|
-
Without the `key`, one policy serves the whole
|
|
3480
|
+
Without the `key`, one policy serves the whole action, and a new subscription on one socket would stop the subscription on another.
|
|
3434
3481
|
|
|
3435
3482
|
Then the route. Each socket gets an id and a controller.
|
|
3436
3483
|
|
|
@@ -3774,7 +3821,7 @@ Source: docs/chat-example.md
|
|
|
3774
3821
|
|
|
3775
3822
|
Use this page when your app has several features that react to each other and you want to see where each piece of logic goes. The example is a chat with rooms, a history that loads, messages that can fail to send, a search across all rooms, and new messages that arrive on their own. A small Fastify server in the example answers every request, and it misbehaves on a known schedule, so each problem on this page really happens.
|
|
3776
3823
|
|
|
3777
|
-
It is a level D example. Start with [Actions,
|
|
3824
|
+
It is a level D example. Start with [Actions, jobs and modules](actions.md) for the API, or with the `domains-ts` template in [Start a project from a template](starter.md#start-with-domains) for a smaller project.
|
|
3778
3825
|
|
|
3779
3826
|
The code is in [`examples/chat`](../examples/chat). Run it on [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat), or on your machine:
|
|
3780
3827
|
|
|
@@ -3820,7 +3867,7 @@ The code is in layers, and a file imports only from the layers below it. The com
|
|
|
3820
3867
|
|
|
3821
3868
|
That rule is why `show-history` exists. Selecting a room must load its history, but `room` does not know `message` and `message` does not know `room`. The feature `show-history` is the one place that knows both: it listens to `rooms/selected` and starts the job `messages/loadHistory`. The same goes for a message that arrives: `receive-messages` emits `incoming/received`, an event that the `message` entity owns and listens to.
|
|
3822
3869
|
|
|
3823
|
-
A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its
|
|
3870
|
+
A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its actions, never through its handlers or its store. All actions live in the events file. An action with no handler says that something happened, and any feature may listen: `rooms/selected` is heard by two. An action with a handler is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createAction`, so the action works wherever it is called and can return a result, and the module binds it with `module.use` for as long as it is mounted. A job is an action with a handler that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
|
|
3824
3871
|
|
|
3825
3872
|
The layers come from [Feature-Sliced Design](https://feature-sliced.design/), reduced to the three that an app of this size needs.
|
|
3826
3873
|
|
|
@@ -3836,7 +3883,7 @@ A job sends `pending` when its handler starts and exactly one of `success`, `fai
|
|
|
3836
3883
|
// message.js
|
|
3837
3884
|
import { createJob } from "nanoeffects/actions";
|
|
3838
3885
|
|
|
3839
|
-
// createJob — a
|
|
3886
|
+
// createJob — an action with a handler that also sends pending, success, fail and cancel
|
|
3840
3887
|
export const loadHistory = createJob("messages/loadHistory");
|
|
3841
3888
|
```
|
|
3842
3889
|
|
|
@@ -3951,11 +3998,11 @@ export const runSearch = debounce(
|
|
|
3951
3998
|
`take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
|
|
3952
3999
|
|
|
3953
4000
|
```js
|
|
3954
|
-
import {
|
|
4001
|
+
import { createAction } from "nanoeffects/actions";
|
|
3955
4002
|
import { take } from "nanoeffects/effects";
|
|
3956
4003
|
|
|
3957
|
-
//
|
|
3958
|
-
const markMessage =
|
|
4004
|
+
// createAction with a handler — an order for the message entity, with the handler that does it
|
|
4005
|
+
const markMessage = createAction("messages/mark", (messageId) => console.log("mark", messageId));
|
|
3959
4006
|
|
|
3960
4007
|
// the room that is open, kept from the rooms/selected event
|
|
3961
4008
|
let selectedRoom = "general";
|
|
@@ -3967,7 +4014,7 @@ export const goToResult = async (hit, ctx) => {
|
|
|
3967
4014
|
ctx.emit("rooms/selected", hit.roomId);
|
|
3968
4015
|
await loaded;
|
|
3969
4016
|
}
|
|
3970
|
-
// ctx.emit with a
|
|
4017
|
+
// ctx.emit with an action that has a handler — the message entity marks the message
|
|
3971
4018
|
ctx.emit(markMessage(hit.id));
|
|
3972
4019
|
};
|
|
3973
4020
|
```
|
|
@@ -4019,10 +4066,149 @@ Each entity and each feature that has state keeps it in a [Zustand](https://zust
|
|
|
4019
4066
|
|
|
4020
4067
|
## Where to go next
|
|
4021
4068
|
|
|
4022
|
-
- The API of
|
|
4069
|
+
- The API of actions, jobs and modules: [Actions, jobs and modules](actions.md).
|
|
4023
4070
|
- Policies such as `latest` and `queue`: [Policies](policies.md). Effects such as `allSettled`, `take` and `poll`: [Effects](effects.md).
|
|
4024
4071
|
- A smaller project with two domains: [Start with domains](starter.md#start-with-domains). Its two domains do not talk to each other, so it needs no layers.
|
|
4025
4072
|
|
|
4073
|
+
---
|
|
4074
|
+
Source: docs/chat-widget.md
|
|
4075
|
+
|
|
4076
|
+
# Build a chat widget that listens only while it is open
|
|
4077
|
+
|
|
4078
|
+
Use this page when a site has a chat with a bot behind a launcher button, and the chat must cost nothing while it is closed. Closed means no subscriptions on the bus, no polling, no keyboard listener, and no request still running. Opening turns all of it on, and closing turns all of it off in one call.
|
|
4079
|
+
|
|
4080
|
+
The code is in [`examples/chat-widget`](../examples/chat-widget): plain DOM and TypeScript, a fake bot in the browser, no server. Run it on [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat-widget), or on your machine:
|
|
4081
|
+
|
|
4082
|
+
```bash
|
|
4083
|
+
cd examples/chat-widget
|
|
4084
|
+
npm install
|
|
4085
|
+
npm run dev
|
|
4086
|
+
```
|
|
4087
|
+
|
|
4088
|
+
It is a level D example: the logic lives in one module, and the page only mounts and unmounts it. For an app with several features see [Build a chat with entities, features, modules and a small server](chat-example.md).
|
|
4089
|
+
|
|
4090
|
+
## Know what happens on open and on close
|
|
4091
|
+
|
|
4092
|
+
| Moment | What happens |
|
|
4093
|
+
| ------ | ------------ |
|
|
4094
|
+
| Open | The module is mounted: the bot handler is subscribed, the operator messages are polled, the `Escape` listener is added. The panel is drawn and listens to the bus. |
|
|
4095
|
+
| The user sends a text | The bot is asked, one text after another, with a time limit and a retry. |
|
|
4096
|
+
| `Escape` | The module emits `chat/closeRequested`; the launcher closes the chat. |
|
|
4097
|
+
| Close | The panel is removed and unsubscribed, the module is unmounted. A request in flight is aborted, the polling stops, the `Escape` listener goes away. |
|
|
4098
|
+
|
|
4099
|
+
## Find the parts in the files
|
|
4100
|
+
|
|
4101
|
+
| File | Job |
|
|
4102
|
+
| ---- | --- |
|
|
4103
|
+
| `src/bus.ts` | The bus, and `modulePlugin()` with no module: the launcher mounts the module itself. |
|
|
4104
|
+
| `src/chat.ts` | The events only: `messageSent`, `messageAdded`, `typingChanged`, `closeRequested`. |
|
|
4105
|
+
| `src/chat-handlers.ts` | `askBot` (`timeout` and `retry` inside) and `watchOperator` (`poll`): plain `async` functions that know nothing about the page. |
|
|
4106
|
+
| `src/chat-module.ts` | The list of what is alive while the chat is open. |
|
|
4107
|
+
| `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. |
|
|
4109
|
+
|
|
4110
|
+
## Bind everything in one module
|
|
4111
|
+
|
|
4112
|
+
A module is the list of what is alive while the chat is open. When it unmounts, the bus undoes every line of it.
|
|
4113
|
+
|
|
4114
|
+
<!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
|
|
4115
|
+
```js
|
|
4116
|
+
// chat-module.js
|
|
4117
|
+
import { createModule } from "nanoeffects/actions";
|
|
4118
|
+
import { queue } from "nanoeffects/policies";
|
|
4119
|
+
import { closeRequested, messageSent } from "./chat";
|
|
4120
|
+
import { askBot, watchOperator } from "./chat-handlers";
|
|
4121
|
+
|
|
4122
|
+
// a task that stays pending until the module unmounts; its signal then aborts
|
|
4123
|
+
const untilAborted = (ctx) =>
|
|
4124
|
+
new Promise((resolve) => {
|
|
4125
|
+
ctx.signal.addEventListener("abort", resolve, { once: true });
|
|
4126
|
+
});
|
|
4127
|
+
|
|
4128
|
+
// a DOM listener that lives as long as the module: `signal` removes it when it aborts
|
|
4129
|
+
const listenToEscape = async (_, ctx) => {
|
|
4130
|
+
const onKey = (event) => {
|
|
4131
|
+
if (event.key === "Escape") {
|
|
4132
|
+
ctx.emit(closeRequested());
|
|
4133
|
+
}
|
|
4134
|
+
};
|
|
4135
|
+
document.addEventListener("keydown", onKey, { signal: ctx.signal });
|
|
4136
|
+
await untilAborted(ctx);
|
|
4137
|
+
};
|
|
4138
|
+
|
|
4139
|
+
export const chatModule = createModule("chat", (module) => {
|
|
4140
|
+
// module.run — runs askBot as a child task of the module, so an unmount aborts it
|
|
4141
|
+
const answer = (text) => module.run(askBot, text);
|
|
4142
|
+
// on + queue — answer the texts one after another, in the order they were sent
|
|
4143
|
+
module.on(messageSent, queue(answer));
|
|
4144
|
+
// spawn — a background task that lives as long as the module
|
|
4145
|
+
module.spawn(watchOperator);
|
|
4146
|
+
module.spawn(listenToEscape);
|
|
4147
|
+
});
|
|
4148
|
+
```
|
|
4149
|
+
|
|
4150
|
+
Three details that make the cleanup complete:
|
|
4151
|
+
|
|
4152
|
+
- **`module.run`, not `askBot` directly.** A handler given to `module.on` runs as a child of whoever emitted the event (here the panel), so unmounting stops listening but does not abort a request that is already running. Running it with `module.run` makes it a child of the module.
|
|
4153
|
+
- **`queue`, not `latest`.** `queue` keeps the order when the user sends two texts fast. [`latest`](policies.md) would abort the first answer, which is wrong for a conversation.
|
|
4154
|
+
- **A DOM listener with `signal`.** `addEventListener` takes a `signal` option and removes the listener when it aborts. The task stays pending until then, because a task that has finished is not aborted again.
|
|
4155
|
+
|
|
4156
|
+
## Open and close from the launcher
|
|
4157
|
+
|
|
4158
|
+
`app.mount` returns the function that unmounts the module. The launcher puts it together with the one that removes the panel.
|
|
4159
|
+
|
|
4160
|
+
<!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
|
|
4161
|
+
```js
|
|
4162
|
+
// main.js
|
|
4163
|
+
import { app, bus } from "./bus";
|
|
4164
|
+
import { closeRequested } from "./chat";
|
|
4165
|
+
import { chatModule } from "./chat-module";
|
|
4166
|
+
import { renderPanel } from "./panel";
|
|
4167
|
+
|
|
4168
|
+
const button = document.querySelector("#chat-launcher");
|
|
4169
|
+
let close = null; // set while the chat is open
|
|
4170
|
+
|
|
4171
|
+
const open = () => {
|
|
4172
|
+
const stopModule = app.mount(chatModule); // subscriptions, polling and Escape start
|
|
4173
|
+
const stopPanel = renderPanel(document.body);
|
|
4174
|
+
close = () => {
|
|
4175
|
+
stopPanel();
|
|
4176
|
+
stopModule(); // everything the module started ends, a running request included
|
|
4177
|
+
close = null;
|
|
4178
|
+
};
|
|
4179
|
+
};
|
|
4180
|
+
|
|
4181
|
+
button.addEventListener("click", () => {
|
|
4182
|
+
if (close) {
|
|
4183
|
+
close();
|
|
4184
|
+
} else {
|
|
4185
|
+
open();
|
|
4186
|
+
}
|
|
4187
|
+
});
|
|
4188
|
+
|
|
4189
|
+
// the launcher stays subscribed: it must hear Escape to close the chat
|
|
4190
|
+
bus.on(closeRequested, () => close?.());
|
|
4191
|
+
```
|
|
4192
|
+
|
|
4193
|
+
The launcher's own `bus.on` is the only subscription that lives while the chat is closed, and it does nothing then: `close` is `null`.
|
|
4194
|
+
|
|
4195
|
+
## Check that closing cleans up
|
|
4196
|
+
|
|
4197
|
+
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
|
+
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: only `chat/closeRequested` has a listener (the launcher). Open: `chat/sent`, `chat/messageAdded` and `chat/typing` have one each as well. That is the proof that nothing is left subscribed.
|
|
4200
|
+
4. Wait 10 s with the chat closed. The operator is not polled: no line from the operator appears.
|
|
4201
|
+
5. Press `Escape` with the chat closed. Nothing happens. Open the chat and press it again: it closes.
|
|
4202
|
+
6. Open the chat again. The old lines are back, and the polling runs.
|
|
4203
|
+
|
|
4204
|
+
`src/tests/chat-widget-example.test.ts` checks the same points without a browser.
|
|
4205
|
+
|
|
4206
|
+
## Where to go next
|
|
4207
|
+
|
|
4208
|
+
- Show an unread count on the launcher while the chat is closed: a second module that stays mounted and listens only to `chat/messageAdded`.
|
|
4209
|
+
- Replace the polling by a WebSocket in a spawned task: the abort closes it the same way (see [`examples/trades`](../examples/trades) for a socket under `retry` and `latest`).
|
|
4210
|
+
- Watch the whole thing in the [trace](tracing.md): the module, the bot task, the retries and the polling are tasks of one tree.
|
|
4211
|
+
|
|
4026
4212
|
---
|
|
4027
4213
|
Source: docs/vue.md
|
|
4028
4214
|
|
|
@@ -4228,7 +4414,7 @@ bus.on("save", (draft, { store }) => {
|
|
|
4228
4414
|
});
|
|
4229
4415
|
```
|
|
4230
4416
|
|
|
4231
|
-
That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`,
|
|
4417
|
+
That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`, actions, modules, and child tasks of effects and policies.
|
|
4232
4418
|
|
|
4233
4419
|
Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may be a function `(task) => fields` when the value depends on the task.
|
|
4234
4420
|
|
|
@@ -4274,9 +4460,9 @@ const bus = createBus<Events>({ context: { store } });
|
|
|
4274
4460
|
---
|
|
4275
4461
|
Source: docs/scope.md
|
|
4276
4462
|
|
|
4277
|
-
# Pin context fields to
|
|
4463
|
+
# Pin context fields to an action, a module or a call
|
|
4278
4464
|
|
|
4279
|
-
Optional, for domain logic. Use it when one
|
|
4465
|
+
Optional, for domain logic. Use it when one action needs its own client, one module its own logger, or one test its own fake, and a field on the whole bus ([context fields](context.md)) is too wide.
|
|
4280
4466
|
|
|
4281
4467
|
A **scope** is a plain object. Every key goes on `ctx` as it is (`cache`, `api`, `logger`), except one: `$effects`. Its functions take `{ signal }` as the first parameter, like `retry(ctx, fn)`, and go on `ctx` bound to the task, so you call `ctx.fetchJson(url)` instead of `fetchJson(ctx, url)`.
|
|
4282
4468
|
|
|
@@ -4295,21 +4481,21 @@ import { scopePlugin } from "nanoeffects/scope";
|
|
|
4295
4481
|
export const bus = createBus().use(scopePlugin());
|
|
4296
4482
|
```
|
|
4297
4483
|
|
|
4298
|
-
The bus is in one file and the
|
|
4484
|
+
The bus is in one file and the actions in files named after their domain, so the next example imports `bus` from here. Each section after it keeps its own `bus.js` and domain file, as if it were an app of its own.
|
|
4299
4485
|
|
|
4300
4486
|
- Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
|
|
4301
4487
|
- Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
|
|
4302
4488
|
|
|
4303
|
-
## Give
|
|
4489
|
+
## Give an action its own fields
|
|
4304
4490
|
|
|
4305
4491
|
<!-- docs-check: continue -->
|
|
4306
4492
|
|
|
4307
4493
|
```js
|
|
4308
4494
|
// user.js
|
|
4309
|
-
import {
|
|
4495
|
+
import { createAction } from "nanoeffects/actions";
|
|
4310
4496
|
|
|
4311
|
-
//
|
|
4312
|
-
export const getUser =
|
|
4497
|
+
// createAction — third argument is the scope of this action
|
|
4498
|
+
export const getUser = createAction(
|
|
4313
4499
|
"user/get",
|
|
4314
4500
|
(id, { api, cache }) => {
|
|
4315
4501
|
// api and cache are on ctx for this command only
|
|
@@ -4326,7 +4512,7 @@ import { getUser } from "./user";
|
|
|
4326
4512
|
const user = await bus.emit(getUser("1")); // the handler has api and cache
|
|
4327
4513
|
```
|
|
4328
4514
|
|
|
4329
|
-
The listeners of `"user/get"` get the same fields when the
|
|
4515
|
+
The listeners of `"user/get"` get the same fields when the action is emitted, because the fields belong to the call.
|
|
4330
4516
|
|
|
4331
4517
|
## Bind an effect to the task
|
|
4332
4518
|
|
|
@@ -4334,14 +4520,14 @@ An effect is a function with `{ signal }` as its first parameter. In `$effects`
|
|
|
4334
4520
|
|
|
4335
4521
|
```js
|
|
4336
4522
|
// feed.js
|
|
4337
|
-
import {
|
|
4523
|
+
import { createAction } from "nanoeffects/actions";
|
|
4338
4524
|
import { delay, race } from "nanoeffects/effects";
|
|
4339
4525
|
|
|
4340
4526
|
// your own effect: first parameter is the task (only its signal is used)
|
|
4341
4527
|
const fetchJson = (ctx, url) =>
|
|
4342
4528
|
fetch(url, { signal: ctx.signal }).then((response) => response.json());
|
|
4343
4529
|
|
|
4344
|
-
export const loadFeed =
|
|
4530
|
+
export const loadFeed = createAction(
|
|
4345
4531
|
"feed/load",
|
|
4346
4532
|
(url, { fetchJson, race, delay }) =>
|
|
4347
4533
|
// race, delay, fetchJson — bound to this task: no ctx argument
|
|
@@ -4387,9 +4573,9 @@ export const bus = createBus().use(scopePlugin());
|
|
|
4387
4573
|
|
|
4388
4574
|
```js
|
|
4389
4575
|
// user.js
|
|
4390
|
-
import {
|
|
4576
|
+
import { createAction } from "nanoeffects/actions";
|
|
4391
4577
|
|
|
4392
|
-
export const getUser =
|
|
4578
|
+
export const getUser = createAction(
|
|
4393
4579
|
"user/get",
|
|
4394
4580
|
(id, { api }) => api.getUser(id),
|
|
4395
4581
|
{ api },
|
|
@@ -4410,7 +4596,7 @@ await bus.emit(getUser("1"), { api: fakeApi });
|
|
|
4410
4596
|
|
|
4411
4597
|
Rules for the second argument:
|
|
4412
4598
|
|
|
4413
|
-
- It works with
|
|
4599
|
+
- It works with the call of an action (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
|
|
4414
4600
|
- It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
|
|
4415
4601
|
- It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
|
|
4416
4602
|
- Fields of the library (`signal`, `emit`, …) in it are ignored.
|
|
@@ -4475,13 +4661,13 @@ In `$effects` a function loses its first parameter, and TypeScript cannot keep a
|
|
|
4475
4661
|
|
|
4476
4662
|
```js
|
|
4477
4663
|
// pick.js
|
|
4478
|
-
import {
|
|
4664
|
+
import { createAction } from "nanoeffects/actions";
|
|
4479
4665
|
import { effect } from "nanoeffects/scope";
|
|
4480
4666
|
|
|
4481
4667
|
// effect — marks the function, so a scope binds it to the task; the editor keeps its generic
|
|
4482
4668
|
const first = effect((ctx, items) => items[0]);
|
|
4483
4669
|
|
|
4484
|
-
export const pick =
|
|
4670
|
+
export const pick = createAction("pick", (items, { first }) => first(items), {
|
|
4485
4671
|
first, // a plain key of the scope, not an entry of `$effects`
|
|
4486
4672
|
});
|
|
4487
4673
|
```
|
|
@@ -4491,7 +4677,7 @@ Call `effect` where you declare the function, as above, and not inline in the sc
|
|
|
4491
4677
|
`Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
|
|
4492
4678
|
|
|
4493
4679
|
```ts
|
|
4494
|
-
import {
|
|
4680
|
+
import { createAction } from "nanoeffects/actions";
|
|
4495
4681
|
import type { Effect } from "nanoeffects/effects";
|
|
4496
4682
|
|
|
4497
4683
|
// First — checker-only: how a handler calls the effect (no ctx)
|
|
@@ -4500,7 +4686,7 @@ type First = <T>(items: T[]) => T;
|
|
|
4500
4686
|
// Effect<First> — the function takes the task first and is typed as First once bound
|
|
4501
4687
|
const first: Effect<First> = (ctx, items) => items[0];
|
|
4502
4688
|
|
|
4503
|
-
const pick =
|
|
4689
|
+
const pick = createAction(
|
|
4504
4690
|
"pick",
|
|
4505
4691
|
(id: string, { first }) => first([id]), // string
|
|
4506
4692
|
{ $effects: { first } },
|
|
@@ -4511,14 +4697,14 @@ const pick = createCommand(
|
|
|
4511
4697
|
|
|
4512
4698
|
## Type the fields (optional)
|
|
4513
4699
|
|
|
4514
|
-
A handler of
|
|
4700
|
+
A handler of an action, job or module with a scope sees the fields of the scope in `ctx`, typed from the values you gave. You write no `WithContext`.
|
|
4515
4701
|
|
|
4516
4702
|
```ts
|
|
4517
|
-
import {
|
|
4703
|
+
import { createAction } from "nanoeffects/actions";
|
|
4518
4704
|
|
|
4519
4705
|
type User = { id: string; name: string };
|
|
4520
4706
|
|
|
4521
|
-
const getUser =
|
|
4707
|
+
const getUser = createAction(
|
|
4522
4708
|
"user/get",
|
|
4523
4709
|
async (id: string, ctx) => {
|
|
4524
4710
|
ctx.cache; // Map<string, User>, from the scope below
|
|
@@ -4536,10 +4722,10 @@ const getUser = createCommand(
|
|
|
4536
4722
|
## Limits
|
|
4537
4723
|
|
|
4538
4724
|
- No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
|
|
4539
|
-
- The fields of the bus are not in the handler of
|
|
4725
|
+
- The fields of the bus are not in the handler of an action made with `createAction(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
|
|
4540
4726
|
- The string form of `emit` takes no scope.
|
|
4541
4727
|
- `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
|
|
4542
|
-
- A scope on
|
|
4728
|
+
- A scope on an action, job or module is typed without the events of a bus, so `take` in its `$effects` takes any event name.
|
|
4543
4729
|
|
|
4544
4730
|
---
|
|
4545
4731
|
Source: docs/oop.md
|
|
@@ -4856,7 +5042,7 @@ export const SearchService = extendBus(bus, SearchServiceBase);
|
|
|
4856
5042
|
|
|
4857
5043
|
Things to know:
|
|
4858
5044
|
|
|
4859
|
-
- The event is a name
|
|
5045
|
+
- The event is a name or an action creator, as for `bus.on`. For a creator the payload of the method is checked against it.
|
|
4860
5046
|
- The second argument, `wrap`, takes the bound handler and returns the one to subscribe: `latest`, or `(handler) => debounce(300, handler)`. A policy needs it: `latest(this.search)` wraps the function without `this`, and the bound handler is what `wrap` gets.
|
|
4861
5047
|
- The subscriptions are made right after the constructor of `Base`. An event that is emitted inside that constructor does not reach them.
|
|
4862
5048
|
- A subclass inherits them. A method that a subclass overrides is subscribed only if the override is decorated.
|
|
@@ -4962,21 +5148,21 @@ Source: docs/typescript.md
|
|
|
4962
5148
|
|
|
4963
5149
|
# Use nanoeffects in a TypeScript app with many files
|
|
4964
5150
|
|
|
4965
|
-
Read this page if your app is in TypeScript and the
|
|
5151
|
+
Read this page if your app is in TypeScript and the handlers and modules live in different files. It shows how to write an action with a handler so that TypeScript infers its payload and result, and what to add when the handlers need the fields of your bus (`ctx.store`, the effects of `effectsPlugin`). For one file or for JavaScript you do not need it.
|
|
4966
5152
|
|
|
4967
|
-
## Write
|
|
5153
|
+
## Write an action with its handler
|
|
4968
5154
|
|
|
4969
|
-
Give `
|
|
5155
|
+
Give `createAction` the handler and write nothing else. The payload comes from the annotated parameter, the result from the body. You do not write a generic.
|
|
4970
5156
|
|
|
4971
5157
|
<!-- docs-check: skip - a file of examples/typescript, type-checked there -->
|
|
4972
5158
|
```ts
|
|
4973
5159
|
// user.ts
|
|
4974
|
-
import {
|
|
5160
|
+
import { createAction, createModule } from "nanoeffects/actions";
|
|
4975
5161
|
|
|
4976
5162
|
export type User = { id: string; name: string };
|
|
4977
5163
|
|
|
4978
5164
|
// payload: string, from `id: string`; result: Promise<User>, from the body
|
|
4979
|
-
export const getUser =
|
|
5165
|
+
export const getUser = createAction(
|
|
4980
5166
|
"user/get",
|
|
4981
5167
|
async (id: string, { signal }) => {
|
|
4982
5168
|
const response = await fetch(`/api/users/${id}`, { signal });
|
|
@@ -5018,11 +5204,11 @@ A wrong payload at the call (`getUser(1)`) is an error. Annotate the parameters,
|
|
|
5018
5204
|
|
|
5019
5205
|
## Give `ctx` the fields of your bus
|
|
5020
5206
|
|
|
5021
|
-
Types of a bus live on the bus. `contextPlugin({ store })` and `effectsPlugin({ race })` make `ctx.store` and `ctx.race` known to the handlers of **that bus**.
|
|
5207
|
+
Types of a bus live on the bus. `contextPlugin({ store })` and `effectsPlugin({ race })` make `ctx.store` and `ctx.race` known to the handlers of **that bus**. An action with a handler made in another file does not see the bus, so its `ctx` has no `store`:
|
|
5022
5208
|
|
|
5023
5209
|
<!-- docs-check: skip - shows the error on purpose -->
|
|
5024
5210
|
```ts
|
|
5025
|
-
export const getUser =
|
|
5211
|
+
export const getUser = createAction("user/get", (id: string, ctx) => {
|
|
5026
5212
|
ctx.store; // error: Property 'store' does not exist
|
|
5027
5213
|
});
|
|
5028
5214
|
```
|
|
@@ -5055,7 +5241,7 @@ export type AppContext = Context<AppEvents>; // ctx of a handler of this bus: ha
|
|
|
5055
5241
|
| Type | What it is | Why you name it |
|
|
5056
5242
|
| ---- | ---------- | --------------- |
|
|
5057
5243
|
| `EventsOf<typeof bus>` | the event map of the bus, with the context fields of the plugins (and of `WithContext`, if you use it) | the source of `AppContext` |
|
|
5058
|
-
| `Context<AppEvents>` | the `ctx` that a handler of the bus gets | annotate the `ctx` of a handler given to `
|
|
5244
|
+
| `Context<AppEvents>` | the `ctx` that a handler of the bus gets | annotate the `ctx` of a handler given to `createAction` |
|
|
5059
5245
|
|
|
5060
5246
|
These types change nothing at runtime. They only tell the checker what the bus already does.
|
|
5061
5247
|
|
|
@@ -5064,10 +5250,10 @@ Annotate `ctx` with `AppContext` where the handler reads a field of the bus, and
|
|
|
5064
5250
|
<!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
|
|
5065
5251
|
```ts
|
|
5066
5252
|
// user.ts
|
|
5067
|
-
import {
|
|
5253
|
+
import { createAction } from "nanoeffects/actions";
|
|
5068
5254
|
import { type AppContext, createModule } from "./bus";
|
|
5069
5255
|
|
|
5070
|
-
export const getUser =
|
|
5256
|
+
export const getUser = createAction(
|
|
5071
5257
|
"user/get",
|
|
5072
5258
|
(id: string, { store, signal }: AppContext) => store.fetchUser(id, { signal }),
|
|
5073
5259
|
);
|
|
@@ -5107,14 +5293,14 @@ Pass `ctx` (or `{ signal }`) when you call it. The effects of `nanoeffects/effec
|
|
|
5107
5293
|
|
|
5108
5294
|
## Keep the contract apart from the code
|
|
5109
5295
|
|
|
5110
|
-
When the file with the
|
|
5296
|
+
When the file with the action must not import the code that implements it, make the action without a handler. Here you write the payload and the result once, as a `Call`, and the handler goes to a module, where it needs no annotation. `Call` is a promise of yours: nothing checks it against the handler, so keep the handler typed from it in the module:
|
|
5111
5297
|
|
|
5112
5298
|
<!-- docs-check: skip - fragment of the file above -->
|
|
5113
5299
|
```ts
|
|
5114
|
-
export const getUser =
|
|
5300
|
+
export const getUser = createAction<Call<string, Promise<User>>>("user/get"); // the contract
|
|
5115
5301
|
|
|
5116
5302
|
createModule("user", (m) => {
|
|
5117
|
-
// payload and result come from the
|
|
5303
|
+
// payload and result come from the Call, ctx from the app: no annotation
|
|
5118
5304
|
m.handle(getUser, (id, { store, signal }) => store.fetchUser(id, { signal }));
|
|
5119
5305
|
});
|
|
5120
5306
|
```
|
|
@@ -5123,11 +5309,11 @@ A wrong payload, a wrong field of `ctx` and a wrong result are errors.
|
|
|
5123
5309
|
|
|
5124
5310
|
## Check your setup
|
|
5125
5311
|
|
|
5126
|
-
- **A field is missing on `ctx`.** The `ctx` of a handler given to `
|
|
5127
|
-
- **`EventsOf<typeof bus>` is `never`.** `bus` is not a bus. `bus.use(
|
|
5312
|
+
- **A field is missing on `ctx`.** The `ctx` of a handler given to `createAction` is not annotated: add `: AppContext`. A handler written inline in `m.handle` or `m.on` of a module made with the `createModule` of `bus.ts` does not need it. The default `ctx` has no fields of your bus.
|
|
5313
|
+
- **`EventsOf<typeof bus>` is `never`.** `bus` is not a bus. `bus.use(actionsPlugin())` and `bus.use(modulePlugin(...))` return the registry or the app, not the bus: keep the bus in its own constant (`bus`, then `app = bus.use(modulePlugin())`).
|
|
5128
5314
|
- **`effectsPlugin` is not in the type.** `use` must come before you read `typeof bus`. Chain `createBus(...).use(effectsPlugin(...))` in one binding.
|
|
5129
5315
|
- **A policy loses the types.** It does not: `latest(handler)` keeps the payload, the result and the context of `handler`.
|
|
5130
|
-
- **Do not pass the context as a generic of `
|
|
5316
|
+
- **Do not pass the context as a generic of `createAction`.** It is the fourth generic of the handler form, after the payload, the result and the type string, so you would have to write all four. Annotate `ctx` with `AppContext`, or put the handler in a module made with the `createModule` of `bus.ts`.
|
|
5131
5317
|
|
|
5132
5318
|
---
|
|
5133
5319
|
Source: docs/migrating-from-saga.md
|
|
@@ -6008,13 +6194,13 @@ Every event of a task below another one has `rootId`, the id of the root of the
|
|
|
6008
6194
|
|
|
6009
6195
|
**Problem.** A flow such as a checkout has several roots (a click, a timer, a store) and many branches. Parent ids give a tree, but you want to filter the log by the flow.
|
|
6010
6196
|
|
|
6011
|
-
**When.** One business flow runs through several
|
|
6197
|
+
**When.** One business flow runs through several actions, and you read the log by flow.
|
|
6012
6198
|
|
|
6013
|
-
Give the flow a `traceGroupId` in the scope of the
|
|
6199
|
+
Give the flow a `traceGroupId` in the scope of the action or the job that starts it. Every task that it starts through `ctx.emit` or `ctx.run` has the same `traceGroupId`, and so does every trace event of those tasks. A task that declares its own `traceGroupId` uses it for itself and for what it starts.
|
|
6014
6200
|
|
|
6015
6201
|
```js
|
|
6016
6202
|
import { createBus } from "nanoeffects";
|
|
6017
|
-
import {
|
|
6203
|
+
import { createAction } from "nanoeffects/actions";
|
|
6018
6204
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
6019
6205
|
|
|
6020
6206
|
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
@@ -6025,7 +6211,7 @@ const bus = createBus({ trace })
|
|
|
6025
6211
|
.use(tracePlugin());
|
|
6026
6212
|
|
|
6027
6213
|
// the scope names the flow; traceGroupId is for the tracer, not a field of ctx
|
|
6028
|
-
const pay =
|
|
6214
|
+
const pay = createAction("checkout/pay", async () => {}, {
|
|
6029
6215
|
traceGroupId: "checkout",
|
|
6030
6216
|
});
|
|
6031
6217
|
```
|
|
@@ -6078,7 +6264,7 @@ The names work wherever an event or a listener is made:
|
|
|
6078
6264
|
| A listener, `bus.on` and `bus.once` | `{ traceId }` as the third argument |
|
|
6079
6265
|
| A listener of a module or of an instance of `extendBus` | the same third argument of its `on` and `once` |
|
|
6080
6266
|
| A method with `@On` or `@Once` | `{ traceId }` in the decorator |
|
|
6081
|
-
|
|
|
6267
|
+
| An action or a job | `traceGroupId` in its scope |
|
|
6082
6268
|
|
|
6083
6269
|
## Turn the names off
|
|
6084
6270
|
|
|
@@ -6423,7 +6609,7 @@ When a file is edited, the bundler loads its new version and runs it again, whil
|
|
|
6423
6609
|
|
|
6424
6610
|
## Replace modules by name
|
|
6425
6611
|
|
|
6426
|
-
Mounting a module whose name is taken replaces the mounted one. The old module is unmounted first — and with it its subscriptions, bound
|
|
6612
|
+
Mounting a module whose name is taken replaces the mounted one. The old module is unmounted first — and with it its subscriptions, bound handlers, sub-modules, and background tasks (their signals abort). The new version of a module file only has to mount again; nothing piles up.
|
|
6427
6613
|
|
|
6428
6614
|
- **Names must be unique.** The name means “this is the new version of that module”, so two different modules with one name replace each other. The name is also the type of the module’s tasks in the trace.
|
|
6429
6615
|
- **Pass `signal` to what the module does.** An unmount aborts the tasks, but a `fetch` or a loop that ignores `ctx.signal` runs on with the old code.
|