nanoeffects 0.0.0-stage → 0.2.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -2
  3. package/dist/action-rqZEnRIp.d.ts +303 -0
  4. package/dist/actions.d.ts +58 -0
  5. package/dist/actions.js +1 -0
  6. package/dist/bus-CRnlwmQ4.js +1 -0
  7. package/dist/context-DlA80GOA.js +1 -0
  8. package/dist/delay-CIHnOh_L.js +1 -0
  9. package/dist/dev.d.ts +46 -0
  10. package/dist/dev.js +1 -0
  11. package/dist/effects.d.ts +284 -0
  12. package/dist/effects.js +1 -0
  13. package/dist/event-0RIBGYqS.js +1 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +1 -0
  16. package/dist/module-BSCdR9D1.d.ts +167 -0
  17. package/dist/notrace/action-rqZEnRIp.d.ts +303 -0
  18. package/dist/notrace/actions.d.ts +222 -0
  19. package/dist/notrace/actions.js +1 -0
  20. package/dist/notrace/bus-Dx0l9X6h.js +1 -0
  21. package/dist/notrace/context-yAWj0BFZ.js +1 -0
  22. package/dist/notrace/delay-Cgvn16oY.js +1 -0
  23. package/dist/notrace/effects.d.ts +284 -0
  24. package/dist/notrace/effects.js +1 -0
  25. package/dist/notrace/event-0RIBGYqS.js +1 -0
  26. package/dist/notrace/index.d.ts +2 -0
  27. package/dist/notrace/index.js +1 -0
  28. package/dist/notrace/policies.d.ts +171 -0
  29. package/dist/notrace/policies.js +1 -0
  30. package/dist/notrace/promise-CSLpzwSR.js +1 -0
  31. package/dist/notrace/redux.d.ts +92 -0
  32. package/dist/notrace/redux.js +1 -0
  33. package/dist/observable.d.ts +20 -0
  34. package/dist/observable.js +1 -0
  35. package/dist/onAbort-BgMfspK4.js +1 -0
  36. package/dist/policies.d.ts +171 -0
  37. package/dist/policies.js +1 -0
  38. package/dist/production/dev.js +1 -0
  39. package/dist/promise-CSLpzwSR.js +1 -0
  40. package/dist/redux.d.ts +92 -0
  41. package/dist/redux.js +1 -0
  42. package/dist/trace.d.ts +162 -0
  43. package/dist/trace.js +1 -0
  44. package/docs/README.md +15 -0
  45. package/docs/actions.md +78 -0
  46. package/docs/context.md +15 -0
  47. package/docs/effects.md +137 -0
  48. package/docs/errors.md +23 -0
  49. package/docs/guide.md +229 -0
  50. package/docs/hmr.md +36 -0
  51. package/docs/observable.md +22 -0
  52. package/docs/policies.md +53 -0
  53. package/docs/semantics.md +7 -0
  54. package/docs/stores.md +50 -0
  55. package/docs/tracing.md +117 -0
  56. package/package.json +184 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vladimir Yazykov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,97 @@
