nanoeffects 0.20.0 → 0.22.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/docs/README.md CHANGED
@@ -19,6 +19,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
19
19
  | Already on Redux / Zustand / MobX / Pinia | [Stores](stores.md) |
20
20
  | A React form with async checks | [Build a sign-up form in React](react.md) |
21
21
  | A Vue app | [Use nanoeffects in a Vue app](vue.md) |
22
+ | A Svelte app | [Use nanoeffects in a Svelte app](svelte.md) |
22
23
  | Old responses overwrite new ones | [Cancel stale requests](cancel-stale-requests.md) |
23
24
  | Debounced input that must also cancel the old request | [Debounce and abort](debounce-and-abort.md) |
24
25
  | A second click sends a second request | [Prevent a double submit](prevent-double-submit.md) |
@@ -34,6 +35,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
34
35
  - [Actions, commands, jobs and modules](actions.md)
35
36
  - [Sign-up form in React](react.md) — one form: races, double submit, retry
36
37
  - [nanoeffects in a Vue app](vue.md) — `watchEmitter` and `useEventRef`
38
+ - [nanoeffects in a Svelte app](svelte.md) — `eventStore`
37
39
  - [Context fields](context.md) — store or client on every task
38
40
  - [Use the bus from a class](oop.md) — `extendBus`, `@WithBus`, `@On`, and `dispose()` for the subscriptions of an instance
39
41
  - [TypeScript in many files](typescript.md) — keep the types of the bus in commands and modules
@@ -44,7 +44,7 @@ Each domain is a folder. The components only emit events and read values; they n
44
44
  | `src/domains/incoming/` | New messages, asked for at an interval. |
45
45
  | `src/ui/` | The components. |
46
46
 
47
- A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store.
47
+ A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any domain may listen: `rooms/selected` is heard by three. 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.
48
48
 
49
49
  ## Load a history that must not race
50
50
 
