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/llms-full.txt
CHANGED
|
@@ -9,7 +9,7 @@ Rules that generated code often gets wrong:
|
|
|
9
9
|
- A handler gets the payload and a task context: `(payload, ctx)`. The context has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`.
|
|
10
10
|
- Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
|
|
11
11
|
- Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
|
|
12
|
-
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
12
|
+
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/svelte` (`sveltePlugin`, adds `bus.eventStore`; needs Svelte), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
13
13
|
- `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -553,7 +553,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
553
553
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
554
554
|
| `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 |
|
|
555
555
|
| `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
|
|
556
|
-
| `svelte-ts` | The `react-ts` app in Svelte 5, with
|
|
556
|
+
| `svelte-ts` | The `react-ts` app in Svelte 5, with `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
|
|
557
557
|
|
|
558
558
|
```bash
|
|
559
559
|
npm create nanoeffects@latest my-app -- --template react
|
|
@@ -579,7 +579,7 @@ npm create nanoeffects@latest my-app -- --template vue-ts
|
|
|
579
579
|
|
|
580
580
|
### Start with Svelte
|
|
581
581
|
|
|
582
|
-
`svelte-ts` is the search app of `react-ts` in Svelte 5.
|
|
582
|
+
`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.
|
|
583
583
|
|
|
584
584
|
```bash
|
|
585
585
|
npm create nanoeffects@latest my-app -- --template svelte-ts
|
|
@@ -603,7 +603,7 @@ npm create nanoeffects@latest my-app -- --template domains-ts
|
|
|
603
603
|
| `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`. |
|
|
604
604
|
| `src/ui/` | The components. None of them calls the fake server in `src/api.ts`. |
|
|
605
605
|
|
|
606
|
-
For a bigger example
|
|
606
|
+
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.
|
|
607
607
|
|
|
608
608
|
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).
|
|
609
609
|
|
|
@@ -692,6 +692,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
692
692
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
693
693
|
| `nanoeffects/observable` | `createObserve`, `Subject` |
|
|
694
694
|
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
|
|
695
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) |
|
|
695
696
|
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) |
|
|
696
697
|
| `nanoeffects/dev` | `devPlugin` (no-op in production) |
|
|
697
698
|
| `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
|
|
@@ -1744,7 +1745,8 @@ bus.on("load", async (id, { race, all, delay }) => {
|
|
|
1744
1745
|
// race — same as race(ctx, …)
|
|
1745
1746
|
const result = await race({
|
|
1746
1747
|
data: ({ signal }) => fetchData(id, { signal }),
|
|
1747
|
-
|
|
1748
|
+
// a starter gets its own child with the same bound effects, so delay(5000) stops with the race
|
|
1749
|
+
timeout: ({ delay }) => delay(5000),
|
|
1748
1750
|
});
|
|
1749
1751
|
if ("timeout" in result) throw new Error("timed out");
|
|
1750
1752
|
|
|
@@ -1827,28 +1829,35 @@ bus.on("watch", (_, ctx) => {
|
|
|
1827
1829
|
- Under `latest`, subscriptions of a superseded run end with that run.
|
|
1828
1830
|
- `ctx.off(event)` clears every handler of the event on the bus, not only the task’s.
|
|
1829
1831
|
|
|
1830
|
-
`take(ctx, event, options?)` waits for the next `event` and resolves with its payload
|
|
1832
|
+
`take(ctx, event, options?)` waits for the next `event` and resolves with its payload. `event` is the name of an event or an action creator, as for `bus.on`:
|
|
1831
1833
|
|
|
1832
1834
|
```js
|
|
1833
1835
|
import { createBus } from "nanoeffects";
|
|
1836
|
+
import { createAction } from "nanoeffects/actions";
|
|
1834
1837
|
import { take, timeout } from "nanoeffects/effects";
|
|
1835
1838
|
|
|
1836
1839
|
const bus = createBus();
|
|
1840
|
+
const stopRequested = createAction("stop"); // an event with a name; calling it builds the action
|
|
1837
1841
|
|
|
1838
1842
|
bus.on("login", async (user, ctx) => {
|
|
1839
1843
|
const reason = await take(ctx, "logout"); // resolves on the next logout
|
|
1840
1844
|
cleanup(user, reason);
|
|
1841
1845
|
|
|
1846
|
+
// an action creator works the same, and its payload type is the type of the result
|
|
1847
|
+
const stopped = await take(ctx, stopRequested);
|
|
1848
|
+
console.log(stopped);
|
|
1849
|
+
|
|
1842
1850
|
// wait for a matching event, but not longer than 5 s
|
|
1843
|
-
const id = await timeout(ctx, 5000, (
|
|
1844
|
-
take(
|
|
1851
|
+
const id = await timeout(ctx, 5000, (child) =>
|
|
1852
|
+
take(child, "saved", { predicate: (savedId) => savedId === draft.id }),
|
|
1845
1853
|
);
|
|
1846
1854
|
});
|
|
1847
1855
|
```
|
|
1848
1856
|
|
|
1849
1857
|
- Sees only events emitted after the call, takes one, and unsubscribes.
|
|
1858
|
+
- With an action creator the result has the payload type of the creator. A creator made without a payload type (`createAction("saved")`) takes it from the event map of the bus.
|
|
1850
1859
|
- `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
|
|
1851
|
-
- `options.signal` stops the wait
|
|
1860
|
+
- `options.signal` stops the wait. In a starter of `race`, `all` or `timeout` pass `child` as the first argument instead: it already carries the signal.
|
|
1852
1861
|
- On abort of the task or of `options.signal`, unsubscribes and rejects with the abort reason.
|
|
1853
1862
|
|
|
1854
1863
|
## Run several starters together
|
|
@@ -1877,6 +1886,29 @@ bus.on("open", async (id, ctx) => {
|
|
|
1877
1886
|
|
|
1878
1887
|
`race(ctx, effects)` settles with the first starter that finishes and cancels the others.
|
|
1879
1888
|
|
|
1889
|
+
```js
|
|
1890
|
+
import { createBus } from "nanoeffects";
|
|
1891
|
+
import { delay, race, take } from "nanoeffects/effects";
|
|
1892
|
+
|
|
1893
|
+
const bus = createBus();
|
|
1894
|
+
|
|
1895
|
+
bus.on("save", async (draft, ctx) => {
|
|
1896
|
+
// race — the first starter to finish wins, the other is cancelled
|
|
1897
|
+
const result = await race(ctx, {
|
|
1898
|
+
waited: (child) => delay(child, 1000),
|
|
1899
|
+
cancelled: (child) => take(child, "cancel"),
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
if ("waited" in result) {
|
|
1903
|
+
ctx.emit("saved", draft);
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
**A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options: `({ signal }) => fetchUser(id, { signal })`. The same holds for `all`, `allSettled` and `timeout`.
|
|
1909
|
+
|
|
1910
|
+
If `ctx` is `{}` or a bare `{ signal }` (outside a task), `child` has only `signal` and `abort`, so it fits `delay` and `poll` but not `take`, which needs a bus.
|
|
1911
|
+
|
|
1880
1912
|
## Pass `ctx` outside a task
|
|
1881
1913
|
|
|
1882
1914
|
Effects that wait inside a body take `ctx` first: `delay`, `all`, `allSettled`, `race`, `cancellable`, `onAbort`, `take`, and the `ctx` forms of `retry` / `timeout` / `poll`. Outside a task, pass `{}` or any object with a `signal`.
|
|
@@ -1934,7 +1966,7 @@ If you only need `on` / `emit` or policies, stay in the [Guide](guide.md) until
|
|
|
1934
1966
|
|
|
1935
1967
|
Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
|
|
1936
1968
|
|
|
1937
|
-
A chat with
|
|
1969
|
+
A chat with two entities and three features, jobs under policies and effects, and a stream tied to a module: [Build a chat with entities, features, modules and a small server](chat-example.md).
|
|
1938
1970
|
|
|
1939
1971
|
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)).
|
|
1940
1972
|
|
|
@@ -2287,7 +2319,7 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
|
|
|
2287
2319
|
|
|
2288
2320
|
`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.
|
|
2289
2321
|
|
|
2290
|
-
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).
|
|
2322
|
+
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.
|
|
2291
2323
|
|
|
2292
2324
|
```js
|
|
2293
2325
|
// bus.js
|
|
@@ -3062,6 +3094,215 @@ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the c
|
|
|
3062
3094
|
|
|
3063
3095
|
A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
|
|
3064
3096
|
|
|
3097
|
+
---
|
|
3098
|
+
Source: docs/server.md
|
|
3099
|
+
|
|
3100
|
+
# Stop work on a server when the client leaves
|
|
3101
|
+
|
|
3102
|
+
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.
|
|
3103
|
+
|
|
3104
|
+
You need Node 18 or newer, `fastify` and `nanoeffects`. `api` below is your own client for another service: replace it with your requests.
|
|
3105
|
+
|
|
3106
|
+
## Why a server needs this
|
|
3107
|
+
|
|
3108
|
+
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.
|
|
3109
|
+
|
|
3110
|
+
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.
|
|
3111
|
+
|
|
3112
|
+
| Unit of work | Created when | Aborted when |
|
|
3113
|
+
| ------------ | ------------ | ------------ |
|
|
3114
|
+
| An HTTP request | the route starts | the connection closes before the answer |
|
|
3115
|
+
| A WebSocket | the socket connects | the socket closes |
|
|
3116
|
+
| A queued job | the job is accepted | the server shuts down |
|
|
3117
|
+
|
|
3118
|
+
## Make the bus and the command
|
|
3119
|
+
|
|
3120
|
+
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.
|
|
3121
|
+
|
|
3122
|
+
```js
|
|
3123
|
+
// bus.js
|
|
3124
|
+
import { createBus } from "nanoeffects";
|
|
3125
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
3126
|
+
|
|
3127
|
+
// createBus — the bus the server shares
|
|
3128
|
+
// signalPlugin — lets emit take { signal }, so a closed connection can abort the work
|
|
3129
|
+
export const bus = createBus().use(signalPlugin());
|
|
3130
|
+
```
|
|
3131
|
+
|
|
3132
|
+
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.
|
|
3133
|
+
|
|
3134
|
+
```js
|
|
3135
|
+
// user.js
|
|
3136
|
+
import { createCommand } from "nanoeffects/actions";
|
|
3137
|
+
import { retry, timeout } from "nanoeffects/effects";
|
|
3138
|
+
|
|
3139
|
+
// createCommand — a command with a handler: emit returns what the handler returns
|
|
3140
|
+
export const getUser = createCommand("user/get", async (id, ctx) => {
|
|
3141
|
+
// one try: the request, with the signal of timeout so it can stop it
|
|
3142
|
+
const load = ({ signal }) => api.getUser(id, { signal });
|
|
3143
|
+
// timeout — fail the try after 5 s
|
|
3144
|
+
const attempt = () => timeout(ctx, 5000, load);
|
|
3145
|
+
// retry — up to 3 tries, 500 ms apart
|
|
3146
|
+
return retry(ctx, attempt, { times: 3, delay: 500 });
|
|
3147
|
+
});
|
|
3148
|
+
```
|
|
3149
|
+
|
|
3150
|
+
## Stop the work of a request
|
|
3151
|
+
|
|
3152
|
+
<!-- docs-check: continue -->
|
|
3153
|
+
|
|
3154
|
+
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.
|
|
3155
|
+
|
|
3156
|
+
```js
|
|
3157
|
+
// server.js
|
|
3158
|
+
import Fastify from "fastify";
|
|
3159
|
+
import { bus } from "./bus";
|
|
3160
|
+
import { getUser } from "./user";
|
|
3161
|
+
|
|
3162
|
+
export const app = Fastify();
|
|
3163
|
+
|
|
3164
|
+
// the controllers of the requests that are still being answered, for the shutdown below
|
|
3165
|
+
export const inFlight = new Set();
|
|
3166
|
+
|
|
3167
|
+
app.get("/users/:id", async (request, reply) => {
|
|
3168
|
+
const controller = new AbortController();
|
|
3169
|
+
inFlight.add(controller);
|
|
3170
|
+
|
|
3171
|
+
// close on the response — the connection ended; if the answer was not finished, the client left
|
|
3172
|
+
reply.raw.on("close", () => {
|
|
3173
|
+
inFlight.delete(controller);
|
|
3174
|
+
if (!reply.raw.writableFinished) {
|
|
3175
|
+
controller.abort();
|
|
3176
|
+
}
|
|
3177
|
+
});
|
|
3178
|
+
|
|
3179
|
+
try {
|
|
3180
|
+
// emit with { signal } — aborting the controller aborts the command: timeout, retry and the request
|
|
3181
|
+
const call = getUser(request.params.id);
|
|
3182
|
+
return await bus.emit(call, { signal: controller.signal });
|
|
3183
|
+
} catch (error) {
|
|
3184
|
+
// nobody is left to answer when the client closed the connection
|
|
3185
|
+
if (reply.raw.destroyed) {
|
|
3186
|
+
return reply;
|
|
3187
|
+
}
|
|
3188
|
+
return reply.code(502).send({ error: String(error) });
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
```
|
|
3192
|
+
|
|
3193
|
+
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.
|
|
3194
|
+
|
|
3195
|
+
## Stop the server
|
|
3196
|
+
|
|
3197
|
+
<!-- docs-check: continue -->
|
|
3198
|
+
|
|
3199
|
+
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.
|
|
3200
|
+
|
|
3201
|
+
```js
|
|
3202
|
+
// shutdown.js
|
|
3203
|
+
import { app, inFlight } from "./server";
|
|
3204
|
+
|
|
3205
|
+
// SIGTERM is what a container sends, SIGINT is Ctrl+C
|
|
3206
|
+
for (const name of ["SIGTERM", "SIGINT"]) {
|
|
3207
|
+
process.on(name, async () => {
|
|
3208
|
+
for (const controller of inFlight) {
|
|
3209
|
+
controller.abort();
|
|
3210
|
+
}
|
|
3211
|
+
await app.close();
|
|
3212
|
+
process.exit(0);
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
```
|
|
3216
|
+
|
|
3217
|
+
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.
|
|
3218
|
+
|
|
3219
|
+
## Tie a WebSocket to its work
|
|
3220
|
+
|
|
3221
|
+
<!-- docs-check: continue -->
|
|
3222
|
+
|
|
3223
|
+
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.
|
|
3224
|
+
|
|
3225
|
+
First the command. It sends a price now and then every second, until its task is aborted.
|
|
3226
|
+
|
|
3227
|
+
```js
|
|
3228
|
+
// prices.js
|
|
3229
|
+
import { createCommand } from "nanoeffects/actions";
|
|
3230
|
+
import { poll } from "nanoeffects/effects";
|
|
3231
|
+
import { latest } from "nanoeffects/policies";
|
|
3232
|
+
|
|
3233
|
+
// createCommand — a command with a handler
|
|
3234
|
+
// latest — a new subscription stops the old one; key — but only the old one of the same socket
|
|
3235
|
+
export const watchPrices = createCommand(
|
|
3236
|
+
"prices/watch",
|
|
3237
|
+
latest(
|
|
3238
|
+
async (call, ctx) => {
|
|
3239
|
+
// poll — call the function now and every second, until the task is aborted
|
|
3240
|
+
await poll(ctx, () => call.send(`${call.symbol}: ${Math.random()}`), 1000, {
|
|
3241
|
+
leading: true,
|
|
3242
|
+
});
|
|
3243
|
+
},
|
|
3244
|
+
{ key: (call) => call.socketId },
|
|
3245
|
+
),
|
|
3246
|
+
);
|
|
3247
|
+
```
|
|
3248
|
+
|
|
3249
|
+
Without the `key`, one policy serves the whole command, and a new subscription on one socket would stop the subscription on another.
|
|
3250
|
+
|
|
3251
|
+
Then the route. Each socket gets an id and a controller.
|
|
3252
|
+
|
|
3253
|
+
```js
|
|
3254
|
+
// sockets.js
|
|
3255
|
+
import websocket from "@fastify/websocket";
|
|
3256
|
+
import Fastify from "fastify";
|
|
3257
|
+
import { bus } from "./bus";
|
|
3258
|
+
import { watchPrices } from "./prices";
|
|
3259
|
+
|
|
3260
|
+
const sockets = Fastify();
|
|
3261
|
+
sockets.register(websocket);
|
|
3262
|
+
|
|
3263
|
+
let lastSocketId = 0;
|
|
3264
|
+
|
|
3265
|
+
sockets.register(async (instance) => {
|
|
3266
|
+
instance.get("/prices", { websocket: true }, (socket) => {
|
|
3267
|
+
lastSocketId += 1;
|
|
3268
|
+
const socketId = lastSocketId;
|
|
3269
|
+
const controller = new AbortController();
|
|
3270
|
+
const { signal } = controller;
|
|
3271
|
+
|
|
3272
|
+
// close — the client left: stop everything started for this socket
|
|
3273
|
+
socket.on("close", () => controller.abort());
|
|
3274
|
+
|
|
3275
|
+
socket.on("message", (data) => {
|
|
3276
|
+
const send = (text) => socket.send(text);
|
|
3277
|
+
const call = watchPrices({ socketId, symbol: String(data), send });
|
|
3278
|
+
// emit with { signal } — the call stops when the socket closes
|
|
3279
|
+
bus.emit(call, { signal });
|
|
3280
|
+
});
|
|
3281
|
+
});
|
|
3282
|
+
});
|
|
3283
|
+
```
|
|
3284
|
+
|
|
3285
|
+
The [ws-server example](../examples/ws-server) adds a close for a silent socket, made of `race`, `take` and `delay`.
|
|
3286
|
+
|
|
3287
|
+
## Run a queue worker
|
|
3288
|
+
|
|
3289
|
+
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.
|
|
3290
|
+
|
|
3291
|
+
Two details matter:
|
|
3292
|
+
|
|
3293
|
+
- 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.
|
|
3294
|
+
- 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.
|
|
3295
|
+
|
|
3296
|
+
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).
|
|
3297
|
+
|
|
3298
|
+
## Examples
|
|
3299
|
+
|
|
3300
|
+
| Folder | What it shows |
|
|
3301
|
+
| ------ | ------------- |
|
|
3302
|
+
| [`api-server`](../examples/api-server) | A Fastify server over PokeAPI: `timeout`, `retry`, `allSettled`, and the abort when the client leaves |
|
|
3303
|
+
| [`ws-server`](../examples/ws-server) | A WebSocket server: one controller for each socket, `latest` with a `key`, an idle close |
|
|
3304
|
+
| [`job-worker`](../examples/job-worker) | A queue worker: `queue`, `retry`, `timeout`, counts from the outcomes, a three-step shutdown |
|
|
3305
|
+
|
|
3065
3306
|
---
|
|
3066
3307
|
Source: docs/react.md
|
|
3067
3308
|
|
|
@@ -3348,7 +3589,7 @@ export const Search = ({ wait }) => {
|
|
|
3348
3589
|
---
|
|
3349
3590
|
Source: docs/chat-example.md
|
|
3350
3591
|
|
|
3351
|
-
# Build a chat with
|
|
3592
|
+
# Build a chat with entities, features, modules and a small server
|
|
3352
3593
|
|
|
3353
3594
|
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.
|
|
3354
3595
|
|
|
@@ -3380,21 +3621,27 @@ Open the Network panel of the browser while you do this. A request that the page
|
|
|
3380
3621
|
|
|
3381
3622
|
## See how the files are split
|
|
3382
3623
|
|
|
3383
|
-
|
|
3624
|
+
The code is in layers, and a file imports only from the layers below it. The components only emit events and read values; they never call the server.
|
|
3384
3625
|
|
|
3385
|
-
| Path | Job |
|
|
3386
|
-
| ---- | --- |
|
|
3387
|
-
| `src/bus.ts` | Creates the bus. It imports no events and no
|
|
3388
|
-
| `src/
|
|
3389
|
-
| `src/
|
|
3390
|
-
| `
|
|
3391
|
-
| `src/
|
|
3392
|
-
| `src/
|
|
3393
|
-
| `src/
|
|
3394
|
-
| `src/
|
|
3395
|
-
| `
|
|
3626
|
+
| Path | Layer | Job |
|
|
3627
|
+
| ---- | ----- | --- |
|
|
3628
|
+
| `src/shared/bus.ts` | shared | Creates the bus. It imports no events, no entity and no feature. |
|
|
3629
|
+
| `src/shared/api.ts` | shared | The client of the server: one function per request. |
|
|
3630
|
+
| `src/entities/room/` | entity | The list of rooms and the selected room. |
|
|
3631
|
+
| `src/entities/message/` | entity | The history of the open room, sending a message, and a message that arrives. |
|
|
3632
|
+
| `src/features/show-history/` | feature | A selected room loads its history. |
|
|
3633
|
+
| `src/features/search-messages/` | feature | Searching every room and opening a result. |
|
|
3634
|
+
| `src/features/receive-messages/` | feature | New messages, asked for at an interval. |
|
|
3635
|
+
| `src/ui/`, `src/app.ts` | top | The components, and the file that mounts the modules. |
|
|
3636
|
+
| `server/` | | The Fastify server and the script that starts it with Vite. |
|
|
3637
|
+
|
|
3638
|
+
**The rule.** An entity is a thing with its own state and orders (a room, a message). It imports `shared` and nothing else. A feature is a scenario that connects things: it imports `shared` and entities, and never another feature. The top imports anything below. No two entities import each other and no two features import each other, and a test in the repository (`src/tests/architecture.test.ts`) fails when one does.
|
|
3639
|
+
|
|
3640
|
+
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.
|
|
3396
3641
|
|
|
3397
|
-
A
|
|
3642
|
+
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 events and commands, never through its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any feature may listen: `rooms/selected` is heard by two. A command (`createCommand`) is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createCommand`, so the command 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 a command 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.
|
|
3643
|
+
|
|
3644
|
+
The layers come from [Feature-Sliced Design](https://feature-sliced.design/), reduced to the three that an app of this size needs.
|
|
3398
3645
|
|
|
3399
3646
|
## Load a history that must not race
|
|
3400
3647
|
|
|
@@ -3405,7 +3652,7 @@ A domain folder has its events (`rooms.ts`, which imports no bus), its work (`ro
|
|
|
3405
3652
|
A job sends `pending` when its handler starts and exactly one of `success`, `fail` and `cancel` when it ends. `latest` around the handler aborts the older load when a newer one starts, so only the newer result is delivered, and the older one sends `cancel`:
|
|
3406
3653
|
|
|
3407
3654
|
```js
|
|
3408
|
-
//
|
|
3655
|
+
// message.js
|
|
3409
3656
|
import { createJob } from "nanoeffects/actions";
|
|
3410
3657
|
|
|
3411
3658
|
// createJob — a command that also sends pending, success, fail and cancel
|
|
@@ -3413,7 +3660,7 @@ export const loadHistory = createJob("messages/loadHistory");
|
|
|
3413
3660
|
```
|
|
3414
3661
|
|
|
3415
3662
|
```js
|
|
3416
|
-
//
|
|
3663
|
+
// message-handlers.js
|
|
3417
3664
|
import { latest } from "nanoeffects/policies";
|
|
3418
3665
|
|
|
3419
3666
|
// latest — a load for a newer room aborts the older one, and its result is never delivered
|
|
@@ -3423,12 +3670,12 @@ export const fetchHistory = latest((roomId, { signal }) => api.loadHistory(roomI
|
|
|
3423
3670
|
The module gives the job its work and keeps the state from the outcomes, so an aborted load clears the flag too (see [Show a loading state with a job](actions.md#show-a-loading-state-with-a-job)):
|
|
3424
3671
|
|
|
3425
3672
|
```js
|
|
3426
|
-
//
|
|
3673
|
+
// message-module.js
|
|
3427
3674
|
import { createModule } from "nanoeffects/actions";
|
|
3428
|
-
import { loadHistory } from "./
|
|
3429
|
-
import { fetchHistory } from "./
|
|
3675
|
+
import { loadHistory } from "./message";
|
|
3676
|
+
import { fetchHistory } from "./message-handlers";
|
|
3430
3677
|
|
|
3431
|
-
export const
|
|
3678
|
+
export const messageModule = createModule("message", (module) => {
|
|
3432
3679
|
// handle — this module does the work of loadHistory while it is mounted
|
|
3433
3680
|
module.handle(loadHistory, fetchHistory);
|
|
3434
3681
|
module.on(loadHistory.pending, () => console.log("show the spinner"));
|
|
@@ -3518,7 +3765,7 @@ export const runSearch = debounce(
|
|
|
3518
3765
|
|
|
3519
3766
|
**Problem.** The reader opens a search result from another room. The message cannot be marked until that room's history has loaded, and the handler must not guess how long that takes.
|
|
3520
3767
|
|
|
3521
|
-
**When.** A
|
|
3768
|
+
**When.** A feature starts work in an entity and needs to continue after its outcome.
|
|
3522
3769
|
|
|
3523
3770
|
`take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
|
|
3524
3771
|
|
|
@@ -3526,7 +3773,7 @@ export const runSearch = debounce(
|
|
|
3526
3773
|
import { createCommand } from "nanoeffects/actions";
|
|
3527
3774
|
import { take } from "nanoeffects/effects";
|
|
3528
3775
|
|
|
3529
|
-
// createCommand — an order for the
|
|
3776
|
+
// createCommand — an order for the message entity, with the handler that does it
|
|
3530
3777
|
const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
|
|
3531
3778
|
|
|
3532
3779
|
// the room that is open, kept from the rooms/selected event
|
|
@@ -3539,7 +3786,7 @@ export const goToResult = async (hit, ctx) => {
|
|
|
3539
3786
|
ctx.emit("rooms/selected", hit.roomId);
|
|
3540
3787
|
await loaded;
|
|
3541
3788
|
}
|
|
3542
|
-
// ctx.emit with a command — the
|
|
3789
|
+
// ctx.emit with a command — the message entity marks the message
|
|
3543
3790
|
ctx.emit(markMessage(hit.id));
|
|
3544
3791
|
};
|
|
3545
3792
|
```
|
|
@@ -3577,23 +3824,23 @@ const watchUpdates = async (_, ctx) => {
|
|
|
3577
3824
|
await poll(ctx, check, 3000, { leading: true });
|
|
3578
3825
|
};
|
|
3579
3826
|
|
|
3580
|
-
export const
|
|
3827
|
+
export const receiveMessagesModule = createModule("receive-messages", (module) => {
|
|
3581
3828
|
// spawn — a background task that lives as long as this module
|
|
3582
3829
|
module.spawn(watchUpdates);
|
|
3583
3830
|
});
|
|
3584
3831
|
```
|
|
3585
3832
|
|
|
3586
|
-
In the example a button calls `app.unmount(
|
|
3833
|
+
In the example a button calls `app.unmount(receiveMessagesModule)` and `app.mount(receiveMessagesModule)`, so you can watch the polling stop (the Network panel shows no more `updates` requests) and start.
|
|
3587
3834
|
|
|
3588
3835
|
## Know where each value lives
|
|
3589
3836
|
|
|
3590
|
-
Each
|
|
3837
|
+
Each entity and each feature that has state keeps it in a [Zustand](https://zustand.docs.pmnd.rs/) store that the module writes and a component reads with `useStore`. Each store lists its mutations, such as `startLoading(roomId)` and `markFailed(localId)`, and a module calls them with the data of an event: `module.on(loadHistory.pending, startLoading)`. A mutation that sets plain values gives `set` an object, as in `set({ searching: true })`. The store of the message entity uses the `immer` middleware, so a mutation that changes a list can change it in place, as in `state.messages.push(message)`, and the store still gives React a new object. Nothing else writes the state. The list of rooms is the exception: it is read with one `useEventState` in a provider at the root. `useEventState` is not a store: an event that came before the provider mounted is lost, and a second component gets its own copy. See [Show the last value of an event as state](react.md#show-the-last-value-of-an-event-as-state).
|
|
3591
3838
|
|
|
3592
3839
|
## Where to go next
|
|
3593
3840
|
|
|
3594
3841
|
- The API of commands, jobs and modules: [Actions, commands, jobs and modules](actions.md).
|
|
3595
3842
|
- Policies such as `latest` and `queue`: [Policies](policies.md). Effects such as `allSettled`, `take` and `poll`: [Effects](effects.md).
|
|
3596
|
-
- A smaller project with two domains: [Start with domains](starter.md#start-with-domains).
|
|
3843
|
+
- 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.
|
|
3597
3844
|
|
|
3598
3845
|
---
|
|
3599
3846
|
Source: docs/vue.md
|
|
@@ -3695,6 +3942,91 @@ In TypeScript the type of the `ref` comes from the event: with `createAction<str
|
|
|
3695
3942
|
- A policy in a component (a debounced save): in `<script setup>` make it once with [`withPolicy`](policies.md#use-a-policy-without-a-bus); Vue runs the setup once, so it needs no hook.
|
|
3696
3943
|
- Keep values in Pinia: [Connect Redux, Zustand, MobX, or Pinia](stores.md).
|
|
3697
3944
|
|
|
3945
|
+
---
|
|
3946
|
+
Source: docs/svelte.md
|
|
3947
|
+
|
|
3948
|
+
# Use nanoeffects in a Svelte app
|
|
3949
|
+
|
|
3950
|
+
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).
|
|
3951
|
+
|
|
3952
|
+
You need Svelte 5 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
|
|
3953
|
+
|
|
3954
|
+
## Add the plugin to the bus
|
|
3955
|
+
|
|
3956
|
+
**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.
|
|
3957
|
+
|
|
3958
|
+
**When.** A component shows the payload of an event, such as the result of a search.
|
|
3959
|
+
|
|
3960
|
+
`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:
|
|
3961
|
+
|
|
3962
|
+
```js
|
|
3963
|
+
// bus.js
|
|
3964
|
+
import { createBus } from "nanoeffects";
|
|
3965
|
+
import { sveltePlugin } from "nanoeffects/svelte";
|
|
3966
|
+
|
|
3967
|
+
// createBus — the event bus, created once outside Svelte
|
|
3968
|
+
// sveltePlugin — adds eventStore to the bus
|
|
3969
|
+
export const bus = createBus().use(sveltePlugin());
|
|
3970
|
+
export const { eventStore } = bus; // a free function: it does not read `this`
|
|
3971
|
+
```
|
|
3972
|
+
|
|
3973
|
+
## Show the result of an event
|
|
3974
|
+
|
|
3975
|
+
<!-- docs-check: continue -->
|
|
3976
|
+
|
|
3977
|
+
`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.
|
|
3978
|
+
|
|
3979
|
+
```js
|
|
3980
|
+
// search.js
|
|
3981
|
+
import { createAction } from "nanoeffects/actions";
|
|
3982
|
+
|
|
3983
|
+
// createAction — an event with a name; calling it builds { type, payload }
|
|
3984
|
+
export const searchLoaded = createAction("search/loaded");
|
|
3985
|
+
```
|
|
3986
|
+
|
|
3987
|
+
<!-- docs-check: skip - a .svelte file -->
|
|
3988
|
+
```svelte
|
|
3989
|
+
<script>
|
|
3990
|
+
import { eventStore } from "./bus";
|
|
3991
|
+
import { searchLoaded } from "./search";
|
|
3992
|
+
|
|
3993
|
+
// eventStore — like a store of [], but searchLoaded sets the value
|
|
3994
|
+
const found = eventStore(searchLoaded, []);
|
|
3995
|
+
</script>
|
|
3996
|
+
|
|
3997
|
+
<ul>
|
|
3998
|
+
{#each $found as title (title)}
|
|
3999
|
+
<li>{title}</li>
|
|
4000
|
+
{/each}
|
|
4001
|
+
</ul>
|
|
4002
|
+
```
|
|
4003
|
+
|
|
4004
|
+
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`.
|
|
4005
|
+
|
|
4006
|
+
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.
|
|
4007
|
+
|
|
4008
|
+
### It is not a store of the app
|
|
4009
|
+
|
|
4010
|
+
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:
|
|
4011
|
+
|
|
4012
|
+
```js
|
|
4013
|
+
// found-store.js
|
|
4014
|
+
import { writable } from "svelte/store";
|
|
4015
|
+
import { bus } from "./bus";
|
|
4016
|
+
import { searchLoaded } from "./search";
|
|
4017
|
+
|
|
4018
|
+
// writable — a Svelte store that you keep for the whole app
|
|
4019
|
+
export const foundTitles = writable([]);
|
|
4020
|
+
|
|
4021
|
+
// bus.on — copy every searchLoaded into the store, whether or not a component is open
|
|
4022
|
+
bus.on(searchLoaded, (titles) => foundTitles.set(titles));
|
|
4023
|
+
```
|
|
4024
|
+
|
|
4025
|
+
## Where to go next
|
|
4026
|
+
|
|
4027
|
+
- The same job for React: [Build a sign-up form in React](react.md); for Vue: [Use nanoeffects in a Vue app](vue.md).
|
|
4028
|
+
- 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.
|
|
4029
|
+
|
|
3698
4030
|
---
|
|
3699
4031
|
Source: docs/context.md
|
|
3700
4032
|
|
|
@@ -4633,7 +4965,7 @@ Read this page if you know redux-saga and want to find the same job in nanoeffec
|
|
|
4633
4965
|
| `fork(fn, x)` | `ctx.run(task, x)` without `await` |
|
|
4634
4966
|
| `spawn(fn, x)` | `ctx.emit(event, x)` or `bus.emit(event, x)` |
|
|
4635
4967
|
| `put(action)` | `ctx.emit(action)` |
|
|
4636
|
-
| `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md) |
|
|
4968
|
+
| `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md), where `event` is a name or an action creator |
|
|
4637
4969
|
| `select` | a [context field](context.md), such as `store` |
|
|
4638
4970
|
| `all`, `race` | [`all`, `race`](effects.md) |
|
|
4639
4971
|
| `retry`, `delay` | [`retry`, `delay`](effects.md) |
|
|
@@ -4681,6 +5013,8 @@ bus.on(
|
|
|
4681
5013
|
|
|
4682
5014
|
Tasks are plain `async` functions. `ctx.signal` replaces `cancel`: pass it to `fetch` or to your client, and the request stops.
|
|
4683
5015
|
|
|
5016
|
+
To see the counter, `takeLatest` with `delay`, a cancel and a failing call running, open the [counter example](../examples/counter): it has a page without a store and a page with Redux Toolkit.
|
|
5017
|
+
|
|
4684
5018
|
## Start a task without waiting
|
|
4685
5019
|
|
|
4686
5020
|
`fork` and `spawn` differ in what happens when the caller is aborted. Pick by that.
|
|
@@ -5269,9 +5603,9 @@ bus.on("watch", async (_, ctx) => {
|
|
|
5269
5603
|
|
|
5270
5604
|
await race(ctx, {
|
|
5271
5605
|
// poll: a check every 5 s; waitForFn: the next pause starts after the check ends
|
|
5272
|
-
polling: (
|
|
5606
|
+
polling: (child) => poll(child, checkStatus, 5000, { waitForFn: true }),
|
|
5273
5607
|
// take: resolves at the next "leave", and race stops the polling
|
|
5274
|
-
leave: (
|
|
5608
|
+
leave: (child) => take(child, "leave"),
|
|
5275
5609
|
});
|
|
5276
5610
|
});
|
|
5277
5611
|
|
package/llms.txt
CHANGED
|
@@ -9,7 +9,7 @@ Rules that generated code often gets wrong:
|
|
|
9
9
|
- A handler gets the payload and a task context: `(payload, ctx)`. The context has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`.
|
|
10
10
|
- Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
|
|
11
11
|
- Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
|
|
12
|
-
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
12
|
+
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/svelte` (`sveltePlugin`, adds `bus.eventStore`; needs Svelte), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
13
13
|
- `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
|
|
14
14
|
|
|
15
15
|
## Start here
|
|
@@ -34,9 +34,11 @@ Rules that generated code often gets wrong:
|
|
|
34
34
|
- [Actions, commands, jobs and modules](docs/actions.md): `createAction`, `createCommand`, `createJob`, `createModule`
|
|
35
35
|
- [Connect Redux, Zustand, MobX, or Pinia](docs/stores.md): the Redux middleware and callbacks of other stores
|
|
36
36
|
- [Use nanoeffects with React Query and SWR](docs/query-libraries.md): call a command from `queryFn` or the SWR fetcher; `signalPlugin` passes the abort of the library to the task
|
|
37
|
+
- [Stop work on a server when the client leaves](docs/server.md): one `AbortController` for each request or socket, `signalPlugin`, a clean shutdown, and a queue worker; Fastify examples
|
|
37
38
|
- [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `useEventState` shows an event as state, `usePolicy` keeps a debounced handler between renders
|
|
38
|
-
- [Build a chat with
|
|
39
|
+
- [Build a chat with entities, features, modules and a small server](docs/chat-example.md): rooms, a history under `latest`, a send under `queue` and `retry`, a search with `allSettled` and `timeout`, new messages polled with `poll`
|
|
39
40
|
- [Use nanoeffects in a Vue app](docs/vue.md): `vuePlugin` adds `watchEmitter` (a value of a component to the bus) and `useEventRef` (an event as a ref)
|
|
41
|
+
- [Use nanoeffects in a Svelte app](docs/svelte.md): `sveltePlugin` adds `eventStore` (an event as a Svelte store)
|
|
40
42
|
- [Context fields](docs/context.md): put a store or client on every task
|
|
41
43
|
- [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
|
|
42
44
|
- [Use the bus from a class](docs/oop.md): `Emitter`, `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
|