nanoeffects 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/{bus-duCCrqrr.js → bus-hHp-T7JV.js} +1 -1
- package/dist/context-UcmrAIhP.js +1 -0
- package/dist/{notrace/delay-BosJKFjg.js → delay-mxZkTg17.js} +1 -1
- package/dist/dev.d.ts +1 -1
- package/dist/effects.d.ts +4 -4
- package/dist/effects.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{module-B0KVDNjY.d.ts → module-D1VVKbE0.d.ts} +1 -1
- package/dist/notrace/actions.d.ts +1 -1
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{bus-BkUtjPHp.js → bus-BdyvumpX.js} +1 -1
- package/dist/notrace/context-_HWcdjZf.js +1 -0
- package/dist/{delay-BnBdGPh7.js → notrace/delay-CqKplsaY.js} +1 -1
- package/dist/notrace/effects.d.ts +4 -4
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-C6KA4dX-.d.ts → plugin-BEvYzo8Q.d.ts} +52 -29
- package/dist/notrace/policies.js +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-D9UTzGRG.d.ts → plugin-BZK83Z3N.d.ts} +52 -29
- package/dist/policies.js +1 -1
- package/dist/react.js +1 -1
- package/dist/scope.d.ts +1 -1
- package/dist/scope.js +1 -1
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +46 -0
- package/dist/svelte.js +1 -0
- package/dist/trace.js +1 -1
- package/dist/{types-CbdJKC2z.d.ts → types-CBoV9m7W.d.ts} +1 -1
- package/dist/{with-policy-CW50Edgk.js → with-policy-D-SQRkPh.js} +1 -1
- package/docs/README.md +3 -0
- package/docs/actions.md +1 -1
- package/docs/chat-example.md +33 -27
- package/docs/effects.md +36 -5
- package/docs/migrating-from-rxjs.md +2 -2
- package/docs/migrating-from-saga.md +3 -1
- package/docs/package.md +1 -0
- package/docs/server.md +205 -0
- package/docs/starter.md +3 -3
- package/docs/stores.md +1 -1
- package/docs/svelte.md +81 -0
- package/llms-full.txt +375 -41
- package/llms.txt +4 -2
- package/package.json +24 -4
- package/dist/context-CJGW6OWj.js +0 -1
- package/dist/notrace/context-yYU3_9lq.js +0 -1
package/docs/package.md
CHANGED
|
@@ -16,6 +16,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
16
16
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
17
17
|
| `nanoeffects/observable` | `createObserve`, `Subject` |
|
|
18
18
|
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
|
|
19
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) |
|
|
19
20
|
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) |
|
|
20
21
|
| `nanoeffects/dev` | `devPlugin` (no-op in production) |
|
|
21
22
|
| `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
|
package/docs/server.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Stop work on a server when the client leaves
|
|
2
|
+
|
|
3
|
+
Read this page if you run a Node server (the examples use [Fastify](https://fastify.dev)) and want the work for a request, a socket or a queued job to stop when nobody needs it. It shows one pattern in three places: a request, a WebSocket and a worker.
|
|
4
|
+
|
|
5
|
+
You need Node 18 or newer, `fastify` and `nanoeffects`. `api` below is your own client for another service: replace it with your requests.
|
|
6
|
+
|
|
7
|
+
## Why a server needs this
|
|
8
|
+
|
|
9
|
+
A browser tab that closes stops its own requests. A server does not: if a client leaves, the requests that the server made for it keep running, and so do their retries. On a busy server that is wasted work and a slower service for everyone else.
|
|
10
|
+
|
|
11
|
+
The fix is one `AbortController` for each unit of work that the client owns, and one rule: **when the client goes, abort the controller.** `signalPlugin` lets `bus.emit` take that signal, so `timeout`, `retry` and the request itself stop.
|
|
12
|
+
|
|
13
|
+
| Unit of work | Created when | Aborted when |
|
|
14
|
+
| ------------ | ------------ | ------------ |
|
|
15
|
+
| An HTTP request | the route starts | the connection closes before the answer |
|
|
16
|
+
| A WebSocket | the socket connects | the socket closes |
|
|
17
|
+
| A queued job | the job is accepted | the server shuts down |
|
|
18
|
+
|
|
19
|
+
## Make the bus and the command
|
|
20
|
+
|
|
21
|
+
The bus and the command live in files that know nothing about HTTP, so the same command can run in a route, a test or a worker.
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
// bus.js
|
|
25
|
+
import { createBus } from "nanoeffects";
|
|
26
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
27
|
+
|
|
28
|
+
// createBus — the bus the server shares
|
|
29
|
+
// signalPlugin — lets emit take { signal }, so a closed connection can abort the work
|
|
30
|
+
export const bus = createBus().use(signalPlugin());
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The command 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.
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// user.js
|
|
37
|
+
import { createCommand } from "nanoeffects/actions";
|
|
38
|
+
import { retry, timeout } from "nanoeffects/effects";
|
|
39
|
+
|
|
40
|
+
// createCommand — a command with a handler: emit returns what the handler returns
|
|
41
|
+
export const getUser = createCommand("user/get", async (id, ctx) => {
|
|
42
|
+
// one try: the request, with the signal of timeout so it can stop it
|
|
43
|
+
const load = ({ signal }) => api.getUser(id, { signal });
|
|
44
|
+
// timeout — fail the try after 5 s
|
|
45
|
+
const attempt = () => timeout(ctx, 5000, load);
|
|
46
|
+
// retry — up to 3 tries, 500 ms apart
|
|
47
|
+
return retry(ctx, attempt, { times: 3, delay: 500 });
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Stop the work of a request
|
|
52
|
+
|
|
53
|
+
<!-- docs-check: continue -->
|
|
54
|
+
|
|
55
|
+
Make a controller when the route starts. Listen to `close` of the **response** (`reply.raw`): it ends when the connection ends. `request.raw` also emits `close`, but it does so when the request body has been read, not when the client leaves, so it is the wrong event here.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// server.js
|
|
59
|
+
import Fastify from "fastify";
|
|
60
|
+
import { bus } from "./bus";
|
|
61
|
+
import { getUser } from "./user";
|
|
62
|
+
|
|
63
|
+
export const app = Fastify();
|
|
64
|
+
|
|
65
|
+
// the controllers of the requests that are still being answered, for the shutdown below
|
|
66
|
+
export const inFlight = new Set();
|
|
67
|
+
|
|
68
|
+
app.get("/users/:id", async (request, reply) => {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
inFlight.add(controller);
|
|
71
|
+
|
|
72
|
+
// close on the response — the connection ended; if the answer was not finished, the client left
|
|
73
|
+
reply.raw.on("close", () => {
|
|
74
|
+
inFlight.delete(controller);
|
|
75
|
+
if (!reply.raw.writableFinished) {
|
|
76
|
+
controller.abort();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// emit with { signal } — aborting the controller aborts the command: timeout, retry and the request
|
|
82
|
+
const call = getUser(request.params.id);
|
|
83
|
+
return await bus.emit(call, { signal: controller.signal });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// nobody is left to answer when the client closed the connection
|
|
86
|
+
if (reply.raw.destroyed) {
|
|
87
|
+
return reply;
|
|
88
|
+
}
|
|
89
|
+
return reply.code(502).send({ error: String(error) });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
After an abort the command rejects with the abort reason. The `catch` above ignores it when the connection is gone, because there is nobody to answer.
|
|
95
|
+
|
|
96
|
+
## Stop the server
|
|
97
|
+
|
|
98
|
+
<!-- docs-check: continue -->
|
|
99
|
+
|
|
100
|
+
Keep the controllers of the requests in flight in a set. On SIGTERM (what a container sends) or SIGINT (Ctrl+C), abort them all, then close the app. A request that is aborted this way answers with an error, and `app.close()` waits for the connections to end.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// shutdown.js
|
|
104
|
+
import { app, inFlight } from "./server";
|
|
105
|
+
|
|
106
|
+
// SIGTERM is what a container sends, SIGINT is Ctrl+C
|
|
107
|
+
for (const name of ["SIGTERM", "SIGINT"]) {
|
|
108
|
+
process.on(name, async () => {
|
|
109
|
+
for (const controller of inFlight) {
|
|
110
|
+
controller.abort();
|
|
111
|
+
}
|
|
112
|
+
await app.close();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The route above fills `inFlight` and empties it in the `close` listener. The [api-server example](../examples/api-server) does the same, with a limit of two seconds for the close.
|
|
119
|
+
|
|
120
|
+
## Tie a WebSocket to its work
|
|
121
|
+
|
|
122
|
+
<!-- docs-check: continue -->
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
First the command. It sends a price now and then every second, until its task is aborted.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
// prices.js
|
|
130
|
+
import { createCommand } from "nanoeffects/actions";
|
|
131
|
+
import { poll } from "nanoeffects/effects";
|
|
132
|
+
import { latest } from "nanoeffects/policies";
|
|
133
|
+
|
|
134
|
+
// createCommand — a command with a handler
|
|
135
|
+
// latest — a new subscription stops the old one; key — but only the old one of the same socket
|
|
136
|
+
export const watchPrices = createCommand(
|
|
137
|
+
"prices/watch",
|
|
138
|
+
latest(
|
|
139
|
+
async (call, ctx) => {
|
|
140
|
+
// poll — call the function now and every second, until the task is aborted
|
|
141
|
+
await poll(ctx, () => call.send(`${call.symbol}: ${Math.random()}`), 1000, {
|
|
142
|
+
leading: true,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
{ key: (call) => call.socketId },
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Without the `key`, one policy serves the whole command, and a new subscription on one socket would stop the subscription on another.
|
|
151
|
+
|
|
152
|
+
Then the route. Each socket gets an id and a controller.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
// sockets.js
|
|
156
|
+
import websocket from "@fastify/websocket";
|
|
157
|
+
import Fastify from "fastify";
|
|
158
|
+
import { bus } from "./bus";
|
|
159
|
+
import { watchPrices } from "./prices";
|
|
160
|
+
|
|
161
|
+
const sockets = Fastify();
|
|
162
|
+
sockets.register(websocket);
|
|
163
|
+
|
|
164
|
+
let lastSocketId = 0;
|
|
165
|
+
|
|
166
|
+
sockets.register(async (instance) => {
|
|
167
|
+
instance.get("/prices", { websocket: true }, (socket) => {
|
|
168
|
+
lastSocketId += 1;
|
|
169
|
+
const socketId = lastSocketId;
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const { signal } = controller;
|
|
172
|
+
|
|
173
|
+
// close — the client left: stop everything started for this socket
|
|
174
|
+
socket.on("close", () => controller.abort());
|
|
175
|
+
|
|
176
|
+
socket.on("message", (data) => {
|
|
177
|
+
const send = (text) => socket.send(text);
|
|
178
|
+
const call = watchPrices({ socketId, symbol: String(data), send });
|
|
179
|
+
// emit with { signal } — the call stops when the socket closes
|
|
180
|
+
bus.emit(call, { signal });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The [ws-server example](../examples/ws-server) adds a close for a silent socket, made of `race`, `take` and `delay`.
|
|
187
|
+
|
|
188
|
+
## Run a queue worker
|
|
189
|
+
|
|
190
|
+
A worker takes jobs from a client and answers at once, and the work goes on in the background. Use `createJob` with `queue` to limit how many run together, `retry` and `timeout` to give up, and the outcomes of the job (`pending`, `success`, `fail`, `cancel`) to count them. Give every `emit` of the job the same signal; aborting it at shutdown cancels the jobs that are left.
|
|
191
|
+
|
|
192
|
+
Two details matter:
|
|
193
|
+
|
|
194
|
+
- Bind the handler with `commands.handle(job, queue(...))` (or `m.handle` in a module), not as the second argument of `createJob`. Only a bound handler lets the job learn from the policy when a call really starts, so `pending` is sent at the start and not at the call.
|
|
195
|
+
- A call that is still waiting in the queue when the signal aborts never starts, and the job sends no outcome for it. Count such calls yourself if you need the totals to add up.
|
|
196
|
+
|
|
197
|
+
The [job-worker example](../examples/job-worker) has all of it: the counts, `GET /stats`, and a shutdown in three steps (stop taking jobs, wait, abort the rest).
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
| Folder | What it shows |
|
|
202
|
+
| ------ | ------------- |
|
|
203
|
+
| [`api-server`](../examples/api-server) | A Fastify server over PokeAPI: `timeout`, `retry`, `allSettled`, and the abort when the client leaves |
|
|
204
|
+
| [`ws-server`](../examples/ws-server) | A WebSocket server: one controller for each socket, `latest` with a `key`, an idle close |
|
|
205
|
+
| [`job-worker`](../examples/job-worker) | A queue worker: `queue`, `retry`, `timeout`, counts from the outcomes, a three-step shutdown |
|
package/docs/starter.md
CHANGED
|
@@ -48,7 +48,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
48
48
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
49
49
|
| `domains-ts` | Commands, 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 |
|
|
50
50
|
| `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
|
|
51
|
-
| `svelte-ts` | The `react-ts` app in Svelte 5, with
|
|
51
|
+
| `svelte-ts` | The `react-ts` app in Svelte 5, with `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
npm create nanoeffects@latest my-app -- --template react
|
|
@@ -74,7 +74,7 @@ npm create nanoeffects@latest my-app -- --template vue-ts
|
|
|
74
74
|
|
|
75
75
|
### Start with Svelte
|
|
76
76
|
|
|
77
|
-
`svelte-ts` is the search app of `react-ts` in Svelte 5.
|
|
77
|
+
`svelte-ts` is the search app of `react-ts` in Svelte 5. Its bus uses `sveltePlugin` from `nanoeffects/svelte`: `eventStore` makes a Svelte store from an event. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. The query goes to the bus from the `oninput` handler, so nothing is emitted when the component mounts. The store listens only while something reads it, and is [not a store of the app](svelte.md#it-is-not-a-store-of-the-app): a value that several components share belongs in a store of your own.
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
npm create nanoeffects@latest my-app -- --template svelte-ts
|
|
@@ -98,7 +98,7 @@ npm create nanoeffects@latest my-app -- --template domains-ts
|
|
|
98
98
|
| `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`. |
|
|
99
99
|
| `src/ui/` | The components. None of them calls the fake server in `src/api.ts`. |
|
|
100
100
|
|
|
101
|
-
For a bigger example
|
|
101
|
+
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.
|
|
102
102
|
|
|
103
103
|
The commands, jobs and modules themselves are explained in [Actions, commands, 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).
|
|
104
104
|
|
package/docs/stores.md
CHANGED
|
@@ -25,7 +25,7 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
|
|
|
25
25
|
|
|
26
26
|
`createEffectsMiddleware(bus)` does the same job: after the reducer runs, the action also hits the nanoeffects bus you pass to it. Keep RTK for state; use policies and effects for async.
|
|
27
27
|
|
|
28
|
-
Skip this section if you are not already on Redux. The recipe as a file, type-checked with the project: [`examples/redux.ts`](../examples/redux.ts).
|
|
28
|
+
Skip this section if you are not already on Redux. The recipe as a file, type-checked with the project: [`examples/redux.ts`](../examples/redux.ts). To run it, open the [counter example](../examples/counter): `redux.html` is a counter with Redux Toolkit and this middleware.
|
|
29
29
|
|
|
30
30
|
```js
|
|
31
31
|
// bus.js
|
package/docs/svelte.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Use nanoeffects in a Svelte app
|
|
2
|
+
|
|
3
|
+
Use this page when your app is in Svelte 5 and a component has to show the result of an event. `sveltePlugin` gives you one function for that, `eventStore`. To see it in a running project, start from the `svelte-ts` template in [Start a project from a template](starter.md#start-with-svelte).
|
|
4
|
+
|
|
5
|
+
You need Svelte 5 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
|
|
6
|
+
|
|
7
|
+
## Add the plugin to the bus
|
|
8
|
+
|
|
9
|
+
**Problem.** A Svelte component reads a value with `$store`. An event of the bus is not a store, so without help the component writes its own `readable` and its own `bus.on`, and has to remember to stop listening. The same few lines are copied into every component.
|
|
10
|
+
|
|
11
|
+
**When.** A component shows the payload of an event, such as the result of a search.
|
|
12
|
+
|
|
13
|
+
`sveltePlugin()` adds `eventStore` to the bus. Take it out of the bus once, in the file that makes it, and import it where you need it:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// bus.js
|
|
17
|
+
import { createBus } from "nanoeffects";
|
|
18
|
+
import { sveltePlugin } from "nanoeffects/svelte";
|
|
19
|
+
|
|
20
|
+
// createBus — the event bus, created once outside Svelte
|
|
21
|
+
// sveltePlugin — adds eventStore to the bus
|
|
22
|
+
export const bus = createBus().use(sveltePlugin());
|
|
23
|
+
export const { eventStore } = bus; // a free function: it does not read `this`
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Show the result of an event
|
|
27
|
+
|
|
28
|
+
<!-- docs-check: continue -->
|
|
29
|
+
|
|
30
|
+
`eventStore(event, initial)` returns a Svelte store. Its value is `initial` at first and the payload of every `event` after that. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. A payload that is a function is stored, not called.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// search.js
|
|
34
|
+
import { createAction } from "nanoeffects/actions";
|
|
35
|
+
|
|
36
|
+
// createAction — an event with a name; calling it builds { type, payload }
|
|
37
|
+
export const searchLoaded = createAction("search/loaded");
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
<!-- docs-check: skip - a .svelte file -->
|
|
41
|
+
```svelte
|
|
42
|
+
<script>
|
|
43
|
+
import { eventStore } from "./bus";
|
|
44
|
+
import { searchLoaded } from "./search";
|
|
45
|
+
|
|
46
|
+
// eventStore — like a store of [], but searchLoaded sets the value
|
|
47
|
+
const found = eventStore(searchLoaded, []);
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<ul>
|
|
51
|
+
{#each $found as title (title)}
|
|
52
|
+
<li>{title}</li>
|
|
53
|
+
{/each}
|
|
54
|
+
</ul>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
In TypeScript the type of the store comes from the event: with `createAction<string[]>("search/loaded")` it is `Readable<string[]>`, and for an event with no payload type it comes from `initial`.
|
|
58
|
+
|
|
59
|
+
To send a value to the bus, call `bus.emit` from the handler of the event, for example `oninput`. Nothing is emitted when the component mounts, so no plugin function is needed for that direction.
|
|
60
|
+
|
|
61
|
+
### It is not a store of the app
|
|
62
|
+
|
|
63
|
+
The bus listens only while something reads the store: the first `$found` starts it, and the last one stops it. The bus keeps no values, so an event that came while nothing read the store is lost, and a component that mounts later starts from `initial`. To keep a value for components that come later, keep it in a store of your own and write to it from a handler of the event:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// found-store.js
|
|
67
|
+
import { writable } from "svelte/store";
|
|
68
|
+
import { bus } from "./bus";
|
|
69
|
+
import { searchLoaded } from "./search";
|
|
70
|
+
|
|
71
|
+
// writable — a Svelte store that you keep for the whole app
|
|
72
|
+
export const foundTitles = writable([]);
|
|
73
|
+
|
|
74
|
+
// bus.on — copy every searchLoaded into the store, whether or not a component is open
|
|
75
|
+
bus.on(searchLoaded, (titles) => foundTitles.set(titles));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Where to go next
|
|
79
|
+
|
|
80
|
+
- The same job for React: [Build a sign-up form in React](react.md); for Vue: [Use nanoeffects in a Vue app](vue.md).
|
|
81
|
+
- A policy in a component (a debounced save): in the script of a component make it once with [`withPolicy`](policies.md#use-a-policy-without-a-bus); Svelte runs the script once, so it needs no hook.
|