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.
- package/README.md +62 -61
- package/dist/{action-BM-UdmJr.d.ts → action-rqZEnRIp.d.ts} +12 -11
- package/dist/actions.d.ts +47 -185
- package/dist/actions.js +1 -1
- package/dist/bus-CRnlwmQ4.js +1 -0
- package/dist/context-DlA80GOA.js +1 -0
- package/dist/dev.d.ts +46 -0
- package/dist/dev.js +1 -0
- package/dist/effects.d.ts +61 -21
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/module-BSCdR9D1.d.ts +167 -0
- package/dist/notrace/{action-BM-UdmJr.d.ts → action-rqZEnRIp.d.ts} +12 -11
- package/dist/notrace/actions.d.ts +71 -45
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-Dx0l9X6h.js +1 -0
- package/dist/notrace/context-yAWj0BFZ.js +1 -0
- package/dist/notrace/{delay-Cko_Q9hl.js → delay-Cgvn16oY.js} +1 -1
- package/dist/notrace/effects.d.ts +61 -21
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +1 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +2 -28
- package/dist/notrace/redux.js +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/policies.d.ts +1 -1
- package/dist/policies.js +1 -1
- package/dist/production/dev.js +1 -0
- package/dist/redux.d.ts +2 -28
- package/dist/redux.js +1 -1
- package/dist/trace.d.ts +22 -4
- package/dist/trace.js +1 -1
- package/docs/README.md +4 -1
- package/docs/actions.md +21 -11
- package/docs/context.md +0 -1
- package/docs/effects.md +49 -31
- package/docs/errors.md +4 -4
- package/docs/guide.md +229 -0
- package/docs/hmr.md +20 -58
- package/docs/policies.md +4 -4
- package/docs/stores.md +0 -2
- package/docs/tracing.md +21 -8
- package/package.json +25 -5
- package/dist/bus-RUiw8f9p.js +0 -1
- package/dist/context-BXQ6Einm.js +0 -1
- package/dist/index-CaxrMD1A.d.ts +0 -1
- package/dist/notrace/bus-CFxgpsRL.js +0 -1
- package/dist/notrace/context-DGPIFXxI.js +0 -1
- package/dist/notrace/index-CaxrMD1A.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,36 +1,53 @@
|
|
|
1
1
|
# nanoeffects
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/nanoeffects)
|
|
4
|
+
[](https://github.com/nanoeffects/nanoeffects/actions/workflows/ci.yml)
|
|
5
|
+
[](https://bundlephobia.com/package/nanoeffects)
|
|
6
|
+
[](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 = {
|
|
13
|
+
type Events = { greet: string };
|
|
11
14
|
|
|
12
15
|
const bus = createBus<Events>();
|
|
13
16
|
|
|
14
|
-
|
|
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("
|
|
19
|
+
bus.emit("greet", "Ada"); // hello Ada
|
|
20
|
+
unbind(); // `on` returns the unsubscribe function, as in nanoevents
|
|
24
21
|
```
|
|
25
22
|
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
58
|
+
ESM only, no dependencies, types included.
|
|
42
59
|
|
|
43
60
|
## Entries
|
|
44
61
|
|
|
45
|
-
| Import
|
|
46
|
-
|
|
|
47
|
-
| `nanoeffects`
|
|
48
|
-
| `nanoeffects/policies`
|
|
49
|
-
| `nanoeffects/effects`
|
|
50
|
-
| `nanoeffects/actions`
|
|
51
|
-
| `nanoeffects/redux`
|
|
52
|
-
| `nanoeffects/observable` | `createObserve`: an event
|
|
53
|
-
| `nanoeffects/
|
|
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
|
-
|
|
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
|
-
`
|
|
75
|
+
`bus.on(event, handler)` calls `handler(payload, ctx)`. Each call is a task:
|
|
68
76
|
|
|
69
|
-
|
|
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
|
|
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
|
-
##
|
|
87
|
+
## Docs
|
|
74
88
|
|
|
75
|
-
|
|
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
|
|
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)
|
|
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
|
-
/**
|
|
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; `
|
|
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
|
|
218
|
-
* these when the names are the same. A function gets
|
|
219
|
-
* the task and returns its fields, for fields that
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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
|
|
184
|
-
*
|
|
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.
|
|
190
|
-
*
|
|
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
|
|
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-
|
|
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};
|