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.
- package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
- package/dist/dev.d.ts +9 -8
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
- package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
- package/dist/notrace/actions.d.ts +52 -17
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +131 -0
- package/dist/oop.js +1 -0
- package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +5 -6
- package/dist/trace.d.ts +1 -1
- package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
- package/docs/README.md +1 -0
- package/docs/actions.md +79 -35
- package/docs/guide.md +142 -18
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +49 -16
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/oop.md +286 -0
- package/docs/package.md +2 -0
- package/docs/plugins.md +227 -51
- package/docs/react.md +17 -5
- package/docs/scope.md +60 -22
- package/docs/stores.md +263 -109
- package/llms-full.txt +1152 -267
- package/llms.txt +1 -0
- 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-
|
|
2
|
-
import { n as NoClash } from "./context-plugin
|
|
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
|
package/dist/policies.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as EventMap, W as Task } from "./action-creator-
|
|
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-
|
|
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-
|
|
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-
|
|
2
|
-
import { n as Scoped } from "./types-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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-
|
|
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-
|
|
2
|
-
import { t as BoundOf } from "./plugin-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
170
|
-
|
|
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
|
-
|
|
255
|
-
import { createCommand
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
import { createCommand
|
|
381
|
+
// user.js
|
|
382
|
+
import { createCommand } from "nanoeffects/actions";
|
|
284
383
|
|
|
285
|
-
const getUser = createCommand("user/get"); // contract — no handler yet
|
|
286
|
-
|
|
384
|
+
export const getUser = createCommand("user/get"); // contract — no handler yet
|
|
385
|
+
```
|
|
287
386
|
|
|
288
|
-
|
|
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
|
```
|