1
- # Temporary Holding Version
1
+ # nanoeffects
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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`.
9
+
10
+ ```ts
11
+ import { createBus } from "nanoeffects";
12
+
13
+ type Events = { greet: string };
14
+
15
+ const bus = createBus<Events>();
16
+
17
+ const unbind = bus.on("greet", (name) => console.log(`hello ${name}`));
18
+
19
+ bus.emit("greet", "Ada"); // hello Ada
20
+ unbind(); // `on` returns the unsubscribe function, as in nanoevents
21
+ ```
22
+
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:
24
+
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.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ npm install nanoeffects
56
+ ```
57
+
58
+ ESM only, no dependencies, types included.
59
+
60
+ ## Entries
61
+
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`, `nanoeffectsMiddleware` |
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 |
72
+
73
+ ## The task context
74
+
75
+ `bus.on(event, handler)` calls `handler(payload, ctx)`. Each call is a task:
76
+
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 |
84
+
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.
86
+
87
+ ## Docs
88
+
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)
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ pnpm verify # what CI runs: types, lint, tests, build, integration, package checks, size
95
+ ```
96
+
97
+ [`AGENTS.md`](AGENTS.md) describes the structure and the rules of the code, for people and AI assistants.
@@ -0,0 +1,303 @@
1
+ //#region src/core/types.d.ts
2
+ type PayloadOf<P> = undefined extends P ? [payload?: P] : [payload: P];
3
+ type EventMap = Record<string, unknown>;
4
+ type Keys<E> = keyof E & string;
5
+ type Task<P, E extends EventMap, R = unknown> = (payload: P, ctx: Context<E>) => R;
6
+ type EffectName = "every" | "latest" | "leading" | "debounce" | "throttle" | "run" | "all" | "race" | "retry" | "timeout" | "poll" | "take" | "queue" | "allSettled" | "command";
7
+ type Phase = "start" | "done" | "error" | "cancel" | "drop";
8
+ type TraceEvent<E = Record<string, unknown>> = { [K in Keys<E>]: {
9
+ id: number;
10
+ parent?: number;
11
+ type: K;
12
+ effect: EffectName;
13
+ phase: Phase;
14
+ payload: E[K];
15
+ error?: unknown;
16
+ }; }[Keys<E>];
17
+ /**
18
+ * The contract of every trace logger: receives each task phase of a bus.
19
+ * Adapters in `nanoeffects/trace` are factories that return one; pass it
20
+ * through `createTracer` to `createBus({ trace })`.
21
+ */
22
+ type TraceLogger<E = Record<string, unknown>> = (event: TraceEvent<E>) => void;
23
+ /** One logger or several; a throwing logger never breaks the task or the others. */
24
+ type Trace<E = Record<string, unknown>> = TraceLogger<E> | readonly TraceLogger<E>[];
25
+ type UnsubscribeCallback = () => void;
26
+ //#endregion
27
+ //#region src/core/store.d.ts
28
+ declare const anyEventType = "*";
29
+ //#endregion
30
+ //#region src/core/tracer.d.ts
31
+ /**
32
+ * What the bus needs from tracing. The implementation, `createTracer` in
33
+ * `nanoeffects/trace`, holds all the machinery (ids, events, loggers), so a
34
+ * bus without `trace` carries none of it. `Ref` is an opaque handle that
35
+ * `task` makes for each task.
36
+ */
37
+ interface Tracer<Ref extends object = object> {
38
+ /** Registers a task and returns its handle; `parent` is the handle of its parent task. */
39
+ task(event: {
40
+ type: string;
41
+ payload?: unknown;
42
+ }, parent?: Ref): Ref;
43
+ /** Reports a phase of a task. */
44
+ report(ref: Ref, effect: EffectName, phase: Phase, error?: unknown): void;
45
+ /** Returns `settle(phase, error?)` that reports only the first phase it gets. */
46
+ settler(ref: Ref, effect: EffectName): (phase: Phase, error?: unknown) => void;
47
+ /**
48
+ * Runs `fn`, reporting `start` and then `done` / `error` of its (sync or
49
+ * async) result, or `cancel` if `signal` aborts first.
50
+ */
51
+ run<R>(ref: Ref, effect: EffectName, signal: AbortSignal, fn: () => R): R;
52
+ }
53
+ //#endregion
54
+ //#region src/core/bus.d.ts
55
+ /** Subscribes to every event; handlers receive the payload of any of them. */
56
+ type AnyEvent = typeof anyEventType;
57
+ type EventKey<E extends EventMap> = Keys<E>;
58
+ type EventPayload<E extends EventMap> = E[EventKey<E>];
59
+ type WithType<K extends string> = {
60
+ type: K;
61
+ };
62
+ type HandlerRef<E extends EventMap, Response = unknown> = (action: Action<E>) => Response;
63
+ type Handler<E extends EventMap, Response = unknown, K extends EventKey<E> = EventKey<E>> = { [P in K]: {
64
+ type: P;
65
+ (payload: E[P]): Response;
66
+ }; }[K];
67
+ type Action<E extends EventMap> = {
68
+ type: EventKey<E>;
69
+ payload: EventPayload<E>;
70
+ };
71
+ type WithPayload<E extends EventMap, K extends EventKey<E>> = undefined extends E[K] ? {
72
+ type: K;
73
+ payload?: E[K];
74
+ } : {
75
+ type: K;
76
+ payload: E[K];
77
+ };
78
+ type EventLike<E extends EventMap> = { [K in EventKey<E>]: WithPayload<E, K>; }[Keys<E>];
79
+ /**
80
+ * A typed request for a result. `bus.emit(command, payload)` runs the handler
81
+ * of the command as a task and returns what it returns. Made by
82
+ * `createCommand` in `nanoeffects/actions`.
83
+ */
84
+ type Command<P = never, R = unknown> = {
85
+ (payload: P, ctx?: never): R;
86
+ readonly type: string;
87
+ /** Wraps the handler that really runs, e.g. to route its outcome. */
88
+ readonly wrap?: (run: (payload: never, ctx: never) => unknown) => (payload: never, ctx: never) => unknown;
89
+ };
90
+ /**
91
+ * What the bus needs to run commands. The implementation, `createCommands` in
92
+ * `nanoeffects/actions`, holds the handlers bound to commands and runs the
93
+ * right one, so a bus without `commands` carries none of that.
94
+ */
95
+ interface Commands {
96
+ run(command: Command, payload: unknown, ctx: unknown): unknown;
97
+ }
98
+ /** Anything `emit` takes as its first argument: an event type, an action or a command. */
99
+ type Emittable<E extends EventMap> = EventKey<E> | Command | EventLike<E>;
100
+ type Emit<E extends EventMap> = {
101
+ <K extends EventKey<E>>(event: K, ...payload: PayloadOf<E[K]>): void;
102
+ <K extends EventKey<E>, R>(event: Handler<E, R, K>, ...payload: PayloadOf<E[K]>): R;
103
+ <P, R>(command: Command<P, R>, ...payload: PayloadOf<P>): R;
104
+ (event: EventLike<E>): void;
105
+ };
106
+ type On<E extends EventMap> = {
107
+ (event: AnyEvent, fn: Task<EventPayload<E>, E>): UnsubscribeCallback;
108
+ <K extends EventKey<E>>(event: K, fn: Task<E[K], E>): UnsubscribeCallback;
109
+ /** An action creator without a payload type: the payload comes from the event map. */
110
+ <K extends EventKey<E>>(creator: FreeActionCreator<K>, fn: Task<E[K], E>): UnsubscribeCallback;
111
+ /** An action creator: the payload comes from it, so an event map is not needed. */
112
+ <P>(creator: ActionCreator<P>, fn: Task<P, E>): UnsubscribeCallback;
113
+ /** A command: the payload comes from it too. */
114
+ <P>(command: Command<P, unknown>, fn: Task<P, E>): UnsubscribeCallback;
115
+ <K extends EventKey<E>, Event extends {
116
+ type: K;
117
+ }>(event: Event & {
118
+ type: K;
119
+ }, fn: Task<E[K], E>): UnsubscribeCallback;
120
+ };
121
+ type Off<E extends EventMap> = {
122
+ (event: AnyEvent): void;
123
+ (event: AnyEvent, fn: Task<EventPayload<E>, E>): void;
124
+ <K extends EventKey<E>>(event: K): void;
125
+ <K extends EventKey<E>>(event: K, fn: Task<E[K], E>): void;
126
+ <K extends EventKey<E>, Event extends {
127
+ type: K;
128
+ }>(event: Event & {
129
+ type: K;
130
+ }, fn: Task<E[K], E>): void;
131
+ <P>(command: Command<P, unknown>, fn?: Task<P, E>): void;
132
+ };
133
+ declare const contextKey: unique symbol;
134
+ /**
135
+ * Your own fields of every task context, in the event map: add it to the
136
+ * events with `&` and give the values in `createBus({ context })`. The fields
137
+ * are in the context of every task of that bus, and the compiler checks them.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * type Events = { save: Draft } & WithContext<{ store: Store }>;
142
+ *
143
+ * const bus = createBus<Events>({ context: { store } });
144
+ * bus.on("save", (draft, { store }) => store.put(draft));
145
+ * ```
146
+ */
147
+ type WithContext<Fields extends object> = {
148
+ readonly [contextKey]?: Fields;
149
+ };
150
+ /** The fields that the event map `E` adds to the context of a task. */
151
+ type ContextOf<E extends EventMap> = typeof contextKey extends keyof E ? NonNullable<E[typeof contextKey]> : unknown;
152
+ /** Fields for the context of every task, or a function that makes them for one task. */
153
+ type ContextOption<E extends EventMap> = ContextOf<E> | ((event: {
154
+ type: string;
155
+ payload?: unknown;
156
+ }) => ContextOf<E>);
157
+ /** Task context inside an effect. */
158
+ type Context<E extends EventMap> = EventLike<E> & ContextOf<E> & {
159
+ signal: AbortSignal;
160
+ /** Abort this task (and attached children). */
161
+ abort: (reason?: unknown) => void;
162
+ /** Emit as this task; no-op after the task is aborted. */
163
+ emit: Emit<E>;
164
+ /**
165
+ * Subscribe as this task. The subscription is removed when the task aborts;
166
+ * after that this is a no-op that returns a no-op.
167
+ */
168
+ on: On<E>;
169
+ /** Same as the bus `off`. */
170
+ off: Off<E>;
171
+ /** Child task: aborted with the parent; errors propagate to the parent. */
172
+ run<P, R>(fn: Task<P, E, R>, ...args: PayloadOf<P>): Promise<Awaited<R>>;
173
+ };
174
+ interface Bus<E extends EventMap> {
175
+ on: On<E>;
176
+ off: Off<E>;
177
+ emit: Emit<E>;
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`. */
185
+ readonly context?: ContextOption<E> | undefined;
186
+ /** What runs the commands; `createCommands(bus)` and `modulePlugin` set it. Without one a command runs its own handler. */
187
+ commands?: Commands | undefined;
188
+ readonly trace?: Tracer | undefined;
189
+ }
190
+ type BusOptions<E extends EventMap> = {
191
+ /** Tracing of every task phase; make it with `createTracer` from `nanoeffects/trace`. */
192
+ trace?: Tracer;
193
+ /**
194
+ * Receives errors of handlers: thrown ones and rejections of async ones.
195
+ * Without it handlers run like in a plain loop: the first error stops the
196
+ * rest and is thrown to the caller of `emit`, and rejections of async
197
+ * handlers are left unhandled. With it every handler runs and errors go
198
+ * here; an `onError` that throws is logged to `console.error`.
199
+ */
200
+ onError?: (error: unknown, event: Action<E>) => void;
201
+ /**
202
+ * Called when an event is emitted and nobody listens to it: the usual sign
203
+ * of a typo in an event name. Not called for commands, which always have a
204
+ * handler to run. Typically a warning in development only.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * createBus<Events>({
209
+ * onUnhandled: import.meta.env.DEV ? ({ type }) => console.warn(`Nobody listens to "${type}"`) : void 0,
210
+ * });
211
+ * ```
212
+ */
213
+ onUnhandled?: (event: Action<E>) => void;
214
+ /**
215
+ * Fields added to the context of every task of this bus, its child tasks
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
+ * 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.
221
+ */
222
+ context?: ContextOption<E>;
223
+ };
224
+ declare const createBus: <Events extends EventMap>(options?: BusOptions<Events>) => Bus<Events>;
225
+ //#endregion
226
+ //#region src/actions/action.d.ts
227
+ type Absent<P> = [P] extends [void] ? undefined : P;
228
+ type Made<P, T extends string> = WithPayload<{ [K in T]: Absent<P>; }, T>;
229
+ declare const free: unique symbol;
230
+ type Free = typeof free;
231
+ /**
232
+ * A creator whose payload type was not given: each call has the type of what
233
+ * it is given, and the bus checks it against its event map, so
234
+ * `createAction("user/loaded")` needs no generics and keeps its literal type.
235
+ */
236
+ interface FreeActionCreator<T extends string> {
237
+ (): {
238
+ type: T;
239
+ payload: undefined;
240
+ };
241
+ <P>(payload: P): {
242
+ type: T;
243
+ payload: P;
244
+ };
245
+ readonly type: T;
246
+ /** Tells whether an action is one of this creator. */
247
+ match(action: {
248
+ type: string;
249
+ payload?: unknown;
250
+ }): action is {
251
+ type: T;
252
+ payload: unknown;
253
+ };
254
+ toString(): T;
255
+ }
256
+ /**
257
+ * Makes actions of one type. It also works as the event of that type:
258
+ * `bus.on(creator, fn)` and `bus.emit(creator(payload))`.
259
+ */
260
+ interface ActionCreator<P = void, T extends string = string, A extends unknown[] = PayloadOf<P>> {
261
+ (...args: A): Made<P, T>;
262
+ readonly type: T;
263
+ /** Tells whether an action is one of this creator, and narrows its type. */
264
+ match(action: {
265
+ type: string;
266
+ payload?: unknown;
267
+ }): action is Made<P, T>;
268
+ toString(): T;
269
+ }
270
+ /**
271
+ * Creates an action creator, as in Redux Toolkit.
272
+ *
273
+ * - `createAction("user/loaded")` needs no generics: the payload type comes
274
+ * from the event map of the bus that the action goes to, and the literal
275
+ * type of the action is kept.
276
+ * - `createAction<User>("user/loaded")` gives the payload type, for a bus
277
+ * without an event map or to check the calls of the creator. The type of
278
+ * the action is then `string`; to keep the literal, give it as the second
279
+ * generic: `createAction<User, "user/loaded">("user/loaded")`.
280
+ * - With `prepare` the arguments of the creator can differ from the payload,
281
+ * and both types are inferred.
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * const added = createAction<number>("counter/added");
286
+ * added(2); // { type: "counter/added", payload: 2 }
287
+ * added.type; // "counter/added"
288
+ * added.match(action); // true for actions of this type
289
+ *
290
+ * const renamed = createAction("user/renamed", (id: string, name: string) => ({
291
+ * payload: { id, name },
292
+ * }));
293
+ *
294
+ * bus.emit(added(2));
295
+ * bus.on(added, (n) => console.log(n));
296
+ * ```
297
+ */
298
+ declare function createAction<P = Free, T extends string = string>(type: T): [P] extends [Free] ? FreeActionCreator<T> : ActionCreator<P, T>;
299
+ declare function createAction<A extends unknown[], P, T extends string = string>(type: T, prepare: (...args: A) => {
300
+ payload: P;
301
+ }): ActionCreator<P, T, A>;
302
+ //#endregion
303
+ export { Keys as A, WithPayload as C, anyEventType as D, Tracer as E, TraceEvent as F, TraceLogger as I, UnsubscribeCallback as L, Phase as M, Task as N, EffectName as O, Trace as P, WithContext as S, createBus as T, EventPayload as _, AnyEvent as a, Off as b, Command as c, ContextOf as d, ContextOption as f, EventLike as g, EventKey as h, Action as i, PayloadOf as j, EventMap as k, Commands as l, Emittable as m, FreeActionCreator as n, Bus as o, Emit as p, createAction as r, BusOptions as s, ActionCreator as t, Context as u, Handler as v, WithType as w, On as x, HandlerRef as y };
@@ -0,0 +1,58 @@
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";
3
+ //#region src/actions/job.d.ts
4
+ type Creator = {
5
+ (...args: never[]): {
6
+ type: string;
7
+ payload?: unknown;
8
+ };
9
+ readonly type: string;
10
+ };
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;
21
+ };
22
+ /**
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.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const { command: loadUser, successAction: userLoaded, failAction: userFailed } =
45
+ * createJob("user/load", fetchUser, { success: "user/loaded", fail: "user/failed" });
46
+ *
47
+ * bus.on(userLoaded, (user) => render(user));
48
+ * bus.emit(loadUser, "1"); // userLoaded(user) or userFailed(error) comes later
49
+ * ```
50
+ */
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>;
57
+ //#endregion
58
+ export { type ActionCreator, type Binding, type FreeActionCreator, type Job, type Module, type ModuleApi, type ModuleContext, bindHandler, createAction, createCommand, createCommands, createModule, modulePlugin };
@@ -0,0 +1 @@
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};
@@ -0,0 +1 @@
1
+ import{t as e}from"./onAbort-BgMfspK4.js";const t=2**31-1,n=(e,n)=>{let r={id:null};if(n===1/0)return r;let i=n=>{r.id=setTimeout(()=>{n>t?i(n-t):(r.id=null,e())},Math.min(n,t))};return i(n>0?n:0),r},r=e=>{e.id!==null&&(clearTimeout(e.id),e.id=null)},i=(t,i)=>new Promise((a,o)=>{let s=n(()=>{c(),a()},typeof i==`function`?i():i),c=e(t,e=>{r(s),o(e)})});export{r as n,n as r,i 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};