nanoeffects 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +62 -61
  2. package/dist/{action-BM-UdmJr.d.ts → action-rqZEnRIp.d.ts} +12 -11
  3. package/dist/actions.d.ts +47 -185
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-CRnlwmQ4.js +1 -0
  6. package/dist/context-DlA80GOA.js +1 -0
  7. package/dist/dev.d.ts +46 -0
  8. package/dist/dev.js +1 -0
  9. package/dist/effects.d.ts +61 -21
  10. package/dist/effects.js +1 -1
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.js +1 -1
  13. package/dist/module-BSCdR9D1.d.ts +167 -0
  14. package/dist/notrace/{action-BM-UdmJr.d.ts → action-rqZEnRIp.d.ts} +12 -11
  15. package/dist/notrace/actions.d.ts +71 -45
  16. package/dist/notrace/actions.js +1 -1
  17. package/dist/notrace/bus-Dx0l9X6h.js +1 -0
  18. package/dist/notrace/context-yAWj0BFZ.js +1 -0
  19. package/dist/notrace/{delay-Cko_Q9hl.js → delay-Cgvn16oY.js} +1 -1
  20. package/dist/notrace/effects.d.ts +61 -21
  21. package/dist/notrace/effects.js +1 -1
  22. package/dist/notrace/index.d.ts +1 -2
  23. package/dist/notrace/index.js +1 -1
  24. package/dist/notrace/policies.d.ts +1 -1
  25. package/dist/notrace/policies.js +1 -1
  26. package/dist/notrace/redux.d.ts +2 -28
  27. package/dist/notrace/redux.js +1 -1
  28. package/dist/observable.d.ts +1 -1
  29. package/dist/policies.d.ts +1 -1
  30. package/dist/policies.js +1 -1
  31. package/dist/production/dev.js +1 -0
  32. package/dist/redux.d.ts +2 -28
  33. package/dist/redux.js +1 -1
  34. package/dist/trace.d.ts +22 -4
  35. package/dist/trace.js +1 -1
  36. package/docs/README.md +4 -1
  37. package/docs/actions.md +21 -11
  38. package/docs/context.md +0 -1
  39. package/docs/effects.md +49 -31
  40. package/docs/errors.md +4 -4
  41. package/docs/guide.md +229 -0
  42. package/docs/hmr.md +20 -58
  43. package/docs/policies.md +4 -4
  44. package/docs/stores.md +0 -2
  45. package/docs/tracing.md +21 -8
  46. package/package.json +25 -5
  47. package/dist/bus-RUiw8f9p.js +0 -1
  48. package/dist/context-BXQ6Einm.js +0 -1
  49. package/dist/index-CaxrMD1A.d.ts +0 -1
  50. package/dist/notrace/bus-CFxgpsRL.js +0 -1
  51. package/dist/notrace/context-DGPIFXxI.js +0 -1
  52. package/dist/notrace/index-CaxrMD1A.d.ts +0 -1
package/README.md CHANGED
@@ -1,36 +1,53 @@
1
1
  # nanoeffects
2
2
 