@@ -173,19 +173,24 @@ export const runSearch = debounce(
173
173
  `take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
174
174
 
175
175
  ```js
176
+ import { createCommand } from "nanoeffects/actions";
176
177
  import { take } from "nanoeffects/effects";
177
178
 
179
+ // createCommand — an order for the messages domain, with the handler that does it
180
+ const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
181
+
178
182
  // the room that is open, kept from the rooms/selected event
179
183
  let selectedRoom = "general";
180
184
 
181
- export const openResult = async (hit, ctx) => {
185
+ export const goToResult = async (hit, ctx) => {
182
186
  if (hit.roomId !== selectedRoom) {
183
187
  // take — wait for the next event of that name; started before the emit so nothing is missed
184
188
  const loaded = take(ctx, "messages/loadHistory/success");
185
189
  ctx.emit("rooms/selected", hit.roomId);
186
190
  await loaded;
187
191
  }
188
- ctx.emit("messages/marked", hit.id);
192
+ // ctx.emit with a command — the messages domain marks the message
193
+ ctx.emit(markMessage(hit.id));
189
194
  };
190
195
  ```
191
196
 
package/docs/effects.md CHANGED
@@ -244,18 +244,24 @@ bus.on("watch", (_, ctx) => {
244
244
  - Under `latest`, subscriptions of a superseded run end with that run.
245
245
  - `ctx.off(event)` clears every handler of the event on the bus, not only the task’s.
246
246
 
247
- `take(ctx, event, options?)` waits for the next `event` and resolves with its payload:
247
+ `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`:
248
248
 
249
249
  ```js
250
250
  import { createBus } from "nanoeffects";
251
+ import { createAction } from "nanoeffects/actions";
251
252
  import { take, timeout } from "nanoeffects/effects";
252
253
 
253
254
  const bus = createBus();
255
+ const stopRequested = createAction("stop"); // an event with a name; calling it builds the action
254
256
 
255
257
  bus.on("login", async (user, ctx) => {
256
258
  const reason = await take(ctx, "logout"); // resolves on the next logout
257
259
  cleanup(user, reason);
258
260
 
261
+ // an action creator works the same, and its payload type is the type of the result
262
+ const stopped = await take(ctx, stopRequested);
263
+ console.log(stopped);
264
+
259
265
  // wait for a matching event, but not longer than 5 s
260
266
  const id = await timeout(ctx, 5000, ({ signal }) =>
261
267
  take(ctx, "saved", { signal, predicate: (savedId) => savedId === draft.id }),
@@ -264,6 +270,7 @@ bus.on("login", async (user, ctx) => {
264
270
  ```
265
271
 
266
272
  - Sees only events emitted after the call, takes one, and unsubscribes.
273
+ - 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.
267
274
  - `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
268
275
  - `options.signal` stops the wait (how a `race` or `timeout` starter cancels it).
269
276
  - On abort of the task or of `options.signal`, unsubscribes and rejects with the abort reason.
@@ -14,7 +14,7 @@ Read this page if you know redux-saga and want to find the same job in nanoeffec
14
14
  | `fork(fn, x)` | `ctx.run(task, x)` without `await` |
15
15
  | `spawn(fn, x)` | `ctx.emit(event, x)` or `bus.emit(event, x)` |
16
16
  | `put(action)` | `ctx.emit(action)` |
17
- | `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md) |
17
+ | `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md), where `event` is a name or an action creator |
18
18
  | `select` | a [context field](context.md), such as `store` |
19
19
  | `all`, `race` | [`all`, `race`](effects.md) |
20
20
  | `retry`, `delay` | [`retry`, `delay`](effects.md) |
@@ -62,6 +62,8 @@ bus.on(
62
62
 
63
63
  Tasks are plain `async` functions. `ctx.signal` replaces `cancel`: pass it to `fetch` or to your client, and the request stops.
64
64
 
65
+ 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.
66
+
65
67
  ## Start a task without waiting
66
68
 
67
69
  `fork` and `spawn` differ in what happens when the caller is aborted. Pick by that.
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/policies.md CHANGED
@@ -36,6 +36,33 @@ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-s
36
36
  | `throttle(ms, fn, options?)` | at most one run per `ms` |
37
37
  | `queue(fn, options?)` | one after another, in order |
38
38
 
39
+ ## Call a policy without its function
40
+
41
+ **Problem.** Some code builds the function for you and only asks which policy to use, as [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) does. If you must pass your own function there, it is easy to pass the wrong one.
42
+
43
+ **When.** You pass a policy to `usePolicy`. With a bus, `withPolicy` or your own code, call the policy with its function as above.
44
+
45
+ Leave the function out and the policy returns a wrapper. The wrapper takes a function and gives back the same policy as the direct call:
46
+
47
+ ```js
48
+ import { debounce } from "nanoeffects/policies";
49
+
50
+ const runSearch = async (query) => {
51
+ await api.search(query);
52
+ };
53
+
54
+ // debounce(300) — no function yet: returns a wrapper
55
+ const wrap = debounce(300);
56
+
57
+ // the wrapper takes the function; same as debounce(300, runSearch)
58
+ const search = wrap(runSearch);
59
+ ```
60
+
61
+ - It works for `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` and `queue()`, with the options as the last argument: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
62
+ - Each time you apply a wrapper to a function, you get a new policy with its own state.
63
+ - To combine policies, nest them as before: `throttle(100, latest(fn))`.
64
+ - With `key`, annotate the payload of the key function in TypeScript: `latest({ key: (user: User) => user.id })`. Without the annotation the wrapper cannot know the payload type.
65
+
39
66
  ## Configure debounce and throttle
40
67
 
41
68
  `debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
@@ -122,3 +122,5 @@ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the c
122
122
  - **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it. Commands that fetch are async, so this only matters for a sync handler that sets up a callback.
123
123
  - **Give each request its own signal for an event.** For a command or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
124
124
  - **The signal reaches the tasks that this `emit` starts**, with the listeners of the command's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
125
+
126
+ A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
package/docs/react.md CHANGED
@@ -248,8 +248,8 @@ export const Search = ({ wait }) => {
248
248
  const found = await api.search(query, { signal });
249
249
  setItems(found);
250
250
  },
251
- // the second argument builds the policy once; task runs the callback above
252
- (task) => debounce(wait, task),
251
+ // debounce(wait) — the second argument builds the policy once around the callback above
252
+ debounce(wait),
253
253
  // deps — list what the second argument reads; a new wait makes a new policy
254
254
  [wait],
255
255
  );
@@ -265,7 +265,9 @@ export const Search = ({ wait }) => {
265
265
 
266
266
  - `onChange` keeps its identity while `wait` stays the same, so it is safe in `useEffect` and in props.
267
267
  - A pending call is cancelled on unmount and when `deps` change.
268
- - Do not write `usePolicy(debounce(300, callback))`. The policy would be made on every render or keep a stale callback.
268
+ - Write the policy without its callback: `debounce(300)`, not `debounce(300, callback)`. `usePolicy` puts the callback in itself, always the one of the latest render.
269
+ - To combine policies, write a function of the task: `(task) => throttle(100, latest(task))`. Here `task` is the callback that `usePolicy` keeps current; pass it, not your own function.
270
+ - `deps` stay your job: `debounce(wait)` is made again on every render, but only the first one (and the one after `deps` change) is used.
269
271
  - Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
270
272
 
271
273
  ## Where to go next
package/docs/starter.md CHANGED
@@ -48,6 +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 `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
51
52
 
52
53
  ```bash
53
54
  npm create nanoeffects@latest my-app -- --template react
@@ -71,6 +72,16 @@ npm create nanoeffects@latest my-app -- --template vue-ts
71
72
 
72
73
  `npm run build` type-checks the `.vue` files with `vue-tsc`, which needs TypeScript 5: the template depends on it.
73
74
 
75
+ ### Start with Svelte
76
+
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
+
79
+ ```bash
80
+ npm create nanoeffects@latest my-app -- --template svelte-ts
81
+ ```
82
+
83
+ `npm run build` type-checks the `.svelte` files with `svelte-check`, which needs TypeScript 5: the template depends on it.
84
+
74
85
  ### Start with domains
75
86
 
76
87
  `domains-ts` is not the search box of the other templates grown in TypeScript: it shows where logic goes when an app has several features. Each domain is a folder under `src/domains/`. It has the events of the domain, the work that handles them, and a module that mounts the work. The React components only emit events and read values.
@@ -93,7 +104,7 @@ The commands, jobs and modules themselves are explained in [Actions, commands, j
93
104
 
94
105
  ## Read the files
95
106
 
96
- Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components).
107
+ Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components, `.svelte` for Svelte components).
97
108
 
98
109
  | File | Job |
99
110
  | ---- | --- |
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.
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,6 +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 `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
556
557
 
557
558
  ```bash
558
559
  npm create nanoeffects@latest my-app -- --template react
@@ -576,6 +577,16 @@ npm create nanoeffects@latest my-app -- --template vue-ts
576
577
 
577
578
  `npm run build` type-checks the `.vue` files with `vue-tsc`, which needs TypeScript 5: the template depends on it.
578
579
 
580
+ ### Start with Svelte
581
+
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
+
584
+ ```bash
585
+ npm create nanoeffects@latest my-app -- --template svelte-ts
586
+ ```
587
+
588
+ `npm run build` type-checks the `.svelte` files with `svelte-check`, which needs TypeScript 5: the template depends on it.
589
+
579
590
  ### Start with domains
580
591
 
581
592
  `domains-ts` is not the search box of the other templates grown in TypeScript: it shows where logic goes when an app has several features. Each domain is a folder under `src/domains/`. It has the events of the domain, the work that handles them, and a module that mounts the work. The React components only emit events and read values.
@@ -598,7 +609,7 @@ The commands, jobs and modules themselves are explained in [Actions, commands, j
598
609
 
599
610
  ## Read the files
600
611
 
601
- Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components).
612
+ Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components, `.svelte` for Svelte components).
602
613
 
603
614
  | File | Job |
604
615
  | ---- | --- |
@@ -681,6 +692,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
681
692
  | `nanoeffects/redux` | `createEffectsMiddleware` |
682
693
  | `nanoeffects/observable` | `createObserve`, `Subject` |
683
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) |
684
696
  | `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) |
685
697
  | `nanoeffects/dev` | `devPlugin` (no-op in production) |
686
698
  | `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
@@ -1069,6 +1081,33 @@ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-s
1069
1081
  | `throttle(ms, fn, options?)` | at most one run per `ms` |
1070
1082
  | `queue(fn, options?)` | one after another, in order |
1071
1083
 
1084
+ ## Call a policy without its function
1085
+
1086
+ **Problem.** Some code builds the function for you and only asks which policy to use, as [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) does. If you must pass your own function there, it is easy to pass the wrong one.
1087
+
1088
+ **When.** You pass a policy to `usePolicy`. With a bus, `withPolicy` or your own code, call the policy with its function as above.
1089
+
1090
+ Leave the function out and the policy returns a wrapper. The wrapper takes a function and gives back the same policy as the direct call:
1091
+
1092
+ ```js
1093
+ import { debounce } from "nanoeffects/policies";
1094
+
1095
+ const runSearch = async (query) => {
1096
+ await api.search(query);
1097
+ };
1098
+
1099
+ // debounce(300) — no function yet: returns a wrapper
1100
+ const wrap = debounce(300);
1101
+
1102
+ // the wrapper takes the function; same as debounce(300, runSearch)
1103
+ const search = wrap(runSearch);
1104
+ ```
1105
+
1106
+ - It works for `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` and `queue()`, with the options as the last argument: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
1107
+ - Each time you apply a wrapper to a function, you get a new policy with its own state.
1108
+ - To combine policies, nest them as before: `throttle(100, latest(fn))`.
1109
+ - With `key`, annotate the payload of the key function in TypeScript: `latest({ key: (user: User) => user.id })`. Without the annotation the wrapper cannot know the payload type.
1110
+
1072
1111
  ## Configure debounce and throttle
1073
1112
 
1074
1113
  `debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
@@ -1789,18 +1828,24 @@ bus.on("watch", (_, ctx) => {
1789
1828
  - Under `latest`, subscriptions of a superseded run end with that run.
1790
1829
  - `ctx.off(event)` clears every handler of the event on the bus, not only the task’s.
1791
1830
 
1792
- `take(ctx, event, options?)` waits for the next `event` and resolves with its payload:
1831
+ `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`:
1793
1832
 
1794
1833
  ```js
1795
1834
  import { createBus } from "nanoeffects";
1835
+ import { createAction } from "nanoeffects/actions";
1796
1836
  import { take, timeout } from "nanoeffects/effects";
1797
1837
 
1798
1838
  const bus = createBus();
1839
+ const stopRequested = createAction("stop"); // an event with a name; calling it builds the action
1799
1840
 
1800
1841
  bus.on("login", async (user, ctx) => {
1801
1842
  const reason = await take(ctx, "logout"); // resolves on the next logout
1802
1843
  cleanup(user, reason);
1803
1844
 
1845
+ // an action creator works the same, and its payload type is the type of the result
1846
+ const stopped = await take(ctx, stopRequested);
1847
+ console.log(stopped);
1848
+
1804
1849
  // wait for a matching event, but not longer than 5 s
1805
1850
  const id = await timeout(ctx, 5000, ({ signal }) =>
1806
1851
  take(ctx, "saved", { signal, predicate: (savedId) => savedId === draft.id }),
@@ -1809,6 +1854,7 @@ bus.on("login", async (user, ctx) => {
1809
1854
  ```
1810
1855
 
1811
1856
  - Sees only events emitted after the call, takes one, and unsubscribes.
1857
+ - 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.
1812
1858
  - `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
1813
1859
  - `options.signal` stops the wait (how a `race` or `timeout` starter cancels it).
1814
1860
  - On abort of the task or of `options.signal`, unsubscribes and rejects with the abort reason.
@@ -2249,7 +2295,7 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
2249
2295
 
2250
2296
  `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.
2251
2297
 
2252
- 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).
2298
+ 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.
2253
2299
 
2254
2300
  ```js
2255
2301
  // bus.js
@@ -3022,6 +3068,8 @@ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the c
3022
3068
  - **Give each request its own signal for an event.** For a command or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
3023
3069
  - **The signal reaches the tasks that this `emit` starts**, with the listeners of the command's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
3024
3070
 
3071
+ A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
3072
+
3025
3073
  ---
3026
3074
  Source: docs/react.md
3027
3075
 
@@ -3275,8 +3323,8 @@ export const Search = ({ wait }) => {
3275
3323
  const found = await api.search(query, { signal });
3276
3324
  setItems(found);
3277
3325
  },
3278
- // the second argument builds the policy once; task runs the callback above
3279
- (task) => debounce(wait, task),
3326
+ // debounce(wait) — the second argument builds the policy once around the callback above
3327
+ debounce(wait),
3280
3328
  // deps — list what the second argument reads; a new wait makes a new policy
3281
3329
  [wait],
3282
3330
  );
@@ -3292,7 +3340,9 @@ export const Search = ({ wait }) => {
3292
3340
 
3293
3341
  - `onChange` keeps its identity while `wait` stays the same, so it is safe in `useEffect` and in props.
3294
3342
  - A pending call is cancelled on unmount and when `deps` change.
3295
- - Do not write `usePolicy(debounce(300, callback))`. The policy would be made on every render or keep a stale callback.
3343
+ - Write the policy without its callback: `debounce(300)`, not `debounce(300, callback)`. `usePolicy` puts the callback in itself, always the one of the latest render.
3344
+ - To combine policies, write a function of the task: `(task) => throttle(100, latest(task))`. Here `task` is the callback that `usePolicy` keeps current; pass it, not your own function.
3345
+ - `deps` stay your job: `debounce(wait)` is made again on every render, but only the first one (and the one after `deps` change) is used.
3296
3346
  - Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
3297
3347
 
3298
3348
  ## Where to go next
@@ -3352,7 +3402,7 @@ Each domain is a folder. The components only emit events and read values; they n
3352
3402
  | `src/domains/incoming/` | New messages, asked for at an interval. |
3353
3403
  | `src/ui/` | The components. |
3354
3404
 
3355
- A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store.
3405
+ A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any domain may listen: `rooms/selected` is heard by three. 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.
3356
3406
 
3357
3407
  ## Load a history that must not race
3358
3408
 
@@ -3481,19 +3531,24 @@ export const runSearch = debounce(
3481
3531
  `take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
3482
3532
 
3483
3533
  ```js
3534
+ import { createCommand } from "nanoeffects/actions";
3484
3535
  import { take } from "nanoeffects/effects";
3485
3536
 
3537
+ // createCommand — an order for the messages domain, with the handler that does it
3538
+ const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
3539
+
3486
3540
  // the room that is open, kept from the rooms/selected event
3487
3541
  let selectedRoom = "general";
3488
3542
 
3489
- export const openResult = async (hit, ctx) => {
3543
+ export const goToResult = async (hit, ctx) => {
3490
3544
  if (hit.roomId !== selectedRoom) {
3491
3545
  // take — wait for the next event of that name; started before the emit so nothing is missed
3492
3546
  const loaded = take(ctx, "messages/loadHistory/success");
3493
3547
  ctx.emit("rooms/selected", hit.roomId);
3494
3548
  await loaded;
3495
3549
  }
3496
- ctx.emit("messages/marked", hit.id);
3550
+ // ctx.emit with a command — the messages domain marks the message
3551
+ ctx.emit(markMessage(hit.id));
3497
3552
  };
3498
3553
  ```
3499
3554
 
@@ -3648,6 +3703,91 @@ In TypeScript the type of the `ref` comes from the event: with `createAction<str
3648
3703
  - 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.
3649
3704
  - Keep values in Pinia: [Connect Redux, Zustand, MobX, or Pinia](stores.md).
3650
3705
 
3706
+ ---
3707
+ Source: docs/svelte.md
3708
+
3709
+ # Use nanoeffects in a Svelte app
3710
+
3711
+ 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).
3712
+
3713
+ You need Svelte 5 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
3714
+
3715
+ ## Add the plugin to the bus
3716
+
3717
+ **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.
3718
+
3719
+ **When.** A component shows the payload of an event, such as the result of a search.
3720
+
3721
+ `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:
3722
+
3723
+ ```js
3724
+ // bus.js
3725
+ import { createBus } from "nanoeffects";
3726
+ import { sveltePlugin } from "nanoeffects/svelte";
3727
+
3728
+ // createBus — the event bus, created once outside Svelte
3729
+ // sveltePlugin — adds eventStore to the bus
3730
+ export const bus = createBus().use(sveltePlugin());
3731
+ export const { eventStore } = bus; // a free function: it does not read `this`
3732
+ ```
3733
+
3734
+ ## Show the result of an event
3735
+
3736
+ <!-- docs-check: continue -->
3737
+
3738
+ `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.
3739
+
3740
+ ```js
3741
+ // search.js
3742
+ import { createAction } from "nanoeffects/actions";
3743
+
3744
+ // createAction — an event with a name; calling it builds { type, payload }
3745
+ export const searchLoaded = createAction("search/loaded");
3746
+ ```
3747
+
3748
+ <!-- docs-check: skip - a .svelte file -->
3749
+ ```svelte
3750
+ <script>
3751
+ import { eventStore } from "./bus";
3752
+ import { searchLoaded } from "./search";
3753
+
3754
+ // eventStore — like a store of [], but searchLoaded sets the value
3755
+ const found = eventStore(searchLoaded, []);
3756
+ </script>
3757
+
3758
+ <ul>
3759
+ {#each $found as title (title)}
3760
+ <li>{title}</li>
3761
+ {/each}
3762
+ </ul>
3763
+ ```
3764
+
3765
+ 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`.
3766
+
3767
+ 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.
3768
+
3769
+ ### It is not a store of the app
3770
+
3771
+ 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:
3772
+
3773
+ ```js
3774
+ // found-store.js
3775
+ import { writable } from "svelte/store";
3776
+ import { bus } from "./bus";
3777
+ import { searchLoaded } from "./search";
3778
+
3779
+ // writable — a Svelte store that you keep for the whole app
3780
+ export const foundTitles = writable([]);
3781
+
3782
+ // bus.on — copy every searchLoaded into the store, whether or not a component is open
3783
+ bus.on(searchLoaded, (titles) => foundTitles.set(titles));
3784
+ ```
3785
+
3786
+ ## Where to go next
3787
+
3788
+ - The same job for React: [Build a sign-up form in React](react.md); for Vue: [Use nanoeffects in a Vue app](vue.md).
3789
+ - 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.
3790
+
3651
3791
  ---
3652
3792
  Source: docs/context.md
3653
3793
 
@@ -4586,7 +4726,7 @@ Read this page if you know redux-saga and want to find the same job in nanoeffec
4586
4726
  | `fork(fn, x)` | `ctx.run(task, x)` without `await` |
4587
4727
  | `spawn(fn, x)` | `ctx.emit(event, x)` or `bus.emit(event, x)` |
4588
4728
  | `put(action)` | `ctx.emit(action)` |
4589
- | `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md) |
4729
+ | `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md), where `event` is a name or an action creator |
4590
4730
  | `select` | a [context field](context.md), such as `store` |
4591
4731
  | `all`, `race` | [`all`, `race`](effects.md) |
4592
4732
  | `retry`, `delay` | [`retry`, `delay`](effects.md) |
@@ -4634,6 +4774,8 @@ bus.on(
4634
4774
 
4635
4775
  Tasks are plain `async` functions. `ctx.signal` replaces `cancel`: pass it to `fetch` or to your client, and the request stops.
4636
4776
 
4777
+ 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.
4778
+
4637
4779
  ## Start a task without waiting
4638
4780
 
4639
4781
  `fork` and `spawn` differ in what happens when the caller is aborted. Pick by that.
package/llms.txt CHANGED
@@ -9,13 +9,13 @@ 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
16
16
 
17
17
  - [Guide](docs/guide.md): bus, handler context, policies, effects, commands and modules, step by step
18
- - [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla`, `react`, `vue-ts` and others, in JavaScript or TypeScript) with a bus, `debounce` and `latest`
18
+ - [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla`, `react`, `vue-ts`, `svelte-ts` and others, in JavaScript or TypeScript) with a bus, `debounce` and `latest`
19
19
  - [Why nanoeffects exists](docs/motivation.md): no dependencies and no global state for SDKs and Module Federation, and why not Redux, Effector or RxJS
20
20
  - [Package entries](docs/package.md): which import holds what
21
21
  - [Plugins](docs/plugins.md): every plugin of a bus, what `bus.use` returns, and the order to use them in
@@ -37,6 +37,7 @@ Rules that generated code often gets wrong:
37
37
  - [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
38
  - [Build a chat with domains, 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
39
  - [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)
40
+ - [Use nanoeffects in a Svelte app](docs/svelte.md): `sveltePlugin` adds `eventStore` (an event as a Svelte store)
40
41
  - [Context fields](docs/context.md): put a store or client on every task
41
42
  - [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
42
43
  - [Use the bus from a class](docs/oop.md): `Emitter`, `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance