nanoeffects 0.7.0 → 0.8.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 (47) hide show
  1. package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
  2. package/dist/actions.d.ts +3 -3
  3. package/dist/actions.js +1 -1
  4. package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
  5. package/dist/dev.d.ts +9 -8
  6. package/dist/dev.js +1 -1
  7. package/dist/effects.d.ts +2 -2
  8. package/dist/effects.js +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/match.d.ts +50 -0
  11. package/dist/match.js +1 -0
  12. package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
  13. package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
  14. package/dist/notrace/actions.d.ts +52 -17
  15. package/dist/notrace/actions.js +1 -1
  16. package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
  17. package/dist/notrace/effects.d.ts +2 -2
  18. package/dist/notrace/effects.js +1 -1
  19. package/dist/notrace/index.d.ts +2 -2
  20. package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
  21. package/dist/notrace/policies.d.ts +1 -1
  22. package/dist/notrace/redux.d.ts +1 -1
  23. package/dist/observable.d.ts +1 -1
  24. package/dist/oop.d.ts +131 -0
  25. package/dist/oop.js +1 -0
  26. package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
  27. package/dist/policies.d.ts +1 -1
  28. package/dist/react.d.ts +1 -1
  29. package/dist/redux.d.ts +1 -1
  30. package/dist/scope.d.ts +5 -6
  31. package/dist/trace.d.ts +1 -1
  32. package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
  33. package/docs/README.md +1 -0
  34. package/docs/actions.md +79 -35
  35. package/docs/guide.md +142 -18
  36. package/docs/hmr.md +6 -5
  37. package/docs/migrating-from-effector.md +49 -16
  38. package/docs/migrating-from-rxjs.md +17 -6
  39. package/docs/oop.md +286 -0
  40. package/docs/package.md +2 -0
  41. package/docs/plugins.md +227 -51
  42. package/docs/react.md +17 -5
  43. package/docs/scope.md +60 -22
  44. package/docs/stores.md +263 -109
  45. package/llms-full.txt +1152 -267
  46. package/llms.txt +1 -0
  47. package/package.json +20 -2
package/dist/oop.d.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { B as EventMap, _ as Command, h as Bus, t as ActionCreator } from "./action-creator-BrZF6hXc.js";
2
+ //#region src/oop/types.d.ts
3
+ type Slots = "ctx" | "commands" | "trace" | "addContext" | "applyScope" | "fitScope" | "app" | "use";
4
+ type AnyFunction = (...args: never[]) => unknown;
5
+ /**
6
+ * The methods an instance of a class from `extendBus` gets: the function
7
+ * members of the bus (`on`, `once`, `off`, `emit`, and those of the plugins used
8
+ * before `extendBus`), `dispose`, and the bus itself as `bus`. A member of the
9
+ * class with the same name is kept and not replaced.
10
+ */
11
+ type BusMethods<B> = { [K in keyof B as K extends Slots ? never : B[K] extends AnyFunction ? K : never]: B[K]; } & {
12
+ /** Removes every subscription made through `on` and `once` of this instance. */
13
+ dispose(): void;
14
+ [Symbol.dispose](): void;
15
+ readonly bus: B;
16
+ };
17
+ /** Any class that `extendBus` can extend. */
18
+ type Constructor = new (...args: never[]) => object;
19
+ /**
20
+ * The class `extendBus` returns: it takes the arguments of `C`, its instances
21
+ * are instances of `C` with the methods of `B` that `C` does not have, and its
22
+ * static members are those of `C` plus the bus.
23
+ */
24
+ type Extended<C extends Constructor, B> = {
25
+ new (...args: ConstructorParameters<C>): InstanceType<C> & Omit<BusMethods<B>, keyof InstanceType<C>>;
26
+ readonly bus: B;
27
+ readonly prototype: InstanceType<C>;
28
+ } & Omit<C, "prototype">;
29
+ /** A wrapper for the handler of `@On` and `@Once`, such as `latest`. */
30
+ type Wrap = (handler: (payload: unknown, ctx: unknown) => unknown) => unknown;
31
+ type Handler<P> = (payload: P, ctx: never) => unknown;
32
+ /**
33
+ * What `@On(event)` and `@Once(event)` return: a method decorator of the
34
+ * language and of `experimentalDecorators`. The payload of the method is
35
+ * checked against the event when the event is an action creator or a command.
36
+ */
37
+ interface MethodDecorator<P> {
38
+ (value: Handler<P>, context: ClassMethodDecoratorContext): void;
39
+ (target: object, key: string | symbol, descriptor: TypedPropertyDescriptor<Handler<P>>): void;
40
+ }
41
+ /** The events a decorator takes. */
42
+ type EventOf<P> = ActionCreator<P> | Command<P, unknown>;
43
+ //#endregion
44
+ //#region src/oop/decorators.d.ts
45
+ /**
46
+ * A class decorator that does what `extendBus` does: `@WithBus(bus) class A {}`
47
+ * replaces `A` by `extendBus(bus, A)`, and `@WithBus() class A {}` by
48
+ * `extendBus(A)`. It works for the decorators of the language and for
49
+ * `experimentalDecorators`. TypeScript does not change the type of a decorated
50
+ * class, so declare what it gets next to it:
51
+ * `interface A extends BusMethods<typeof bus> {}`.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * interface SearchService extends BusMethods<typeof bus> {}
56
+ *
57
+ * @WithBus(bus)
58
+ * class SearchService {}
59
+ * ```
60
+ */
61
+ export declare function WithBus<B extends object>(bus: B): <C extends Constructor>(Base: C) => Extended<C, B>;
62
+ export declare function WithBus(): <C extends Constructor>(Base: C) => Extended<C, Bus<EventMap>>;
63
+ /**
64
+ * Makes a method of a class from `extendBus` or `@WithBus` a handler of `event`
65
+ * for each instance, with `this` bound to the instance, from the construction
66
+ * until `dispose()`. `wrap` takes the bound handler and returns the one to
67
+ * subscribe, such as `latest`. An event emitted inside the constructor of the
68
+ * base class does not reach it: the subscription is made right after.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * class SearchServiceBase {
73
+ * @On(queryChanged, latest) // a new query aborts the one before it
74
+ * async search(query: string, { signal }: Context) {}
75
+ * }
76
+ * ```
77
+ */
78
+ export declare function On<P>(event: EventOf<P>, wrap?: Wrap): MethodDecorator<P>;
79
+ export declare function On(event: string, wrap?: Wrap): MethodDecorator<never>;
80
+ /** Like `On`, but the handler runs at most once for an instance. */
81
+ export declare function Once<P>(event: EventOf<P>, wrap?: Wrap): MethodDecorator<P>;
82
+ export declare function Once(event: string, wrap?: Wrap): MethodDecorator<never>;
83
+ //#endregion
84
+ //#region src/oop/extend.d.ts
85
+ /**
86
+ * Makes a class from `Base` whose instances have the methods of a bus, with the
87
+ * subscriptions made through an instance ended by `dispose()`. `new Extended(…)`
88
+ * runs the constructor of `Base`; the class has the name of `Base`, and its
89
+ * instances are instances of `Base`. The members of `Base` win: a method called
90
+ * `emit` or `dispose` in `Base` is kept, and `disposeBus(this)` ends the
91
+ * subscriptions from there.
92
+ *
93
+ * Without a bus, `extendBus(Base)` makes one for the class (`Extended.bus`),
94
+ * shared by its instances. The methods of the plugins used on `bus` before this
95
+ * call are on the instances too; the ones of plugins used later are not.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * class SearchServiceBase {
100
+ * constructor(private api: Api) {
101
+ * this.on("query/changed", (query) => this.load(query)); // ends with dispose()
102
+ * }
103
+ * load(query: string) {}
104
+ * }
105
+ *
106
+ * export const SearchService = extendBus(bus, SearchServiceBase);
107
+ * const service = new SearchService(api);
108
+ * service.dispose(); // its subscriptions are gone
109
+ * ```
110
+ */
111
+ export declare function extendBus<C extends Constructor>(Base: C): Extended<C, Bus<EventMap>>;
112
+ export declare function extendBus<B extends object, C extends Constructor>(bus: B, Base: C): Extended<C, B>;
113
+ //#endregion
114
+ //#region src/oop/subscriptions.d.ts
115
+ /**
116
+ * Removes every subscription an instance made through `on` and `once`, and does
117
+ * nothing for any other value. A class with a `dispose` of its own calls it from
118
+ * there.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * class SearchService {
123
+ * dispose() {
124
+ * disposeBus(this); // the subscriptions of this instance are removed
125
+ * }
126
+ * }
127
+ * ```
128
+ */
129
+ export declare const disposeBus: (instance: unknown) => void;
130
+ //#endregion
131
+ export type { BusMethods, Extended, MethodDecorator };
package/dist/oop.js ADDED
@@ -0,0 +1 @@
1
+ import{n as e}from"./bus-BAs9x7IQ.js";const t=/* @__PURE__ */ new WeakMap,n=e=>typeof e==`function`,r=e=>Reflect.ownKeys(e).flatMap(t=>{let r=Reflect.getOwnPropertyDescriptor(e,t)?.value;return n(r)?[{key:t,value:r}]:[]}),i=/* @__PURE__ */ new WeakMap,a=e=>{let n=i.get(e);if(n)return n;let a=/* @__PURE__ */ new Set,o=[];for(let n=e;n&&n!==Object.prototype;n=Reflect.getPrototypeOf(n))for(let{key:e,value:i}of r(n)){if(e===`constructor`||a.has(e))continue;a.add(e);let n=t.get(i);n&&o.push({key:e,method:i,spec:n})}return i.set(e,o),o},o=/* @__PURE__ */ new WeakMap,s=e=>typeof e==`object`&&!!e||typeof e==`function`,c=e=>{let t=o.get(e);if(t)return t;let n=/* @__PURE__ */ new Set;return o.set(e,n),n},l=e=>{if(typeof e==`string`)return e;if(s(e)){let t=Reflect.get(e,`type`);return typeof t==`string`?t:e}return e},u=(e,t,n)=>Reflect.apply(Reflect.get(e,t),e,n),d=(e,t,n,r)=>{let[i,a,...o]=r,s=c(e),l={event:i,handler:a,off:()=>{}},d=u(t,n,[i,n===`once`?(e,t)=>(s.delete(l),typeof a==`function`?Reflect.apply(a,null,[e,t]):void 0):a,...o]),f=typeof d==`function`?()=>d():()=>{};return l.off=f,s.add(l),()=>{s.delete(l),f()}},f=(e,t,n)=>{let[r,i]=n,a=c(e);r===void 0&&a.clear();for(let e of[...a])Object.is(l(e.event),l(r))&&(i===void 0||e.handler===i)&&(a.delete(e),e.off());return u(t,`off`,n)},p=e=>{if(!s(e))return;let t=o.get(e);if(t){o.delete(e);for(let e of t)e.off()}},m=/* @__PURE__ */ new Set([`on`,`once`,`off`,`emit`,`ctx`,`commands`,`trace`,`addContext`,`applyScope`,`fitScope`,`app`,`use`]),h=Reflect.get(Symbol,`dispose`),g=e=>typeof e==`function`,_=(e,t,n,r)=>{n in t||Object.defineProperty(e,n,{configurable:!0,enumerable:!1,...r})},v=/* @__PURE__ */ new WeakMap,y=(e,t)=>{let n=v.get(e);if(n)return n;let r={on:(...n)=>d(e,t,`on`,n),once:(...n)=>d(e,t,`once`,n),off:(...n)=>f(e,t,n)};return v.set(e,r),r},b=(e,t)=>(...n)=>Reflect.apply(Reflect.get(e,t),e,n),x=(e,t)=>{for(let{method:n,spec:r}of a(Reflect.getPrototypeOf(e)??{})){let i=(t,r)=>Reflect.apply(n,e,[t,r]),a=r.wrap?r.wrap(i):i;d(e,t,r.once?`once`:`on`,[r.event,a])}},S=(e,t,n)=>{for(let r of[`on`,`once`,`off`])_(e,t,r,{get(){return y(this,n)[r]}});_(e,t,`emit`,{value:b(n,`emit`)});for(let[r,i]of Object.entries(n))typeof i==`function`&&!m.has(r)&&_(e,t,r,{value:b(n,r)});let r=function(){p(this)};_(e,t,`dispose`,{value:r}),h&&_(e,t,h,{value:r}),_(e,t,`bus`,{get:()=>n})},C=(e,t)=>{let n=function(...r){let i=Reflect.construct(t,r,new.target??n);return x(i,e),i};return Object.setPrototypeOf(n,t),n.prototype=Object.create(t.prototype,{constructor:{value:n,writable:!0,configurable:!0}}),Object.defineProperty(n,"name",{value:t.name,configurable:!0}),`bus`in t||Object.defineProperty(n,"bus",{value:e,configurable:!0}),S(n.prototype,t.prototype,e),n};function w(t,n){if(g(t))return C(e(),t);if(!n)throw TypeError(`extendBus: pass the class to extend`);return C(t,n)}function T(e){return t=>e?w(e,t):w(t)}const E=e=>{let[t,n,r]=e;return typeof n==`string`||typeof n==`symbol`?Reflect.get(Object(r),`value`):t},D=(e,n,r)=>((...i)=>{let a=E(i);typeof a==`function`&&t.set(a,{event:e,wrap:n,once:r})});function O(e,t){return D(e,t,!1)}function k(e,t){return D(e,t,!0)}export{O as On,k as Once,T as WithBus,p as disposeBus,w as extendBus};
@@ -1,5 +1,5 @@
1
- import { B as EventMap, N as WithContext, V as Keys, b as Context, d as Effects, h as Bus, l as BoundForm } from "./action-creator-Bd4TBZgl.js";
2
- import { n as NoClash } from "./context-plugin-BVNKqkIx.js";
1
+ import { B as EventMap, N as WithContext, V as Keys, b as Context, d as Effects, h as Bus, l as BoundForm } from "./action-creator-BrZF6hXc.js";
2
+ import { n as NoClash } from "./context-plugin--HkNN1ha.js";
3
3
  //#region src/utils/delay.d.ts
4
4
  /**
5
5
  * Resolves after `ms`, or rejects early if `ctx.signal` aborts. `ms` can be a
@@ -1,4 +1,4 @@
1
- import { B as EventMap, W as Task } from "./action-creator-Bd4TBZgl.js";
1
+ import { B as EventMap, W as Task } from "./action-creator-BrZF6hXc.js";
2
2
  //#region src/policies/keyed.d.ts
3
3
  /** Options shared by every policy that can keep its state per key. */
4
4
  type KeyOption<P> = {
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { B as EventMap, T as EventKey, h as Bus, r as FreeActionCreator, t as ActionCreator } from "./action-creator-Bd4TBZgl.js";
1
+ import { B as EventMap, T as EventKey, h as Bus, r as FreeActionCreator, t as ActionCreator } from "./action-creator-BrZF6hXc.js";
2
2
  //#region src/react/plugin.d.ts
3
3
  /** What `reactPlugin` adds to the bus. */
4
4
  /** Tells whether two values are the same: `Object.is` unless the caller gives another. */
package/dist/redux.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { B as EventMap, h as Bus } from "./action-creator-Bd4TBZgl.js";
1
+ import { B as EventMap, h as Bus } from "./action-creator-BrZF6hXc.js";
2
2
  //#region src/store/redux/middleware.d.ts
3
3
  /** What Redux passes through a middleware: an action is a plain object with a `type`. */
4
4
  type ReduxAction = {
package/dist/scope.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as EventMap, a as EmitScope, d as Effects, f as Signaled, h as Bus, o as Scope, u as Effect } from "./action-creator-Bd4TBZgl.js";
2
- import { n as Scoped } from "./types-BJ0fzV1p.js";
1
+ import { B as EventMap, a as EmitScope, d as Effects, f as Signaled, h as Bus, o as Scope, u as Effect } from "./action-creator-BrZF6hXc.js";
2
+ import { n as ScopeSlots, r as Scoped } from "./types-KavbvHix.js";
3
3
  //#region src/scope/effect.d.ts
4
4
  /**
5
5
  * Marks a function whose first parameter is `{ signal }` as an effect, so that
@@ -35,9 +35,8 @@ export declare function effect<A extends unknown[], R>(fn: (ctx: Signaled, ...ar
35
35
  * Fields of the library (`signal`, `emit`, `run`, …) are never replaced. Returns
36
36
  * the same bus.
37
37
  *
38
- * Use it after `effectsPlugin`, which assigns its effects over those of a scope
39
- * when it comes later, and before `modulePlugin`, which mounts its modules when
40
- * it is used. Without this plugin a scope is ignored.
38
+ * It works in any order with the other plugins. Without this plugin a scope is
39
+ * ignored.
41
40
  *
42
41
  * @example
43
42
  * ```ts
@@ -53,6 +52,6 @@ export declare function effect<A extends unknown[], R>(fn: (ctx: Signaled, ...ar
53
52
  * bus.emit(getUser("1"), { http: fakeHttp }); // this call only
54
53
  * ```
55
54
  */
56
- export declare const scopePlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus<E>;
55
+ export declare const scopePlugin: () => <E extends EventMap>(bus: Bus<E> & ScopeSlots) => Bus<E> & ScopeSlots;
57
56
  //#endregion
58
57
  export type { Effect, Effects, EmitScope, Scope, Scoped, Signaled };
package/dist/trace.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Trace, K as TraceEvent, L as Tracer, q as TraceLogger } from "./action-creator-Bd4TBZgl.js";
1
+ import { G as Trace, K as TraceEvent, L as Tracer, q as TraceLogger } from "./action-creator-BrZF6hXc.js";
2
2
  //#region src/trace/console.d.ts
3
3
  type ConsoleLoggerOptions = {
4
4
  /** Defaults to `globalThis.console`. */
@@ -1,6 +1,16 @@
1
- import { B as EventMap, d as Effects, o as Scope, s as WithFields } from "./action-creator-Bd4TBZgl.js";
2
- import { t as BoundOf } from "./plugin-DQNPBcxx.js";
1
+ import { B as EventMap, d as Effects, o as Scope, s as WithFields } from "./action-creator-BrZF6hXc.js";
2
+ import { t as BoundOf } from "./plugin-DkXCvsmB.js";
3
3
  //#region src/scope/types.d.ts
4
+ /**
5
+ * The slots `scopePlugin` fills for `modulePlugin`. They are not on `Bus`: a bus
6
+ * has them in its type once the plugin returned it.
7
+ */
8
+ interface ScopeSlots {
9
+ /** Puts the fields of a module's scope on the context of one of its tasks. Without it a module's scope is ignored. */
10
+ applyScope?: ((ctx: object, scope: Scope) => void) | undefined;
11
+ /** Wraps a handler so it runs with the fields of a module's scope. */
12
+ fitScope?: ((scope: Scope, fn: unknown) => unknown) | undefined;
13
+ }
4
14
  type BoundEffects<E extends EventMap, S extends Scope> = S extends {
5
15
  $effects: infer H extends Effects;
6
16
  } ? BoundOf<H, E> : unknown;
@@ -12,4 +22,4 @@ type FieldsOfScope<E extends EventMap, S extends Scope> = [keyof S] extends [nev
12
22
  */
13
23
  type Scoped<E extends EventMap, S extends Scope> = WithFields<E, FieldsOfScope<E, S>>;
14
24
  //#endregion
15
- export { Scoped as n, FieldsOfScope as t };
25
+ export { ScopeSlots as n, Scoped as r, FieldsOfScope as t };
package/docs/README.md CHANGED
@@ -27,6 +27,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
27
27
  - [Actions, commands, jobs and modules](actions.md)
28
28
  - [Sign-up form in React](react.md) — one form: races, double submit, retry
29
29
  - [Context fields](context.md) — store or client on every task
30
+ - [Use the bus from a class](oop.md) — `extendBus`, `@WithBus`, `@On`, and `dispose()` for the subscriptions of an instance
30
31
  - [TypeScript in many files](typescript.md) — keep the types of the bus in commands and modules
31
32
  - [Errors and unheard events](errors.md)
32
33
  - [Package entries](package.md) — which import holds what
package/docs/actions.md CHANGED
@@ -8,14 +8,28 @@ Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
8
8
 
9
9
  ## Return a value from `emit` with a command
10
10
 
11
+ The bus lives in one file and its events in another, so any module can import both without a cycle.
12
+
11
13
  ```js
14
+ // bus.js
12
15
  import { createBus } from "nanoeffects";
13
- import { createCommand } from "nanoeffects/actions";
14
16
 
15
- const bus = createBus();
17
+ // createBus — one bus for the app
18
+ export const bus = createBus();
19
+ ```
20
+
21
+ ```js
22
+ // math.js
23
+ import { createCommand } from "nanoeffects/actions";
16
24
 
17
25
  // createCommand — emit runs the handler and returns its result
18
- const double = createCommand("math/double", (value) => value * 2);
26
+ export const double = createCommand("math/double", (value) => value * 2);
27
+ ```
28
+
29
+ ```js
30
+ import { bus } from "./bus";
31
+ import { double } from "./math";
32
+
19
33
  bus.emit(double(21)); // 42
20
34
  ```
21
35
 
@@ -24,27 +38,30 @@ bus.emit(double(21)); // 42
24
38
  `nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
25
39
 
26
40
  ```js
41
+ // user.js
27
42
  import { createAction, createCommand, createJob } from "nanoeffects/actions";
28
43
 
29
44
  // createAction — event with a payload (like RTK createAction)
30
- const userLoaded = createAction("user/loaded");
31
- const loadFailed = createAction("user/failed");
45
+ export const userLoaded = createAction("user/loaded");
46
+ export const loadFailed = createAction("user/failed");
32
47
 
33
48
  // createCommand with a handler — emit returns what the handler returns
34
- const getUser = createCommand("user/get", (id, { signal }) => {
49
+ export const getUser = createCommand("user/get", (id, { signal }) => {
35
50
  return fetchUser(id, { signal });
36
51
  });
37
52
 
38
53
  // createCommand without a handler — a contract; register the work elsewhere
39
- const sendMail = createCommand("mail/send");
54
+ export const sendMail = createCommand("mail/send");
40
55
 
41
56
  // createJob — command that also emits success / fail actions
42
- const loadUser = createJob("user/load", fetchUser, {
57
+ export const loadUser = createJob("user/load", fetchUser, {
43
58
  success: userLoaded,
44
59
  fail: "user/failed",
45
60
  });
46
61
  ```
47
62
 
63
+ These are the events of the user domain, in a file of their own (`user.js`). The bus is made in another file (`bus.js` below), and neither imports the other.
64
+
48
65
  ### Actions
49
66
 
50
67
  `createAction(type)` returns a creator. Without generics it keeps the literal type; the bus checks the payload against its event map. Without an event map, give the payload type: `createAction<User>("user/loaded")`.
@@ -65,22 +82,22 @@ Calling a command builds `{ type, payload }` like `createAction`. `bus.emit(comm
65
82
  **Where handlers are registered.** The command may carry a handler. The bus can also take handlers from a registry (`commandsPlugin`). A handler bound there wins over the command’s own. Each bus (each test) has its own registry.
66
83
 
67
84
  ```js
85
+ // bus.js
68
86
  import { createBus } from "nanoeffects";
69
- import {
70
- bindHandler,
71
- commandsPlugin,
72
- createCommand,
73
- } from "nanoeffects/actions";
74
-
75
- const sendMail = createCommand("mail/send");
76
- const getUser = createCommand("user/get");
87
+ import { bindHandler, commandsPlugin } from "nanoeffects/actions";
88
+ import { sendMail } from "./user";
77
89
 
78
- const bus = createBus();
90
+ export const bus = createBus();
79
91
 
80
92
  // commandsPlugin — registry of handlers for this bus
81
- const commands = bus.use(
93
+ export const commands = bus.use(
82
94
  commandsPlugin([bindHandler(sendMail, smtpSend)]),
83
95
  );
96
+ ```
97
+
98
+ ```js
99
+ import { commands } from "./bus";
100
+ import { getUser } from "./user";
84
101
 
85
102
  // later, e.g. in a test — latest binding wins
86
103
  const unbind = commands.handle(getUser, async () => fakeUser);
@@ -89,7 +106,7 @@ unbind();
89
106
 
90
107
  A command with no handler anywhere throws `No handler for command "..."` from `emit` (the call itself only builds the action).
91
108
 
92
- The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second plugin on the same bus replaces it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
109
+ The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second `commandsPlugin` binds its list in that registry and returns it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
93
110
 
94
111
  ### Jobs
95
112
 
@@ -112,18 +129,20 @@ The registry lives in `nanoeffects/actions`, so a bus without one carries none o
112
129
  `bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `commands`, and `createModule`: the same as `createModule` below, with the context fields of the bus typed (see [TypeScript](typescript.md)).
113
130
 
114
131
  ```js
115
- import { createBus } from "nanoeffects";
116
- import {
117
- createCommand,
118
- createModule,
119
- modulePlugin,
120
- } from "nanoeffects/actions";
132
+ // user.js
133
+ import { createCommand } from "nanoeffects/actions";
121
134
 
122
- const getUser = createCommand("user/get");
123
- const saveUser = createCommand("user/save", persistUser);
135
+ export const getUser = createCommand("user/get");
136
+ export const saveUser = createCommand("user/save", persistUser);
137
+ ```
138
+
139
+ ```js
140
+ // user-module.js
141
+ import { createModule } from "nanoeffects/actions";
142
+ import { getUser, saveUser } from "./user";
124
143
 
125
144
  // createModule — one domain; unmount undoes everything below
126
- const userModule = createModule("user", (m) => {
145
+ export const userModule = createModule("user", (m) => {
127
146
  m.handle(getUser, fetchUser);
128
147
  m.use(saveUser, authModule);
129
148
  m.on("login", (user, ctx) => {
@@ -133,10 +152,22 @@ const userModule = createModule("user", (m) => {
133
152
  /* background work; ends when the module unmounts */
134
153
  });
135
154
  });
155
+ ```
156
+
157
+ ```js
158
+ // bus.js
159
+ import { createBus } from "nanoeffects";
160
+ import { modulePlugin } from "nanoeffects/actions";
161
+ import { userModule } from "./user-module";
136
162
 
137
- const bus = createBus();
163
+ export const bus = createBus();
138
164
  // modulePlugin — mount modules on the bus
139
- const app = bus.use(modulePlugin([userModule]));
165
+ export const app = bus.use(modulePlugin([userModule]));
166
+ ```
167
+
168
+ ```js
169
+ import { app } from "./bus";
170
+ import { getUser } from "./user";
140
171
 
141
172
  const unmount = app.mount(orderModule); // later, e.g. after a lazy import
142
173
  app.commands.handle(getUser, fakeUser); // in a test
@@ -147,6 +178,7 @@ app.dispose(); // unmounts every module
147
178
  - **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
148
179
  - **Registry.** If `bus.use(commandsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.commands` and `app.commands`.
149
180
  - A disposed app cannot mount modules.
181
+ - **The given modules are mounted on the first use** of the app or the bus (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `commands`), so the plugins you use after `modulePlugin` still reach them. The `bus.on` calls made in between are held and made right after, so the listeners of a module stay ahead of them. See [using plugins in any order](plugins.md#use-them-in-any-order).
150
182
  - Module context = task context plus `handle`, `use`, and `spawn`:
151
183
  - `handle` — give a command a handler (contract, or override).
152
184
  - `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
@@ -158,17 +190,29 @@ app.dispose(); // unmounts every module
158
190
  A command can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createCommand`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
159
191
 
160
192
  ```js
193
+ // bus.js
161
194
  import { createBus } from "nanoeffects";
162
- import { createCommand } from "nanoeffects/actions";
163
195
  import { scopePlugin } from "nanoeffects/scope";
164
196
 
165
197
  // scopePlugin — makes the scope of a command work on this bus
166
- const bus = createBus().use(scopePlugin());
198
+ export const bus = createBus().use(scopePlugin());
199
+ ```
200
+
201
+ ```js
202
+ // user.js
203
+ import { createCommand } from "nanoeffects/actions";
167
204
 
168
205
  // the third argument — fields of this command only
169
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
170
- api,
171
- });
206
+ export const getUser = createCommand(
207
+ "user/get",
208
+ (id, { api }) => api.getUser(id),
209
+ { api },
210
+ );
211
+ ```
212
+
213
+ ```js
214
+ import { bus } from "./bus";
215
+ import { getUser } from "./user";
172
216
 
173
217
  // a test replaces them for one call
174
218
  const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
package/docs/guide.md CHANGED
@@ -69,6 +69,74 @@ unbind();
69
69
  - An event that only `"*"` listens to counts as heard, so `onUnhandled` does not fire for it.
70
70
  - `bus.off("*")` removes every `"*"` listener; `bus.off()` clears the whole bus, `"*"` included.
71
71
 
72
+ ### Pick events with a function
73
+
74
+ To listen to a group of events, such as every `user/*` event, `"*"` and a check of `ctx.type` do the job but leave the check to you. `matchPlugin` lets `on` take a function that picks the events. The function gets `{ type, payload }` of each event, and the handler runs when it returns true.
75
+
76
+ <!-- docs-check: new -->
77
+
78
+ ```js
79
+ // bus.js
80
+ import { createBus } from "nanoeffects";
81
+ import { matchPlugin } from "nanoeffects/match";
82
+
83
+ // matchPlugin — lets on, once and off take a function that picks the events
84
+ export const bus = createBus().use(matchPlugin());
85
+ ```
86
+
87
+ ```js
88
+ // user.js
89
+ import { createAction } from "nanoeffects/actions";
90
+
91
+ export const userLoaded = createAction("user/loaded");
92
+ export const userFailed = createAction("user/failed");
93
+ ```
94
+
95
+ ```js
96
+ // user-handlers.js
97
+ import { bus } from "./bus";
98
+
99
+ // the function picks the events; the handler gets the payload and the context of each
100
+ bus.on(
101
+ (action) => action.type.startsWith("user/"),
102
+ (payload, ctx) => {
103
+ console.log(ctx.type, payload);
104
+ },
105
+ );
106
+ ```
107
+
108
+ ```js
109
+ import { bus } from "./bus";
110
+ import { userFailed, userLoaded } from "./user";
111
+
112
+ bus.emit(userLoaded({ id: "1" })); // user/loaded { id: "1" }
113
+ bus.emit("order/placed", 5); // not picked
114
+ bus.emit(userFailed("boom")); // user/failed boom
115
+ ```
116
+
117
+ - `once(fn, handler)` waits for the first event that `fn` accepts. `off(fn)` removes every handler of `fn`, and `off(fn, handler)` removes one.
118
+ - An action creator or a command still means that one event: only a function with no `type` is a function that picks events.
119
+ - The cost: the handler listens to `"*"`, so each event makes one task for each such subscription, also when the function says no, and an event that only such a subscription listens to counts as heard for `onUnhandled`. For a few known events, `on(creator, handler)` is cheaper.
120
+ - It works in any order with the other plugins, `devPlugin` included.
121
+
122
+ In TypeScript a function that is a type guard narrows the payload of the handler:
123
+
124
+ ```ts
125
+ import { bus } from "./bus";
126
+
127
+ type User = { id: string };
128
+
129
+ // the guard says which event this is, and the handler gets its payload typed
130
+ const isLoaded = (action: {
131
+ type: string;
132
+ payload: unknown;
133
+ }): action is { type: "user/loaded"; payload: User } => action.type === "user/loaded";
134
+
135
+ bus.on(isLoaded, (user) => console.log(user.id)); // user: User
136
+ ```
137
+
138
+ `ctx.on` takes such a function at run time too, but its type does not know it yet.
139
+
72
140
  ## 2. Use the handler context
73
141
 
74
142
  The second argument is the **task** context for this call. Emit further events through it. Pass `ctx.signal` to `fetch` when you cancel work.
@@ -225,22 +293,42 @@ More in [Effects](effects.md).
225
293
 
226
294
  `emit` and `on` take a string, an object `{ type, payload }` (a Redux action fits), or a function with a `type` property (action creators and commands).
227
295
 
296
+ From here on the bus is kept in one file and the events in files named after their domain (`move.js`, `math.js`), as in a real app: the events can then be imported anywhere without importing the bus.
297
+
228
298
  ```js
299
+ // bus.js
229
300
  import { createBus } from "nanoeffects";
230
- import { createAction, createCommand } from "nanoeffects/actions";
231
301
 
232
- const bus = createBus();
302
+ export const bus = createBus();
303
+ ```
304
+
305
+ ```js
306
+ // move.js
307
+ import { createAction } from "nanoeffects/actions";
308
+
309
+ // createAction — names an event; calling it builds { type, payload }
310
+ export const moved = createAction("moved");
311
+ ```
312
+
313
+ ```js
314
+ // math.js
315
+ import { createCommand } from "nanoeffects/actions";
316
+
317
+ // createCommand — like an action, but emit runs a handler and returns its result
318
+ export const double = createCommand("math/double", (value) => value * 2);
319
+ ```
320
+
321
+ ```js
322
+ import { bus } from "./bus";
323
+ import { moved } from "./move";
324
+ import { double } from "./math";
233
325
 
234
326
  bus.emit("moved", { x: 1, y: 2 });
235
327
  bus.emit({ type: "moved", payload: { x: 2, y: 2 } });
236
328
 
237
- // createAction — names an event; calling it builds { type, payload }
238
- const moved = createAction("moved");
239
329
  bus.emit(moved({ x: 3, y: 2 }));
240
330
  bus.on(moved, ({ x }) => console.log(x));
241
331
 
242
- // createCommand — like an action, but emit runs a handler and returns its result
243
- const double = createCommand("math/double", (value) => value * 2);
244
332
  bus.emit(double(21)); // 42
245
333
  ```
246
334
 
@@ -251,14 +339,25 @@ Use strings for the plain case. Use objects when other code already builds actio
251
339
  A command may carry its handler, or only be a **contract** (name + types) with the work registered elsewhere — another file, or a fake in a test.
252
340
 
253
341
  ```js
254
- import { createBus } from "nanoeffects";
255
- import { createCommand, commandsPlugin } from "nanoeffects/actions";
342
+ // user.js
343
+ import { createCommand } from "nanoeffects/actions";
256
344
 
257
- const getUser = createCommand("user/get"); // contract — no handler yet
345
+ export const getUser = createCommand("user/get"); // contract — no handler yet
346
+ ```
258
347
 
259
- const bus = createBus();
348
+ ```js
349
+ // bus.js
350
+ import { createBus } from "nanoeffects";
351
+ import { commandsPlugin } from "nanoeffects/actions";
352
+
353
+ export const bus = createBus();
260
354
  // commandsPlugin — registry of handlers for this bus
261
- const commands = bus.use(commandsPlugin());
355
+ export const commands = bus.use(commandsPlugin());
356
+ ```
357
+
358
+ ```js
359
+ import { bus, commands } from "./bus";
360
+ import { getUser } from "./user";
262
361
 
263
362
  bus.emit(getUser("1")); // throws: No handler for command "user/get"
264
363
 
@@ -279,16 +378,27 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
279
378
  **What you do.** `createModule(name, setup)` describes the feature. `bus.use(modulePlugin([…]))` mounts it. `app.unmount(module)` undoes everything the setup registered.
280
379
 
281
380
  ```js
282
- import { createBus } from "nanoeffects";
283
- import { createCommand, createModule, modulePlugin } from "nanoeffects/actions";
381
+ // user.js
382
+ import { createCommand } from "nanoeffects/actions";
284
383
 
285
- const getUser = createCommand("user/get"); // contract — no handler yet
286
- const double = createCommand("math/double", (value) => value * 2); // carries its handler
384
+ export const getUser = createCommand("user/get"); // contract — no handler yet
385
+ ```
287
386
 
288
- const bus = createBus();
387
+ ```js
388
+ // math.js
389
+ import { createCommand } from "nanoeffects/actions";
390
+
391
+ export const double = createCommand("math/double", (value) => value * 2); // carries its handler
392
+ ```
393
+
394
+ ```js
395
+ // greeting-module.js
396
+ import { createModule } from "nanoeffects/actions";
397
+ import { getUser } from "./user";
398
+ import { double } from "./math";
289
399
 
290
400
  // createModule — one feature; name is stable for hot reload replace
291
- const greeting = createModule("greeting", (m) => {
401
+ export const greeting = createModule("greeting", (m) => {
292
402
  // m.on — like bus.on, removed automatically on unmount
293
403
  m.on("greet", (name) => console.log(`hello ${name}`));
294
404
 
@@ -298,9 +408,23 @@ const greeting = createModule("greeting", (m) => {
298
408
  // m.use — register a command that already has a handler, for this module’s life
299
409
  m.use(double);
300
410
  });
411
+ ```
412
+
413
+ ```js
414
+ // bus.js
415
+ import { createBus } from "nanoeffects";
416
+ import { modulePlugin } from "nanoeffects/actions";
417
+ import { greeting } from "./greeting-module";
301
418
 
419
+ export const bus = createBus();
302
420
  // modulePlugin — mount modules on the bus
303
- const app = bus.use(modulePlugin([greeting]));
421
+ export const app = bus.use(modulePlugin([greeting]));
422
+ ```
423
+
424
+ ```js
425
+ import { app, bus } from "./bus";
426
+ import { greeting } from "./greeting-module";
427
+
304
428
  bus.emit("greet", "Ada"); // hello Ada
305
429
  app.unmount(greeting); // greet listener and getUser binding are gone
306
430
  ```