3
- A typed event bus where every handler is a **task**: it gets a context with an `AbortSignal`, and the policies that decide what happens when calls overlap (`latest`, `debounce`, `throttle`, `queue`, ...) are plain functions around the handler.
3
+ [![npm](https://img.shields.io/npm/v/nanoeffects)](https://www.npmjs.com/package/nanoeffects)
4
+ [![CI](https://github.com/nanoeffects/nanoeffects/actions/workflows/ci.yml/badge.svg)](https://github.com/nanoeffects/nanoeffects/actions/workflows/ci.yml)
5
+ [![size](https://img.shields.io/bundlephobia/minzip/nanoeffects)](https://bundlephobia.com/package/nanoeffects)
6
+ [![license](https://img.shields.io/npm/l/nanoeffects)](LICENSE)
7
+
8
+ A small typed event bus. Handlers are tasks with an `AbortSignal`, and policies (`latest`, `debounce`, `throttle`, `queue`) and effects (`retry`, `timeout`) are plain functions that go straight into `on`.
4
9
 
5
10
  ```ts
6
11
  import { createBus } from "nanoeffects";
7
- import { latest } from "nanoeffects/policies";
8
- import { retry } from "nanoeffects/effects";
9
12
 
10
- type Events = { search: string; results: Item[] };
13
+ type Events = { greet: string };
11
14
 
12
15
  const bus = createBus<Events>();
13
16
 
14
- // a new search aborts the previous one; the request is retried and cancelled with the task
15
- bus.on(
16
- "search",
17
- latest(async (query, ctx) => {
18
- const items = await retry(ctx, () => api.search(query, { signal: ctx.signal }));
19
- ctx.emit("results", items); // does nothing if this task was aborted meanwhile
20
- }),
21
- );
17
+ const unbind = bus.on("greet", (name) => console.log(`hello ${name}`));
22
18
 
23
- bus.emit("search", "nano");
19
+ bus.emit("greet", "Ada"); // hello Ada
20
+ unbind(); // `on` returns the unsubscribe function, as in nanoevents
24
21
  ```
25
22
 
26
- What it is for: the glue between events and async work in an app, where the usual answer is a pile of `AbortController`s, `debounce` from lodash, `p-queue` and a hand-written retry. Here they share one model of cancellation and one trace.
23
+ `emit` takes the event as a string, as an object `{ type, payload }` (a Redux action fits as it is), or as a function with a `type`, a command that runs and returns its result:
27
24
 
28
- - **Tiny core.** `createBus` is about 1.2 kB gzip. Everything else is a separate entry and is bundled only when imported.
29
- - **Cancellation is the base of everything.** Aborting a task aborts its children, ends its `ctx.on` subscriptions and drops its result.
30
- - **Policies and effects are functions**: `latest`, `leading`, `debounce`, `throttle`, `queue`, `retry`, `timeout`, `poll`, `all`, `allSettled`, `race`, `take`.
31
- - **Typed commands, actions, jobs and modules** to describe what the app does apart from the code that does it.
32
- - **Tracing by design**: causal links between tasks, sampling, Redux DevTools, and a build that cuts all of it out.
33
- - **Checked against the libraries it replaces**: `throttle` and `debounce` are compared with lodash on random call sequences, and known issues of lodash, RxJS, p-queue, p-retry, bottleneck and effector are regression tests.
25
+ ```ts
26
+ bus.emit("greet", "Ada");
27
+ bus.emit({ type: "greet", payload: "Ada" });
28
+ bus.emit(double, 21); // a command: runs, returns 42
29
+ ```
30
+
31
+ Then the same bus with a policy, no other change:
32
+
33
+ ```ts
34
+ import { latest } from "nanoeffects/policies";
35
+
36
+ // a newer search aborts the older one
37
+ bus.on(
38
+ "search",
39
+ latest(async (query, ctx) => {
40
+ const items = await api.search(query, { signal: ctx.signal });
41
+ ctx.emit("results", items);
42
+ }),
43
+ );
44
+ ```
45
+
46
+ - **Tiny.** `createBus` is about 1.2 kB gzip; every other entry is bundled only when imported.
47
+ - **Cancellation first.** Aborting a task aborts its children and `ctx.on` subscriptions and drops its result.
48
+ - **One model** instead of `AbortController`, lodash `debounce`, `p-queue` and a hand-written retry.
49
+ - **Typed** commands, actions, jobs and modules; **traced** with Redux DevTools; the trace can be cut out of the build.
50
+ - **Checked** against lodash on random call sequences, and against known issues of RxJS, p-queue, p-retry and effector.
34
51
 
35
52
  ## Install
36
53
 
@@ -38,59 +55,43 @@ What it is for: the glue between events and async work in an app, where the usua
38
55
  npm install nanoeffects
39
56
  ```
40
57
 
41
- ESM only, no dependencies. Types are included.
58
+ ESM only, no dependencies, types included.
42
59
 
43
60
  ## Entries
44
61
 
45
- | Import | What is in it |
46
- | ----------------------- | -------------------------------------------------------------------------------------- |
47
- | `nanoeffects` | `createBus`, the types (`Bus`, `Context`, `WithContext`, ...) |
48
- | `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
49
- | `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `cancellable`, `onAbort` |
50
- | `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `registerModules` |
51
- | `nanoeffects/redux` | `createEffectsMiddleware`, `nanoeffectsMiddleware`: Redux actions to the bus, `ctx.store` in tasks |
52
- | `nanoeffects/observable` | `createObserve`: an event of the bus as an Observable (RxJS and others) |
53
- | `nanoeffects/trace` | `createTracer` and the loggers (Redux DevTools, console, performance) |
54
-
55
- ## Handlers and the task context
56
-
57
- `bus.on(event, handler)` calls `handler(payload, ctx)` for every `bus.emit(event, payload)`. Each call is a task with its own context:
62
+ | Import | What is in it |
63
+ | ------------------------ | ----------------------------------------------------------------------------- |
64
+ | `nanoeffects` | `createBus`, the types |
65
+ | `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
66
+ | `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race` |
67
+ | `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin` |
68
+ | `nanoeffects/redux` | `createEffectsMiddleware` |
69
+ | `nanoeffects/observable` | `createObserve`: an event as an Observable (RxJS) |
70
+ | `nanoeffects/dev` | `devPlugin`: warnings about listeners that pile up and messages about modules; a no-op in production |
71
+ | `nanoeffects/trace` | `createTracer` and the loggers |
58
72
 
59
- | `ctx` | Meaning |
60
- | ---------------- | ------------------------------------------------------------------------------ |
61
- | `signal`, `abort` | the abort signal of the task, and a way to abort it (and its children) |
62
- | `emit` | emit as this task; does nothing after the task is aborted |
63
- | `on`, `off` | subscribe as this task; the subscription ends when the task aborts |
64
- | `run(fn, ...)` | a child task: aborted with this one, its errors go to this one |
65
- | `type`, `payload` | the event the task works on |
73
+ ## The task context
66
74
 
67
- `emit` is a synchronous loop over the handlers, and `bus.on` returns the function that unsubscribes.
75
+ `bus.on(event, handler)` calls `handler(payload, ctx)`. Each call is a task:
68
76
 
69
- **The event map is optional.** Describe the events once, `type Events = { name: Payload }` (an event without data is `void`), and `createBus<Events>()` checks every `on` and `emit`. Or skip it: `createBus()` takes any string as an event and passes the payload as `unknown`. Actions and commands bring their own types, so they are typed on either kind of bus: `createAction("user/loaded")` needs no generics on a bus with an event map (the payload type comes from the map), and `createAction<User>("user/loaded")` gives it on a bus without one. On a bus with an event map, an action or a command that is emitted by its object must have its type in the map.
77
+ | `ctx` | Meaning |
78
+ | ----------------- | -------------------------------------------------------- |
79
+ | `signal`, `abort` | the signal of the task, and a way to abort it |
80
+ | `emit` | emit as this task; does nothing after an abort |
81
+ | `on`, `off` | subscribe; the subscription ends when the task aborts |
82
+ | `run(fn, ...)` | a child task, aborted with this one |
83
+ | `type`, `payload` | the event the task works on |
70
84
 
71
- The event map is optional. Describe the events once, `type Events = { name: Payload }` (an event without data is `void`), and `createBus<Events>()` checks every `on` and `emit`. Or skip it: `createBus()` takes any string as an event and passes the payload as `unknown`. Actions and commands bring their own types, so they are typed on either kind of bus.
85
+ The event map (`type Events = { name: Payload }`) is optional: `createBus()` takes any string event with an `unknown` payload. `createAction("user/loaded")` takes its payload type from the map, and `createAction<User>("user/loaded")` gives it when there is none.
72
86
 
73
- ## Where to go next
87
+ ## Docs
74
88
 
75
- - [Policies](docs/policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, and `key` for a state per key.
76
- - [Effects and cancellation](docs/effects.md): `retry`, `timeout`, `poll`, `take`, `all`, `race`, and `ctx.on`.
77
- - [Actions, commands, jobs and modules](docs/actions.md): describe what an app does apart from the code that does it.
78
- - [Your own fields in the context](docs/context.md): a store, a client or a logger in every task, with types.
79
- - [Errors and unheard events](docs/errors.md): `onError` and `onUnhandled`.
80
- - [Tracing](docs/tracing.md): causal traces, Redux DevTools, sampling, and a build without tracing.
81
- - [Redux, Zustand, MobX](docs/stores.md).
82
- - [Observable](docs/observable.md): events as streams for RxJS and others.
83
- - [Hot reload](docs/hmr.md): modules are replaced by name, so an edit does not pile up handlers.
84
- - [Semantics](docs/semantics.md): the rules that hold everywhere.
85
- - [`examples/`](examples): code that is type-checked together with the project.
89
+ [**Guide**](docs/guide.md) · [Policies](docs/policies.md) · [Effects](docs/effects.md) · [Actions, commands, modules](docs/actions.md) · [Context fields](docs/context.md) · [Errors](docs/errors.md) · [Tracing](docs/tracing.md) · [Redux, Zustand, MobX](docs/stores.md) · [Observable](docs/observable.md) · [Hot reload and dev diagnostics](docs/hmr.md) · [Semantics](docs/semantics.md) · [Examples](examples)
86
90
 
87
91
  ## Development
88
92
 
89
93
  ```bash
90
- pnpm verify # type check, lint, tests, build, integration tests, package checks, bundle size: what CI runs
91
- pnpm test # vitest
92
- pnpm test:integration # the real Redux Toolkit, Zustand, MobX, Jotai; Vite and webpack bundling the package
93
- pnpm check # biome with the readability plugins in lint/
94
+ pnpm verify # what CI runs: types, lint, tests, build, integration, package checks, size
94
95
  ```
95
96
 
96
- [`AGENTS.md`](AGENTS.md) (also read by Claude Code through `CLAUDE.md`) describes the structure, the decisions behind it and the rules of the code, for people and for AI assistants that work on the project.
97
+ [`AGENTS.md`](AGENTS.md) describes the structure and the rules of the code, for people and AI assistants.
@@ -175,19 +175,19 @@ interface Bus<E extends EventMap> {
175
175
  on: On<E>;
176
176
  off: Off<E>;
177
177
  emit: Emit<E>;
178
- /** `BusOptions.context`, `.trace` and `.commands`, for what makes tasks of its own, like `registerModules`. */
178
+ /**
179
+ * Calls `plugin` with the bus and returns what it returns: an api, a
180
+ * function that undoes it. A plugin is any function of the bus; the bus keeps
181
+ * no list of them. See `modulePlugin` in `nanoeffects/actions`.
182
+ */
183
+ use<R>(plugin: (bus: Bus<E>) => R): R;
184
+ /** `BusOptions.context` and `.trace`, and `commands`, for what makes tasks of its own, like `modulePlugin`. */
179
185
  readonly context?: ContextOption<E> | undefined;
180
- /** What runs the commands; `registerModules` sets it when the bus was made without. */
186
+ /** What runs the commands; `createCommands(bus)` and `modulePlugin` set it. Without one a command runs its own handler. */
181
187
  commands?: Commands | undefined;
182
188
  readonly trace?: Tracer | undefined;
183
189
  }
184
190
  type BusOptions<E extends EventMap> = {
185
- /**
186
- * Runs commands: make it with `createCommands` from `nanoeffects/actions`.
187
- * Without it an event that is a function runs as it is, with the payload and
188
- * the task context.
189
- */
190
- commands?: Commands;
191
191
  /** Tracing of every task phase; make it with `createTracer` from `nanoeffects/trace`. */
192
192
  trace?: Tracer;
193
193
  /**
@@ -214,9 +214,10 @@ type BusOptions<E extends EventMap> = {
214
214
  /**
215
215
  * Fields added to the context of every task of this bus, its child tasks
216
216
  * and the tasks of effects included: a store, a logger, a client. Their
217
- * types come from `WithContext` in the event map. The fields of the library win over
218
- * these when the names are the same. A function gets `{ type, payload }` of
219
- * the task and returns its fields, for fields that depend on the task.
217
+ * types come from `WithContext` in the event map. The fields of the library
218
+ * win over these when the names are the same. A function gets
219
+ * `{ type, payload }` of the task and returns its fields, for fields that
220
+ * depend on the task.
220
221
  */
221
222
  context?: ContextOption<E>;
222
223
  };
package/dist/actions.d.ts CHANGED
@@ -1,196 +1,58 @@
1
- import { L as UnsubscribeCallback, N as Task, c as Command, j as PayloadOf, k as EventMap, m as Emittable, n as FreeActionCreator, o as Bus, r as createAction, t as ActionCreator, u as Context } from "./action-BM-UdmJr.js";
2
- //#region src/actions/command.d.ts
3
- /**
4
- * Creates a command: a typed request for a result. `bus.emit(command, payload)`
5
- * runs its handler as a task, with a context, and returns what the handler
6
- * returns. Listeners of the command's type are told first, as for any event.
7
- *
8
- * The handler is optional. A command without one is only a contract, and the
9
- * bus gets its implementation from the `commands` registry (see
10
- * `createCommands` and `bindHandler`). A handler bound there wins over the one
11
- * the command was created with, so a test can give a bus its own implementation.
12
- *
13
- * A handler can be wrapped in a policy, e.g. `latest(fn)`. Calling the command
14
- * directly, `command(payload)`, runs its own handler outside of any bus.
15
- *
16
- * @example
17
- * ```ts
18
- * const getUser = createCommand("user/get", (id: string, { signal }) =>
19
- * fetchUser(id, { signal }),
20
- * );
21
- * const sendMail = createCommand<Mail, void>("mail/send"); // implemented elsewhere
22
- *
23
- * const commands = createCommands([bindHandler(sendMail, smtpSend)]);
24
- * const bus = createBus({ commands });
25
- * const user = await bus.emit(getUser, "1");
26
- *
27
- * commands.handle(getUser, async () => fakeUser); // in a test
28
- * ```
29
- */
30
- export declare const createCommand: <P = void, R = void, T extends string = string, E extends EventMap = EventMap>(type: T, handler?: Task<P, E, R>) => Command<P, R> & {
31
- readonly type: T;
32
- };
33
- /** A command (or its type) with the handler to bind to it; `bindHandler` makes one. */
34
- type Binding<E extends EventMap = EventMap> = readonly [Command<never, unknown> | string, Task<never, E, unknown>];
35
- /**
36
- * Pairs a command with the handler to bind to it, for `createCommands`. It
37
- * checks that the handler fits the command.
38
- *
39
- * @example
40
- * ```ts
41
- * createCommands([bindHandler(sendMail, smtpSend)]);
42
- * ```
43
- */
44
- export declare const bindHandler: <P, R, E extends EventMap>(command: Command<P, R>, fn: Task<P, E, R>) => Binding<E>;
45
- /**
46
- * The handlers bound to commands, for `createBus({ commands })`. Each bus gets
47
- * its own, so each test can have its own implementation. `handle` binds more
48
- * later and returns the function that unbinds.
49
- *
50
- * What the bus runs for a command: the handler bound last, else the one the
51
- * command was created with, else it throws `No handler for command "..."`.
52
- *
53
- * @example
54
- * ```ts
55
- * const commands = createCommands([bindHandler(sendMail, smtpSend)]);
56
- * const bus = createBus<Events>({ commands });
57
- *
58
- * const unbind = commands.handle(getUser, async () => fakeUser);
59
- * unbind(); // the command's own handler is back
60
- * ```
61
- */
62
- export declare const createCommands: <E extends EventMap = EventMap>(bindings?: readonly Binding<E>[]) => {
63
- run: (command: Command, payload: unknown, ctx: unknown) => unknown;
64
- handle: {
65
- <P, R>(command: Command<P, R>, fn: Task<P, E, R>): UnsubscribeCallback;
66
- (type: string, fn: Task<never, E, unknown>): UnsubscribeCallback;
67
- (bindings: readonly Binding<E>[]): UnsubscribeCallback;
68
- };
69
- };
70
- //#endregion
1
+ import { N as Task, c as Command, k as EventMap, n as FreeActionCreator, r as createAction, t as ActionCreator } from "./action-rqZEnRIp.js";
2
+ import { a as modulePlugin, c as createCommand, i as createModule, l as createCommands, n as ModuleApi, o as Binding, r as ModuleContext, s as bindHandler, t as Module } from "./module-BSCdR9D1.js";
71
3
  //#region src/actions/job.d.ts
72
- /**
73
- * Creates a command whose outcome is sent on: when the handler succeeds, its
74
- * result goes to `done`, and when it fails, the error goes to `fail`. Each is
75
- * anything `emit` takes as its first argument, and receives the result (or the
76
- * error) as the payload: an event type, an action creator, a command, or a
77
- * plain action. It is made for the case where nobody waits for the result and
78
- * the bus carries it on, like `createAsyncThunk` or an effector effect.
79
- *
80
- * `bus.emit(job, payload)` returns a promise of the result. When `fail` is set
81
- * the error counts as handled: it goes to `fail` and the promise resolves with
82
- * `undefined`; without `fail` the promise rejects. The outcome is routed by the
83
- * bus, also for a handler bound with `commands.handle`, and is skipped once the
84
- * task is aborted. Calling the job directly runs only its handler.
85
- *
86
- * @example
87
- * ```ts
88
- * const loaded = createAction<User>("user/loaded");
89
- * const failed = createAction<Error>("user/failed");
90
- *
91
- * const loadUser = createJob("user/load", fetchUser, { done: loaded, fail: failed });
92
- *
93
- * bus.on(loaded, (user) => render(user));
94
- * bus.emit(loadUser, "1"); // loaded(user) or failed(error) comes later
95
- * ```
96
- */
97
- export declare const createJob: <P = void, R = void, T extends string = string, E extends EventMap = EventMap>(type: T, handler?: Task<P, E, R>, outcomes?: {
98
- /** Gets the result when the handler succeeds. */
99
- done?: Emittable<E>;
100
- /** Gets the error when the handler fails. */
101
- fail?: Emittable<E>;
102
- }) => Command<P, Promise<Awaited<R> | undefined>> & {
103
- readonly type: T;
104
- };
105
- //#endregion
106
- //#region src/actions/module.d.ts
107
- /**
108
- * What a module's `setup` gets: the context of the module's task, so `on`,
109
- * `emit` and `run` work as in a handler, plus `handle` to bind commands, `use`
110
- * to mount sub-modules and `spawn` to start a background task. Everything the
111
- * module does is undone when it is unmounted.
112
- */
113
- type ModuleContext<E extends EventMap = EventMap> = Context<E> & {
114
- /** Binds the handler of a command, or a list of them; undone on unmount. */
115
- handle<P, R>(command: Command<P, R>, fn: Task<P, E, R>): UnsubscribeCallback;
116
- handle(type: string, fn: Task<never, E, unknown>): UnsubscribeCallback;
117
- handle(bindings: readonly Binding<E>[]): UnsubscribeCallback;
118
- /**
119
- * Mounts sub-modules; they are unmounted with this one. The returned function
120
- * unmounts just these, e.g. when they were added later from a listener.
121
- */
122
- use(...modules: Module<E>[]): UnsubscribeCallback;
123
- /**
124
- * Starts a background task that lives as long as the module: it is aborted
125
- * on unmount, and an error of it is left unhandled, as in any handler.
126
- */
127
- spawn<P>(fn: Task<P, E, unknown>, ...payload: PayloadOf<P>): void;
128
- };
129
- type Module<E extends EventMap = EventMap> = {
130
- readonly name: string;
131
- readonly setup: (m: ModuleContext<E>) => void;
4
+ type Creator = {
5
+ (...args: never[]): {
6
+ type: string;
7
+ payload?: unknown;
8
+ };
9
+ readonly type: string;
132
10
  };
133
- /**
134
- * Describes one domain of an app: the commands it handles, the events it
135
- * listens to, its sub-modules and its background tasks. The `name` shows up as
136
- * the type of the module's tasks in the trace.
137
- *
138
- * @example
139
- * ```ts
140
- * export const userModule = createModule<Events>("user", (m) => {
141
- * m.handle(getUser, fetchUser);
142
- * m.handle([bindHandler(sendMail, smtpSend)]);
143
- * m.use(authModule);
144
- *
145
- * m.on(loginRequested, (user, ctx) => ctx.emit(userLoaded(user)));
146
- * m.spawn(async (_, ctx) => {
147
- * for (;;) await handleNext(await take(ctx, "job"));
148
- * });
149
- * });
150
- * ```
151
- */
152
- export declare const createModule: <E extends EventMap = EventMap>(name: string, setup: (m: ModuleContext<E>) => void) => Module<E>;
153
- type Registry<E extends EventMap> = ReturnType<typeof createCommands<E>>;
154
- /** What `registerModules` returns. */
155
- type ModuleApi<E extends EventMap = EventMap> = {
156
- /** The registry of command handlers of the bus: the one it was made with, or the app's own. */
157
- commands: Registry<E>;
158
- /** Mounts a module and returns the function that unmounts it; a module that is mounted already is not run again. */
159
- mount(module: Module<E>): UnsubscribeCallback;
160
- unmount(module: Module<E>): void;
161
- isMounted(module: Module<E>): boolean;
162
- /** Unmounts every module; a disposed app cannot mount modules. */
163
- dispose(): void;
11
+ type Outcome = string | Creator;
12
+ /** What `createJob` returns: the command of the job and the actions of its outcome. */
13
+ type Job<P, R, T extends string, S extends Outcome, F extends Outcome> = {
14
+ command: Command<P, Promise<Awaited<R> | undefined>> & {
15
+ readonly type: T;
16
+ };
17
+ /** Made from a string with the result as its payload, or the creator that was given. */
18
+ successAction: S extends string ? ActionCreator<Awaited<R>, S> : S;
19
+ /** Made from a string with the error as its payload, or the creator that was given. */
20
+ failAction: F extends string ? ActionCreator<unknown, F> : F;
164
21
  };
165
22
  /**
166
- * Assembles an app from modules on `bus`: mounts `modules` at once, in the
167
- * order given, and returns `mount`, `unmount`, `isMounted` and `dispose` for
168
- * the modules that come and go later. The bus runs the commands that the
169
- * modules bind: if it was made with `commands: createCommands()` that registry
170
- * is used, otherwise the app brings its own, as `app.commands`.
171
- *
172
- * A module is known by its `name`: mounting a module whose name is taken
173
- * replaces the mounted one, which is unmounted first. That is what hot reload
174
- * needs (see `docs/hmr.md`): the new version of a module file mounts again and
175
- * the old version goes away with its subscriptions, commands and tasks.
176
- *
177
- * Unmounting undoes everything the module did: its subscriptions, its bound
178
- * commands, its sub-modules and its background tasks. `dispose` unmounts them
179
- * all, and a disposed app cannot mount modules.
23
+ * Creates a job: a command whose outcome is sent on as actions. When the
24
+ * handler succeeds, `successAction(result)` is emitted on the bus, and when it
25
+ * fails, `failAction(error)`. It is made for the case where nobody waits for
26
+ * the result and the bus carries it on, like `createAsyncThunk` or an effector
27
+ * effect. It returns the three together: `{ command, successAction, failAction }`.
28
+ *
29
+ * `success` and `fail` are a type string, from which the job makes an action
30
+ * creator with `createAction`, or an action creator that exists already, which
31
+ * is used and returned as it is. A missing one is made with the type
32
+ * `<type>/success` or `<type>/fail`.
33
+ *
34
+ * `bus.emit(command, payload)` returns a promise of the result. An error of the
35
+ * handler always counts as handled: it goes to `failAction` and the promise
36
+ * resolves with `undefined`; a command is the tool for a result that must reject.
37
+ * The actions are emitted as the task that ran the job, also for a handler bound
38
+ * with `commands.handle`, and are skipped once the task is aborted. A missing
39
+ * handler is not an outcome: `emit` throws `MissingHandlerError`. Calling the
40
+ * command directly runs only its handler.
180
41
  *
181
42
  * @example
182
43
  * ```ts
183
- * const bus = createBus<Events>();
184
- * const app = registerModules(bus, [userModule]);
185
- *
186
- * const unmount = app.mount(orderModule); // later, e.g. after a lazy import
187
- * unmount(); // or app.unmount(orderModule)
44
+ * const { command: loadUser, successAction: userLoaded, failAction: userFailed } =
45
+ * createJob("user/load", fetchUser, { success: "user/loaded", fail: "user/failed" });
188
46
  *
189
- * bus.emit(getUser, "1");
190
- * app.commands.handle(getUser, fakeUser); // in a test
191
- * app.dispose();
47
+ * bus.on(userLoaded, (user) => render(user));
48
+ * bus.emit(loadUser, "1"); // userLoaded(user) or userFailed(error) comes later
192
49
  * ```
193
50
  */
194
- export declare const registerModules: <E extends EventMap>(bus: Bus<E>, modules?: readonly Module<E>[]) => ModuleApi<E>;
51
+ export declare const createJob: <P = void, R = void, T extends string = string, E extends EventMap = EventMap, S extends Outcome = `${T}/success`, F extends Outcome = `${T}/fail`>(type: T, handler?: Task<P, E, R>, outcomes?: {
52
+ /** Gets the result when the handler succeeds: a type to make an action creator of, or an action creator. */
53
+ success?: S;
54
+ /** Gets the error when the handler fails: a type to make an action creator of, or an action creator. */
55
+ fail?: F;
56
+ }) => Job<P, R, T, S, F>;
195
57
  //#endregion
196
- export { type ActionCreator, type Binding, type FreeActionCreator, type Module, type ModuleApi, type ModuleContext, createAction };
58
+ export { type ActionCreator, type Binding, type FreeActionCreator, type Job, type Module, type ModuleApi, type ModuleContext, bindHandler, createAction, createCommand, createCommands, createModule, modulePlugin };
package/dist/actions.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./onAbort-BgMfspK4.js";import{i as t,n}from"./context-BXQ6Einm.js";import{t as r}from"./event-0RIBGYqS.js";function i(e,t){return Object.assign((...n)=>({type:e,payload:t?t(...n).payload:n[0]}),{type:e,match:t=>t.type===e,toString:()=>e})}var a=class extends Error{};const o=(e,t)=>Object.assign((r,i)=>{if(!t)throw new a(`No handler for command "${e}"`);return t(r,i??n({type:e,payload:r}).ctx)},{type:e}),s=(e,t)=>[e,t],c=(e=[])=>{let t=/* @__PURE__ */ new Map,n=(e,n)=>{let r={fn:n};return t.set(e,[...t.get(e)??[],r]),()=>{let n=(t.get(e)??[]).filter(e=>e!==r);n.length>0?t.set(e,n):t.delete(e)}};function i(e,t){if(t)return n(r(e),t);let i=e.map(([e,t])=>n(r(e),t));return()=>{for(let e of i)e()}}return i(e),{run:(e,n,r)=>{let i=(t.get(e.type)??[]).at(-1)?.fn,a=i&&e.wrap?e.wrap(i):i??e;return Reflect.apply(a,null,[n,r])},handle:i}},l=(e,t,n)=>{let r=e.emit;typeof t==`string`?r(t,n):typeof t==`function`?r(`match`in t?t.type:t,n):r({type:t.type,payload:n})},u=e=>{try{return Promise.resolve(e())}catch(e){if(e instanceof a)throw e;return Promise.reject(e)}},d=(e,t,n={})=>{let r=o(e,t),i=(e,t,r)=>{let i=r;return u(()=>Reflect.apply(e,null,[t,r])).then(e=>(n.done&&l(i,n.done,e),e),e=>{if(!n.fail)throw e;l(i,n.fail,e)})};return Object.assign((e,t)=>t?i(r,e,t):u(()=>r(e)),{type:e,wrap:e=>(t,n)=>i(e,t,n)})},f=(e,t)=>({name:e,setup:t}),p=(r,i=[])=>{let a=r.commands??c();r.commands??=a;let o={handle:(...e)=>{if(!a.handle)throw Error("registerModules: the `commands` of the bus is not a registry from `createCommands()`");return Reflect.apply(a.handle,null,e)}},s=n({type:`app`,payload:void 0},r,r.trace),l=n=>{let r=/* @__PURE__ */ new Map,i=/* @__PURE__ */ new Map;return{mount:a=>{let s=r.get(a);if(s)return s;let c=i.get(a.name);c&&r.get(c)?.();let u=t(n,a.name),d=u.ctx,f=l(d),p=()=>{r.delete(a),i.get(a.name)===a&&i.delete(a.name),d.abort(),u.unlink()};r.set(a,p),i.set(a.name,a);let m=(t,n)=>{let r=Reflect.apply(o.handle,null,[t,n]);return e(d,r),r},h=(...e)=>{let t=e.map(f.mount);return()=>{for(let e of t)e()}},g=(e,...t)=>{d.run(e,...t).catch(e=>{if(!d.signal.aborted)throw e})};try{a.setup(Object.assign(d,{handle:m,use:h,spawn:g}))}catch(e){throw p(),e}return p},unmount:e=>r.get(e)?.(),isMounted:e=>r.has(e)}},u=l(s.ctx);for(let e of i)u.mount(e);return{commands:a,mount:e=>(s.ctx.signal.throwIfAborted(),u.mount(e)),unmount:u.unmount,isMounted:u.isMounted,dispose:()=>s.ctx.abort()}};export{s as bindHandler,i as createAction,o as createCommand,c as createCommands,d as createJob,f as createModule,p as registerModules};
1
+ import{t as e}from"./onAbort-BgMfspK4.js";import{i as t,n}from"./context-DlA80GOA.js";import{t as r}from"./event-0RIBGYqS.js";function i(e,t){return Object.assign((...n)=>({type:e,payload:t?t(...n).payload:n[0]}),{type:e,match:t=>t.type===e,toString:()=>e})}var a=class extends Error{};const o=(e,t)=>Object.assign((r,i)=>{if(!t)throw new a(`No handler for command "${e}"`);return t(r,i??n({event:{type:e,payload:r}}).ctx)},{type:e}),s=(e,t)=>[e,t],c=(e,t=[])=>{let n=/* @__PURE__ */ new Map,i=(e,t)=>{let r={fn:t};return n.set(e,[...n.get(e)??[],r]),()=>{let t=(n.get(e)??[]).filter(e=>e!==r);t.length>0?n.set(e,t):n.delete(e)}};function a(e,t){if(t)return i(r(e),t);let n=e.map(([e,t])=>i(r(e),t));return()=>{for(let e of n)e()}}a(t);let o={run:(e,t,r)=>{let i=(n.get(e.type)??[]).at(-1)?.fn,a=i&&e.wrap?e.wrap(i):i??e;return Reflect.apply(a,null,[t,r])},handle:a};return e.commands=o,o},l=e=>typeof e==`string`?i(e):e,u=(e,t,n)=>{let r=e.emit;r(Reflect.apply(t,null,[n]))},d=e=>{try{return Promise.resolve(e())}catch(e){if(e instanceof a)throw e;return Promise.reject(e)}},f=(e,t,n={})=>{let r=o(e,t),i=l(n.success??`${e}/success`),a=l(n.fail??`${e}/fail`),s=(e,t,n)=>{let r=n;return d(()=>Reflect.apply(e,null,[t,n])).then(e=>(u(r,i,e),e),e=>{u(r,a,e)})};return{command:Object.assign((e,t)=>t?s(r,e,t):d(()=>r(e)),{type:e,wrap:e=>(t,n)=>s(e,t,n)}),successAction:i,failAction:a}},p=(e,t)=>({name:e,setup:t}),m=(r=[])=>i=>{let a=i.commands??c(i),o={handle:(...e)=>{if(!a.handle)throw Error("modulePlugin: the `commands` of the bus is not a registry from `createCommands()`");return Reflect.apply(a.handle,null,e)}},s=n({event:{type:`app`,payload:void 0},bus:i,tracer:i.trace}),l=n=>{let r=/* @__PURE__ */ new Map,i=/* @__PURE__ */ new Map;return{mount:a=>{let s=r.get(a);if(s)return s;let c=i.get(a.name);c&&r.get(c)?.();let u=t(n,a.name),d=u.ctx,f=l(d),p=()=>{r.delete(a),i.get(a.name)===a&&i.delete(a.name),d.abort(),u.unlink()};r.set(a,p),i.set(a.name,a);let m=(t,n)=>{let r=Reflect.apply(o.handle,null,[t,n]);return e(d,r),r},h=(...e)=>{let t=e.map(e=>typeof e==`function`?m(e,e):f.mount(e));return()=>{for(let e of t)e()}},g=(e,...t)=>{d.run(e,...t).catch(e=>{if(!d.signal.aborted)throw e})};try{a.setup(Object.assign(d,{handle:m,use:h,spawn:g}))}catch(e){throw p(),e}return p},unmount:e=>r.get(e)?.(),isMounted:e=>r.has(e)}},u=l(s.ctx);for(let e of r)u.mount(e);return{commands:a,mount:e=>(s.ctx.signal.throwIfAborted(),u.mount(e)),unmount:u.unmount,isMounted:u.isMounted,dispose:()=>s.ctx.abort()}};export{s as bindHandler,i as createAction,o as createCommand,c as createCommands,f as createJob,p as createModule,m as modulePlugin};
@@ -0,0 +1 @@
1
+ import{n as e}from"./onAbort-BgMfspK4.js";import{a as t,n,r}from"./context-DlA80GOA.js";import{n as i}from"./promise-CSLpzwSR.js";import{n as a,t as o}from"./event-0RIBGYqS.js";const s=`*`,c=()=>{let e={},t=/* @__PURE__ */ new WeakSet,n=n=>{let r=e[n]??[];if(!t.has(r))return e[n]=r,r;let i=[...r];return e[n]=i,i},r=(t,r)=>{if(!r){e[t]=[];return}let i=(e[t]??[]).indexOf(r);i>=0&&n(t).splice(i,1)};return{get:n=>{let r=e[n]??[];return t.add(r),r},add:(e,t)=>(n(e).push(t),()=>{r(e,t)}),remove:r}},l=(e,t,n)=>Reflect.apply(e,null,[t,n]),u=(s={})=>{let u=c(),d=(e,t)=>u.add(o(e),t),f=(e,t)=>u.remove(o(e),t),p=(t,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:m,tracer:s.trace,parent:{ref:r()}});if(!o){t(a.payload,c);return}let l=t=>{try{o(t,a)}catch(t){e(`onError failed`,t)}};try{let e=t(a.payload,c);i(e)&&Promise.resolve(e).catch(l)}catch(e){l(e)}},m={on:d,off:f,emit:(e,i)=>{let c=o(e),d=a(e,i),f={type:c,payload:d},h=[...u.get(c),...u.get(`*`)];h.length===0&&typeof e!=`function`&&s.onUnhandled?.(f);for(let e of h)p(e,f);if(typeof e==`function`){let{ctx:i}=n({event:f,bus:m,tracer:s.trace,parent:{ref:r()}}),a=m.commands?.run??l;return t(i,`command`,()=>a(e,d,i))}},use:e=>e(m),context:s.context,trace:s.trace};return m};export{s as n,u as t};
@@ -0,0 +1 @@
1
+ import{t as e}from"./onAbort-BgMfspK4.js";const t=t=>{let n=new AbortController;return{controller:n,unlink:e({signal:t},e=>n.abort(e))}},n=typeof __NANOEFFECTS_TRACE__>`u`||__NANOEFFECTS_TRACE__,r=/* @__PURE__ */ new WeakMap;let i;const a=()=>i,o=()=>{},s=(e,t,i,a)=>{let o=n&&r.get(e);o&&o.tracer.report(o.ref,t,i,a)},c=(e,t)=>{let i=n&&r.get(e);return i?i.tracer.settler(i.ref,t):o},l=(e,t,i)=>{let a=n&&r.get(e);return a?a.tracer.run(a.ref,t,e.signal,i):i()},u=(e,t)=>typeof e==`function`?e(t):e,d=e=>e,f=({event:a,bus:s,tracer:c,parent:p})=>{let{controller:m,unlink:h}=t(p?.signal),{signal:g}=m,_=n&&c?{tracer:c,ref:c.task(a,p?.ref)}:void 0,v=(...e)=>{if(g.aborted)return;let t=s?.emit,n=i;i=_?.ref;try{return t?.(...e)}finally{i=n}},y=(...t)=>{if(g.aborted)return o;let n=s?.on,r=n?.(...t);if(!r)return o;let i=e({signal:g},r);return()=>{i(),r()}},b=async(e,...t)=>{g.throwIfAborted();let n=t[0],r=f({event:{type:a.type,payload:n},bus:s,tracer:c,parent:{signal:g,ref:_?.ref}});try{return await l(r.ctx,`run`,()=>e(n,r.ctx))}finally{r.unlink()}},x=d({...u(s?.context,a),type:a.type,payload:a.payload,signal:g,abort:e=>m.abort(e),emit:v,on:y,off:s?.off??o,run:b});return _&&r.set(x,_),{ctx:x,unlink:h}},p=(e,t)=>{let i=n?r.get(e):void 0;return f({event:{type:t??e.type??``,payload:e.payload},bus:{emit:e.emit,on:e.on,off:e.off,context:e},tracer:i?.tracer,parent:{signal:e.signal,ref:i?.ref}})};export{l as a,p as i,f as n,s as o,a as r,c as t};
package/dist/dev.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { L as UnsubscribeCallback, k as EventMap, o as Bus } from "./action-rqZEnRIp.js";
2
+ import { n as ModuleApi } from "./module-BSCdR9D1.js";
3
+ //#region src/dev/types.d.ts
4
+ /** The parts of what `modulePlugin` returned that the diagnostics report. */
5
+ type ReportedApp<E extends EventMap> = Pick<ModuleApi<E>, "mount" | "unmount" | "isMounted">;
6
+ type DevOptions<E extends EventMap = EventMap> = {
7
+ /** Warn when more listeners than this are added to one event and not unsubscribed. Default 10. */
8
+ maxListeners?: number;
9
+ /** Check the number of listeners. Default `true`. */
10
+ leaks?: boolean;
11
+ /** Report the modules of `app` that are mounted, replaced and unmounted. Default `true`. */
12
+ modules?: boolean;
13
+ /** Receives every message. Default: `console.warn` for leaks and `console.info` for modules. */
14
+ log?: (message: string) => void;
15
+ /** What `modulePlugin` returned, to report its modules. */
16
+ app?: ReportedApp<E>;
17
+ };
18
+ /** A plugin that connects the diagnostics to a bus; the function it returns disconnects them. */
19
+ type DevPlugin = <E extends EventMap>(options?: DevOptions<E>) => (bus: Bus<E>) => UnsubscribeCallback;
20
+ //#endregion
21
+ //#region src/dev/plugin.d.ts
22
+ /**
23
+ * A plugin with development diagnostics for a bus, used while the app is built
24
+ * and while its files are reloaded:
25
+ *
26
+ * - **Listeners that pile up.** When more than `maxListeners` listeners are
27
+ * added to one event and not unsubscribed, one warning names the event and
28
+ * the count: a file that was reloaded and subscribes again, or an
29
+ * unsubscribe that is never called. The count follows the function that `on`
30
+ * returns; `bus.off(event, fn)` is not seen.
31
+ * - **Modules.** With `app` (what `modulePlugin` returned) it reports a
32
+ * module that is mounted, replaced by name and unmounted.
33
+ *
34
+ * It wraps `bus.on`, `app.mount` and `app.unmount`; the function that `bus.use` returns
35
+ * puts the originals back. In a production build `nanoeffects/dev` is a no-op, so
36
+ * the call can stay as it is.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const app = bus.use(modulePlugin([userModule]));
41
+ * const disconnect = bus.use(devPlugin({ app })); // warnings and messages in the console while developing
42
+ * ```
43
+ */
44
+ export declare const devPlugin: DevPlugin;
45
+ //#endregion
46
+ export type { DevOptions, DevPlugin };
package/dist/dev.js ADDED
@@ -0,0 +1 @@
1
+ import{t as e}from"./event-0RIBGYqS.js";const t=`[nanoeffects]`,n=(e={})=>t=>{let{maxListeners:n=10,leaks:a=!0,modules:o=!0,app:s}=e,c=e.log??(e=>console.warn(e)),l=e.log??(e=>console.info(e)),u=[];return a&&u.push(r(t,n,c)),o&&s&&u.push(i(s,l)),()=>{for(let e of u.splice(0))e()}},r=(n,r,i)=>{let a=n.on,o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Set;return n.on=(...c)=>{let[l]=c,u=e(l),d=(o.get(u)??0)+1;o.set(u,d),d>r&&!s.has(u)&&(s.add(u),i(`${t} ${d} listeners of "${u}" were added and not unsubscribed (more than ${r}). A file that is reloaded and subscribes again, or an unsubscribe that is never called?`));let f=Reflect.apply(a,n,c),p=!1;return()=>{if(!p){p=!0;let e=(o.get(u)??1)-1;o.set(u,e),e<=r&&s.delete(u)}f()}},()=>{n.on=a}},i=(e,n)=>{let{mount:r,unmount:i}=e,a=/* @__PURE__ */ new Map;return e.mount=i=>{let o=a.get(i.name);if(o===i&&e.isMounted(i))return r(i);let s=o!==void 0&&e.isMounted(o),c=r(i);return a.set(i.name,i),n(`${t} module "${i.name}" ${s?`replaced`:`mounted`}`),()=>{e.isMounted(i)&&n(`${t} module "${i.name}" unmounted`),c()}},e.unmount=r=>{e.isMounted(r)&&n(`${t} module "${r.name}" unmounted`),i(r)},()=>{e.mount=r,e.unmount=i}};export{n as devPlugin};