nanoeffects 0.33.0 → 0.35.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/README.md +9 -1
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/dev.d.ts +1 -1
- package/dist/{module-DPKjzfRk.d.ts → module-Fk4OtkZk.d.ts} +10 -27
- package/dist/notrace/actions.d.ts +10 -27
- package/dist/notrace/actions.js +1 -1
- package/dist/signal.d.ts +11 -10
- package/dist/signal.js +1 -1
- package/docs/README.md +29 -2
- package/docs/actions.md +13 -7
- package/docs/cancel-stale-requests.md +1 -1
- package/docs/context.md +1 -1
- package/docs/guide.md +6 -6
- package/docs/package.md +1 -1
- package/docs/pass-the-signal.md +45 -17
- package/docs/policies.md +1 -1
- package/docs/react.md +41 -0
- package/docs/ru/README.md +29 -2
- package/docs/ru/actions.md +13 -7
- package/docs/ru/cancel-stale-requests.md +1 -1
- package/docs/ru/context.md +1 -1
- package/docs/ru/guide.md +6 -6
- package/docs/ru/package.md +1 -1
- package/docs/ru/pass-the-signal.md +46 -18
- package/docs/ru/policies.md +1 -1
- package/docs/ru/react.md +41 -0
- package/docs/ru/svelte.md +41 -0
- package/docs/ru/vue.md +45 -0
- package/docs/svelte.md +41 -0
- package/docs/vue.md +45 -0
- package/llms-full.txt +195 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,15 @@ Move business logic and side effects out of React, Vue and Svelte components int
|
|
|
13
13
|
npm install nanoeffects
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add nanoeffects
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
yarn add nanoeffects
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The package is ESM only (use `import`, not `require`), has no dependencies and includes its types. It runs on Node 18 or newer and in any bundler.
|
|
17
25
|
|
|
18
26
|
New project: `npm create nanoeffects@latest my-app` ([Docs](docs/starter.md)). Try it first: [playground](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground) in your browser, no install.
|
|
19
27
|
|
package/dist/actions.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { G as PayloadOf, H as EventMap, T as EventKey, a as FreeActionCreator, c as Scope, i as Free, n as Call, q as Task, t as ActionCreator, u as WithScope, v as Bus } from "./action-creator-DprJV-pN.js";
|
|
2
2
|
import { r as Scoped, t as FieldsOfScope } from "./types-DxPZDzZG.js";
|
|
3
|
-
import { a as createModule, c as
|
|
3
|
+
import { a as createModule, c as MadeHandled, l as ScopedHandled, n as ModuleApi, o as modulePlugin, r as ModuleContext, s as CarryingCalls, t as Module, u as actionsPlugin } from "./module-Fk4OtkZk.js";
|
|
4
4
|
import { t as OriginOptions } from "./origin-key-CxK0SkeG.js";
|
|
5
5
|
//#region src/actions/action.d.ts
|
|
6
6
|
type Reserved = "handle" | "prepare";
|
|
@@ -194,4 +194,4 @@ export declare const createJob: <P = void, R = void, T extends string = string,
|
|
|
194
194
|
/** Context values and `$effects` for the handler and for the listeners of the outcome; see `scopePlugin`. */
|
|
195
195
|
scope?: X) => CarryingCalls<Job<P, R, T, S, F, PE, CE>, P, Promise<Awaited<R> | undefined>, FieldsOfScope<E, X>>;
|
|
196
196
|
//#endregion
|
|
197
|
-
export { type ActionCreator, type
|
|
197
|
+
export { type ActionCreator, type EmitterApi, type FreeActionCreator, type Job, type Module, type ModuleApi, type ModuleContext, actionsPlugin, createModule, modulePlugin };
|
package/dist/actions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e,t}from"./caller-DULg_ZYV.js";import{n,t as r}from"./onAbort-BgMfspK4.js";import{i,n as a}from"./context-Dp_QRXzb.js";import{t as o}from"./emit-key-C6FL7Ewn.js";import{t as s}from"./event-DVcW0mME.js";import{r as c}from"./bus-Cmk4U1hQ.js";import{n as l,t as u}from"./lifecycle-key-8iX8RUVD.js";import{t as d}from"./origin-key-BbWN3gmN.js";import{t as f}from"./scope-key-B7l7mUS5.js";import{t as p}from"./mount-each-D54UqAhZ.js";const m=(e,t)=>(t&&Object.defineProperty(e,f,{value:t}),e),h=(e,t)=>({...e,...t,$effects:{...e.$effects,...t.$effects}}),g=e=>Reflect.get(e,f),_=e=>{let t=Object.assign(n=>c(t,e,n),{type:e});return t},v=e=>typeof e==`object`&&!!e&&Reflect.get(e,`signal`)instanceof AbortSignal&&typeof Reflect.get(e,`emit`)==`function`;var y=class extends Error{};function b(e,t,n){return x(e,t,n)}function x(e,t,n,r){let i=t=>t.type===e,a=e=>r?e.length===2&&v(e[1]):typeof e[1]==`object`;function o(...i){let[o,s]=i;if(a(i)){if(!t)throw new y(`No handler for action "${e}"`);return t(o,s)}let u=r&&Reflect.apply(r,null,i),d=u?u.payload:o;return m(c(l,e,d),n)}let s=Object.assign(o,{type:e,match:i,toString:()=>e}),l=m(s,n);return l}
|
|
1
|
+
import{i as e,t}from"./caller-DULg_ZYV.js";import{n,t as r}from"./onAbort-BgMfspK4.js";import{i,n as a}from"./context-Dp_QRXzb.js";import{t as o}from"./emit-key-C6FL7Ewn.js";import{t as s}from"./event-DVcW0mME.js";import{r as c}from"./bus-Cmk4U1hQ.js";import{n as l,t as u}from"./lifecycle-key-8iX8RUVD.js";import{t as d}from"./origin-key-BbWN3gmN.js";import{t as f}from"./scope-key-B7l7mUS5.js";import{t as p}from"./mount-each-D54UqAhZ.js";const m=(e,t)=>(t&&Object.defineProperty(e,f,{value:t}),e),h=(e,t)=>({...e,...t,$effects:{...e.$effects,...t.$effects}}),g=e=>Reflect.get(e,f),_=e=>{let t=Object.assign(n=>c(t,e,n),{type:e});return t},v=e=>typeof e==`object`&&!!e&&Reflect.get(e,`signal`)instanceof AbortSignal&&typeof Reflect.get(e,`emit`)==`function`;var y=class extends Error{};function b(e,t,n){return x(e,t,n)}function x(e,t,n,r){let i=t=>t.type===e,a=e=>r?e.length===2&&v(e[1]):typeof e[1]==`object`;function o(...i){let[o,s]=i;if(a(i)){if(!t)throw new y(`No handler for action "${e}"`);return t(o,s)}let u=r&&Reflect.apply(r,null,i),d=u?u.payload:o;return m(c(l,e,d),n)}let s=Object.assign(o,{type:e,match:i,toString:()=>e}),l=m(s,n);return l}const S=e=>typeof Reflect.get(e??{},`handle`)==`function`,C=()=>e=>{let t=e.actions;if(S(t))return t;let n=/* @__PURE__ */ new Map,r=(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 i(e,t){return r(s(e),t)}let a={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:i,bound:e=>n.has(e)?_(e):void 0};return e.actions=a,a};function w(e,t,n){let{handle:r,prepare:i,scope:a}=T(typeof t==`function`?n:t),o=typeof t==`function`?t:r;return o?x(e,o,a,i):Object.assign(m((...t)=>{let n=i&&Reflect.apply(i,null,t);return m({type:e,payload:n?n.payload:t[0]},a)},a),{type:e,match:t=>t.type===e,toString:()=>e})}const T=e=>{if(!e)return{};let{handle:t,prepare:n,...r}=e,i=typeof t==`function`,a=typeof n==`function`;if(!i&&!a)return{scope:e};let o=Object.keys(r).length>0?r:void 0;return{...i&&{handle:t},...a&&{prepare:n},...o&&{scope:o}}},E=()=>e=>{function t(t,n){return(...r)=>{let i=t(...r),a=Reflect.get(e,d);a?a(i,n):e.emit(i)}}return Object.assign(e,{emitter:t})},D=e=>typeof e==`string`?w(e):e,O=(e,t,n,r)=>{let i=g(t);e.emit(Reflect.apply(t,null,[n]),r&&i?h(r,i):r)},k=(e,t,r,i)=>{let a=e;a[o]=!0;try{O(e,t,r,i)}catch(e){n(`cancel listener`,e)}a[o]=!1},A=e=>{try{return Promise.resolve(e())}catch(e){if(e instanceof y)throw e;return Promise.reject(e)}},j=(e,t,i={},a)=>{let o=b(e,t),s=t=>D(i[t]??`${e}/${t}`),d=s(`pending`),f=s(`success`),p=s(`fail`),h=s(`cancel`),g=(e,t,i,o)=>{let s=i,c=`idle`,l=()=>{},m=()=>{c=`closed`,l()},g=e=>{c===`started`&&(m(),k(s,h,e,a))},_=()=>{c===`idle`&&(c=`started`,l=r(s,g),O(s,d,t,a))},v=e=>{c===`started`&&(m(),s.signal.aborted?k(s,h,s.signal.reason,a):`error`in e?O(s,p,e.error,a):O(s,f,e.value,a))};return Reflect.set(s,u,{start:_,cancel:g,done:v,drop:()=>{c===`idle`&&(c=`closed`)}}),A(()=>(o||_(),Reflect.apply(e,null,[t,i]))).then(e=>(v({value:e}),e),e=>{if(c===`idle`)try{_()}catch(e){n(`pending listener`,e)}v({error:e})})},_=e=>{let t=Reflect.get(e,l)===!0;return(n,r)=>g(e,n,r,t)},v=Reflect.get(t??{},l)===!0;function y(n,r){return typeof r==`object`?t?g(o,n,r,v):Reflect.apply(o,null,[n,r]):m(c(S,e,n),a)}let x=Object.assign(y,{type:e,match:o.match,wrap:_,pending:d,success:f,fail:p,cancel:h}),S=m(x,a);return S},M=e=>typeof e==`object`&&!!e,N=e=>{if(typeof e==`string`)return e;if(M(e)||typeof e==`function`){let t=Reflect.get(e,`type`);return typeof t==`string`?t:e}return e},P=(e,t)=>{let[n,r]=e;return n===void 0||Object.is(N(n),N(t[0]))&&(r===void 0||Object.is(r,t[1]))},F=n=>{let r={on:n.on,once:n.once,off:n.off},i=[],a=!0,o=(e,t)=>Reflect.apply(e,n,t),s=(e,n)=>(...r)=>{if(!a)return o(n,r);let s={kind:e,args:r,stack:t(),dropped:!1};return i.push(s),()=>{s.live?s.live():s.dropped=!0}},c=e=>(...t)=>{if(!a)return o(e,t);for(let e of i)e.kind!==`off`&&P(t,e.args)&&(e.dropped=!0);i.push({kind:`off`,args:t,dropped:!1})},l=s(`on`,r.on),u=s(`once`,r.once),d=c(r.off);return n.on=l,n.once=u,n.off=d,{release:()=>{a=!1,n.on===l&&(n.on=r.on),n.once===u&&(n.once=r.once),n.off===d&&(n.off=r.off)},replay:()=>{for(let t of i.splice(0))if(!t.dropped){if(t.kind===`off`)o(r.off,t.args);else{let n=t.kind===`on`?r.on:r.once;t.live=e(()=>o(n,t.args),t.stack)}}}}};function I(e,t,n){return n?{name:e,setup:t,scope:n}:{name:e,setup:t}}const L=(e,t)=>n=>t?e.fitScope?.(t,n)??n:n,R=(e,t,n)=>{n&&e.applyScope?.(t,n)},z=(e,t,n)=>[t,e(n)],B=(t,n)=>{let r=t.on;return(...t)=>{let[i,a,...o]=t;return e(()=>Reflect.apply(r,null,[i,n(a),...o]))}},V=e=>(t,...n)=>{e.run(t,...n).catch(t=>{if(!e.signal.aborted)throw t})},H=(e=[])=>t=>{let o=t.actions??C()(t),s={handle:(...e)=>{if(!o.handle)throw Error("modulePlugin: the `actions` of the bus is not a registry from `actionsPlugin()`");return Reflect.apply(o.handle,null,e)}},c=e=>{let n=/* @__PURE__ */ new Map,a=/* @__PURE__ */ new Map;return{mount:o=>{let l=n.get(o);if(l)return l;let u=a.get(o.name);u&&n.get(u)?.();let d=i(e,o.name),f=d.ctx,p=c(f),m=()=>{n.delete(o),a.get(o.name)===o&&a.delete(o.name),f.abort(),d.unlink()};n.set(o,m),a.set(o.name,o);let h=o.scope,g=L(t,h),_=(e,t)=>{let n=Reflect.apply(s.handle,null,z(g,e,t));return r(f,n),n},v=(...e)=>{let t=e.map(e=>typeof e==`function`?_(e,e):p.mount(e));return()=>{for(let e of t)e()}},y=V(f),b=B(f,g);try{R(t,f,h),o.setup(Object.assign(f,{handle:_,use:v,spawn:y,on:b}))}catch(e){throw m(),e}return m},unmount:e=>n.get(e)?.(),isMounted:e=>n.has(e)}},l=F(t),u=t.emit,d=t.get,f,m=(n=!0)=>{if(f)return f;l.release(),t.emit===h&&(t.emit=u);let r=a({event:{type:`app`,payload:void 0},bus:t,tracer:t.trace});f={root:r,apps:p(c(r.ctx))};try{n&&f.apps.mount(...e)}finally{l.replay()}return f},h=(...e)=>(m(),Reflect.apply(u,t,e));t.emit=h,t.get=((...e)=>(m(),Reflect.apply(d,t,e))),e.length>0&&queueMicrotask(()=>{try{m()}catch(e){n(`the modules of the app could not start`,e)}});let g={get actions(){return m(),o},mount:(...e)=>{let{root:t,apps:n}=m();return t.ctx.signal.throwIfAborted(),n.mount(...e)},unmount:(...e)=>m().apps.unmount(...e),isMounted:e=>m().apps.isMounted(e),dispose:()=>m(!1).root.ctx.abort(),createModule:I};return t.app=g,g};export{C as actionsPlugin,w as createAction,j as createJob,I as createModule,E as emitterPlugin,H as modulePlugin};
|
package/dist/dev.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { H as EventMap, Q as UnsubscribeCallback, v as Bus } from "./action-creator-DprJV-pN.js";
|
|
2
|
-
import { i as WithApp, n as ModuleApi } from "./module-
|
|
2
|
+
import { i as WithApp, n as ModuleApi } from "./module-Fk4OtkZk.js";
|
|
3
3
|
//#region src/dev/types.d.ts
|
|
4
4
|
/** The parts of what `modulePlugin` returned that the diagnostics report. */
|
|
5
5
|
type ReportedApp<E extends EventMap> = Pick<ModuleApi<E>, "mount" | "unmount" | "isMounted">;
|
|
@@ -17,22 +17,6 @@ type MadeHandled<P, R, T extends string> = Handled<P, R> & {
|
|
|
17
17
|
type CarryingCalls<Made, P, R, F> = [unknown] extends [F] ? Made : ((payload: P) => HandledCall<P, R> & WithScope<F>) & Made & WithScope<F>;
|
|
18
18
|
/** An action made with a scope: its calls carry the fields of that scope, for the checker. */
|
|
19
19
|
type ScopedHandled<P, R, T extends string, F> = ((payload: P) => HandledCall<P, R> & WithScope<F>) & MadeHandled<P, R, T> & WithScope<F>;
|
|
20
|
-
/** An action (or its type) with the handler to bind to it; `setHandler` makes one. */
|
|
21
|
-
type Binding<E extends EventMap = EventMap> = readonly [Handled<never, unknown> | string, Task<never, E, unknown>];
|
|
22
|
-
/**
|
|
23
|
-
* Pairs an action with the handler to bind to it, for `actionsPlugin`. For a
|
|
24
|
-
* action (an action made with a handler, or with a `Call` type) it checks that
|
|
25
|
-
* the handler fits; any other action is accepted with a handler of any payload.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* bus.use(actionsPlugin([setHandler(sendMail, smtpSend)]));
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
declare function setHandler<P, R, E extends EventMap>(handled: Handled<P, R>, fn: Task<P, E, R>): Binding<E>;
|
|
33
|
-
declare function setHandler<A extends {
|
|
34
|
-
readonly type: string;
|
|
35
|
-
}, E extends EventMap>(action: A, fn: LooseHandler<A, E>): Binding<E>;
|
|
36
20
|
/**
|
|
37
21
|
* The handler an action that is not an action can have: any. An action
|
|
38
22
|
* (a creator whose calls are calls of an action with a handler) is not accepted here, so its own
|
|
@@ -49,15 +33,14 @@ interface Registry$1<E extends EventMap> extends Actions {
|
|
|
49
33
|
readonly type: string;
|
|
50
34
|
}>(action: A, fn: LooseHandler<A, E>): UnsubscribeCallback;
|
|
51
35
|
handle(type: string, fn: Task<never, E, unknown>): UnsubscribeCallback;
|
|
52
|
-
handle(bindings: readonly Binding<E>[]): UnsubscribeCallback;
|
|
53
36
|
}
|
|
54
37
|
/**
|
|
55
38
|
* The plugin that gives a bus the handlers bound to actions:
|
|
56
|
-
* `bus.use(actionsPlugin(
|
|
39
|
+
* `bus.use(actionsPlugin())` makes the registry, sets it as the
|
|
57
40
|
* actions of the bus and returns it. Each bus has its own, so each test can
|
|
58
|
-
* have its own implementation. `handle` binds
|
|
59
|
-
* function that unbinds. A second `actionsPlugin` on the same bus
|
|
60
|
-
*
|
|
41
|
+
* have its own implementation. `handle` binds a handler and returns the
|
|
42
|
+
* function that unbinds. A second `actionsPlugin` on the same bus returns the
|
|
43
|
+
* registry that is there, so the order of the
|
|
61
44
|
* plugins does not matter. A `actions` that is not such a registry (a custom
|
|
62
45
|
* runner) is replaced.
|
|
63
46
|
*
|
|
@@ -67,13 +50,14 @@ interface Registry$1<E extends EventMap> extends Actions {
|
|
|
67
50
|
* @example
|
|
68
51
|
* ```ts
|
|
69
52
|
* const bus = createBus<Events>();
|
|
70
|
-
* const actions = bus.use(actionsPlugin(
|
|
53
|
+
* const actions = bus.use(actionsPlugin());
|
|
54
|
+
* actions.handle(sendMail, smtpSend);
|
|
71
55
|
*
|
|
72
56
|
* const unbind = actions.handle(getUser, async () => fakeUser);
|
|
73
57
|
* unbind(); // the action's own handler is back
|
|
74
58
|
* ```
|
|
75
59
|
*/
|
|
76
|
-
declare const actionsPlugin: <E extends EventMap>(
|
|
60
|
+
declare const actionsPlugin: <E extends EventMap>() => (bus: Bus<E>) => Registry$1<E>;
|
|
77
61
|
//#endregion
|
|
78
62
|
//#region src/actions/module.d.ts
|
|
79
63
|
/**
|
|
@@ -83,14 +67,13 @@ declare const actionsPlugin: <E extends EventMap>(bindings?: readonly Binding<E>
|
|
|
83
67
|
* module does is undone when it is unmounted.
|
|
84
68
|
*/
|
|
85
69
|
type ModuleContext<E extends EventMap = EventMap> = Context<E> & {
|
|
86
|
-
/** Binds the handler of an action
|
|
70
|
+
/** Binds the handler of an action; undone on unmount. */
|
|
87
71
|
handle<P, R>(handled: Handled<P, R>, fn: Task<P, E, R>): UnsubscribeCallback;
|
|
88
72
|
/** Also an action made without a handler: the handler answers it as it would one made with a handler. */
|
|
89
73
|
handle<A extends {
|
|
90
74
|
readonly type: string;
|
|
91
75
|
}>(action: A, fn: LooseHandler<A, E>): UnsubscribeCallback;
|
|
92
76
|
handle(type: string, fn: Task<never, E, unknown>): UnsubscribeCallback;
|
|
93
|
-
handle(bindings: readonly Binding<E>[]): UnsubscribeCallback;
|
|
94
77
|
/**
|
|
95
78
|
* Brings sub-modules and actions, in any order. A sub-module is mounted and
|
|
96
79
|
* unmounted with this one. An action is bound to the handler it carries, for
|
|
@@ -124,7 +107,7 @@ type Module<E extends EventMap = EventMap> = {
|
|
|
124
107
|
* ```ts
|
|
125
108
|
* export const userModule = createModule<Events>("user", (m) => {
|
|
126
109
|
* m.handle(getUser, fetchUser);
|
|
127
|
-
* m.handle(
|
|
110
|
+
* m.handle(sendMail, smtpSend);
|
|
128
111
|
* m.use(authModule);
|
|
129
112
|
*
|
|
130
113
|
* m.on(loginRequested, (user, ctx) => ctx.emit(userLoaded(user)));
|
|
@@ -220,4 +203,4 @@ type ModuleApi<E extends EventMap = EventMap> = {
|
|
|
220
203
|
*/
|
|
221
204
|
declare const modulePlugin: <E extends EventMap>(modules?: readonly Module<E>[]) => (bus: Bus<E> & ScopeSlots & WithApp) => ModuleApi<E>;
|
|
222
205
|
//#endregion
|
|
223
|
-
export { createModule as a,
|
|
206
|
+
export { createModule as a, MadeHandled as c, WithApp as i, ScopedHandled as l, ModuleApi as n, modulePlugin as o, ModuleContext as r, CarryingCalls as s, Module as t, actionsPlugin as u };
|
|
@@ -40,22 +40,6 @@ type MadeHandled<P, R, T extends string> = Handled<P, R> & {
|
|
|
40
40
|
type CarryingCalls<Made, P, R, F> = [unknown] extends [F] ? Made : ((payload: P) => HandledCall<P, R> & WithScope<F>) & Made & WithScope<F>;
|
|
41
41
|
/** An action made with a scope: its calls carry the fields of that scope, for the checker. */
|
|
42
42
|
type ScopedHandled<P, R, T extends string, F> = ((payload: P) => HandledCall<P, R> & WithScope<F>) & MadeHandled<P, R, T> & WithScope<F>;
|
|
43
|
-
/** An action (or its type) with the handler to bind to it; `setHandler` makes one. */
|
|
44
|
-
type Binding<E extends EventMap = EventMap> = readonly [Handled<never, unknown> | string, Task<never, E, unknown>];
|
|
45
|
-
/**
|
|
46
|
-
* Pairs an action with the handler to bind to it, for `actionsPlugin`. For a
|
|
47
|
-
* action (an action made with a handler, or with a `Call` type) it checks that
|
|
48
|
-
* the handler fits; any other action is accepted with a handler of any payload.
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```ts
|
|
52
|
-
* bus.use(actionsPlugin([setHandler(sendMail, smtpSend)]));
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export declare function setHandler<P, R, E extends EventMap>(handled: Handled<P, R>, fn: Task<P, E, R>): Binding<E>;
|
|
56
|
-
export declare function setHandler<A extends {
|
|
57
|
-
readonly type: string;
|
|
58
|
-
}, E extends EventMap>(action: A, fn: LooseHandler<A, E>): Binding<E>;
|
|
59
43
|
/**
|
|
60
44
|
* The handler an action that is not an action can have: any. An action
|
|
61
45
|
* (a creator whose calls are calls of an action with a handler) is not accepted here, so its own
|
|
@@ -72,15 +56,14 @@ interface Registry$1<E extends EventMap> extends Actions {
|
|
|
72
56
|
readonly type: string;
|
|
73
57
|
}>(action: A, fn: LooseHandler<A, E>): UnsubscribeCallback;
|
|
74
58
|
handle(type: string, fn: Task<never, E, unknown>): UnsubscribeCallback;
|
|
75
|
-
handle(bindings: readonly Binding<E>[]): UnsubscribeCallback;
|
|
76
59
|
}
|
|
77
60
|
/**
|
|
78
61
|
* The plugin that gives a bus the handlers bound to actions:
|
|
79
|
-
* `bus.use(actionsPlugin(
|
|
62
|
+
* `bus.use(actionsPlugin())` makes the registry, sets it as the
|
|
80
63
|
* actions of the bus and returns it. Each bus has its own, so each test can
|
|
81
|
-
* have its own implementation. `handle` binds
|
|
82
|
-
* function that unbinds. A second `actionsPlugin` on the same bus
|
|
83
|
-
*
|
|
64
|
+
* have its own implementation. `handle` binds a handler and returns the
|
|
65
|
+
* function that unbinds. A second `actionsPlugin` on the same bus returns the
|
|
66
|
+
* registry that is there, so the order of the
|
|
84
67
|
* plugins does not matter. A `actions` that is not such a registry (a custom
|
|
85
68
|
* runner) is replaced.
|
|
86
69
|
*
|
|
@@ -90,13 +73,14 @@ interface Registry$1<E extends EventMap> extends Actions {
|
|
|
90
73
|
* @example
|
|
91
74
|
* ```ts
|
|
92
75
|
* const bus = createBus<Events>();
|
|
93
|
-
* const actions = bus.use(actionsPlugin(
|
|
76
|
+
* const actions = bus.use(actionsPlugin());
|
|
77
|
+
* actions.handle(sendMail, smtpSend);
|
|
94
78
|
*
|
|
95
79
|
* const unbind = actions.handle(getUser, async () => fakeUser);
|
|
96
80
|
* unbind(); // the action's own handler is back
|
|
97
81
|
* ```
|
|
98
82
|
*/
|
|
99
|
-
export declare const actionsPlugin: <E extends EventMap>(
|
|
83
|
+
export declare const actionsPlugin: <E extends EventMap>() => (bus: Bus<E>) => Registry$1<E>;
|
|
100
84
|
//#endregion
|
|
101
85
|
//#region src/actions/action.d.ts
|
|
102
86
|
type Reserved = "handle" | "prepare";
|
|
@@ -298,14 +282,13 @@ scope?: X) => CarryingCalls<Job<P, R, T, S, F, PE, CE>, P, Promise<Awaited<R> |
|
|
|
298
282
|
* module does is undone when it is unmounted.
|
|
299
283
|
*/
|
|
300
284
|
type ModuleContext<E extends EventMap = EventMap> = Context<E> & {
|
|
301
|
-
/** Binds the handler of an action
|
|
285
|
+
/** Binds the handler of an action; undone on unmount. */
|
|
302
286
|
handle<P, R>(handled: Handled<P, R>, fn: Task<P, E, R>): UnsubscribeCallback;
|
|
303
287
|
/** Also an action made without a handler: the handler answers it as it would one made with a handler. */
|
|
304
288
|
handle<A extends {
|
|
305
289
|
readonly type: string;
|
|
306
290
|
}>(action: A, fn: LooseHandler<A, E>): UnsubscribeCallback;
|
|
307
291
|
handle(type: string, fn: Task<never, E, unknown>): UnsubscribeCallback;
|
|
308
|
-
handle(bindings: readonly Binding<E>[]): UnsubscribeCallback;
|
|
309
292
|
/**
|
|
310
293
|
* Brings sub-modules and actions, in any order. A sub-module is mounted and
|
|
311
294
|
* unmounted with this one. An action is bound to the handler it carries, for
|
|
@@ -339,7 +322,7 @@ type Module<E extends EventMap = EventMap> = {
|
|
|
339
322
|
* ```ts
|
|
340
323
|
* export const userModule = createModule<Events>("user", (m) => {
|
|
341
324
|
* m.handle(getUser, fetchUser);
|
|
342
|
-
* m.handle(
|
|
325
|
+
* m.handle(sendMail, smtpSend);
|
|
343
326
|
* m.use(authModule);
|
|
344
327
|
*
|
|
345
328
|
* m.on(loginRequested, (user, ctx) => ctx.emit(userLoaded(user)));
|
|
@@ -435,4 +418,4 @@ type ModuleApi<E extends EventMap = EventMap> = {
|
|
|
435
418
|
*/
|
|
436
419
|
export declare const modulePlugin: <E extends EventMap>(modules?: readonly Module<E>[]) => (bus: Bus<E> & ScopeSlots & WithApp) => ModuleApi<E>;
|
|
437
420
|
//#endregion
|
|
438
|
-
export type { ActionCreator,
|
|
421
|
+
export type { ActionCreator, EmitterApi, FreeActionCreator, Job, Module, ModuleApi, ModuleContext };
|
package/dist/notrace/actions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as e,i as t,l as n,n as r,s as i}from"./context-_HWcdjZf.js";import{n as a,r as o}from"./bus-37nlVljc.js";import{n as s,t as c}from"./lifecycle-key-8iX8RUVD.js";import{t as l}from"./origin-key-BbWN3gmN.js";const u=(e,t)=>e(),d=Symbol.for(`nanoeffects.scope`),f=(e,t)=>(t&&Object.defineProperty(e,d,{value:t}),e),p=(e,t)=>({...e,...t,$effects:{...e.$effects,...t.$effects}}),m=e=>Reflect.get(e,d),h=e=>{let t=Object.assign(n=>a(t,e,n),{type:e});return t},g=e=>typeof e==`object`&&!!e&&Reflect.get(e,`signal`)instanceof AbortSignal&&typeof Reflect.get(e,`emit`)==`function`;var _=class extends Error{};function v(e,t,n){return y(e,t,n)}function y(e,t,n,r){let i=t=>t.type===e,o=e=>r?e.length===2&&g(e[1]):typeof e[1]==`object`;function s(...i){let[s,c]=i;if(o(i)){if(!t)throw new _(`No handler for action "${e}"`);return t(s,c)}let u=r&&Reflect.apply(r,null,i),d=u?u.payload:s;return f(a(l,e,d),n)}let c=Object.assign(s,{type:e,match:i,toString:()=>e}),l=f(c,n);return l}
|
|
1
|
+
import{c as e,i as t,l as n,n as r,s as i}from"./context-_HWcdjZf.js";import{n as a,r as o}from"./bus-37nlVljc.js";import{n as s,t as c}from"./lifecycle-key-8iX8RUVD.js";import{t as l}from"./origin-key-BbWN3gmN.js";const u=(e,t)=>e(),d=Symbol.for(`nanoeffects.scope`),f=(e,t)=>(t&&Object.defineProperty(e,d,{value:t}),e),p=(e,t)=>({...e,...t,$effects:{...e.$effects,...t.$effects}}),m=e=>Reflect.get(e,d),h=e=>{let t=Object.assign(n=>a(t,e,n),{type:e});return t},g=e=>typeof e==`object`&&!!e&&Reflect.get(e,`signal`)instanceof AbortSignal&&typeof Reflect.get(e,`emit`)==`function`;var _=class extends Error{};function v(e,t,n){return y(e,t,n)}function y(e,t,n,r){let i=t=>t.type===e,o=e=>r?e.length===2&&g(e[1]):typeof e[1]==`object`;function s(...i){let[s,c]=i;if(o(i)){if(!t)throw new _(`No handler for action "${e}"`);return t(s,c)}let u=r&&Reflect.apply(r,null,i),d=u?u.payload:s;return f(a(l,e,d),n)}let c=Object.assign(s,{type:e,match:i,toString:()=>e}),l=f(c,n);return l}const b=e=>typeof Reflect.get(e??{},`handle`)==`function`,x=()=>e=>{let t=e.actions;if(b(t))return t;let n=/* @__PURE__ */ new Map,r=(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 i(e,t){return r(o(e),t)}let a={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:i,bound:e=>n.has(e)?h(e):void 0};return e.actions=a,a};function S(e,t,n){let{handle:r,prepare:i,scope:a}=C(typeof t==`function`?n:t),o=typeof t==`function`?t:r;return o?y(e,o,a,i):Object.assign(f((...t)=>{let n=i&&Reflect.apply(i,null,t);return f({type:e,payload:n?n.payload:t[0]},a)},a),{type:e,match:t=>t.type===e,toString:()=>e})}const C=e=>{if(!e)return{};let{handle:t,prepare:n,...r}=e,i=typeof t==`function`,a=typeof n==`function`;if(!i&&!a)return{scope:e};let o=Object.keys(r).length>0?r:void 0;return{...i&&{handle:t},...a&&{prepare:n},...o&&{scope:o}}},w=()=>e=>{function t(t,n){return(...r)=>{let i=t(...r),a=Reflect.get(e,l);a?a(i,n):e.emit(i)}}return Object.assign(e,{emitter:t})},T=e=>typeof e==`string`?S(e):e,E=(e,t,n,r)=>{let i=m(t);e.emit(Reflect.apply(t,null,[n]),r&&i?p(r,i):r)},D=(e,t,r,a)=>{let o=e;o[i]=!0;try{E(e,t,r,a)}catch(e){n(`cancel listener`,e)}o[i]=!1},O=e=>{try{return Promise.resolve(e())}catch(e){if(e instanceof _)throw e;return Promise.reject(e)}},k=(t,r,i={},o)=>{let l=v(t,r),u=e=>T(i[e]??`${t}/${e}`),d=u(`pending`),p=u(`success`),m=u(`fail`),h=u(`cancel`),g=(t,r,i,a)=>{let s=i,l=`idle`,u=()=>{},f=()=>{l=`closed`,u()},g=e=>{l===`started`&&(f(),D(s,h,e,o))},_=()=>{l===`idle`&&(l=`started`,u=e(s,g),E(s,d,r,o))},v=e=>{l===`started`&&(f(),s.signal.aborted?D(s,h,s.signal.reason,o):`error`in e?E(s,m,e.error,o):E(s,p,e.value,o))};return Reflect.set(s,c,{start:_,cancel:g,done:v,drop:()=>{l===`idle`&&(l=`closed`)}}),O(()=>(a||_(),Reflect.apply(t,null,[r,i]))).then(e=>(v({value:e}),e),e=>{if(l===`idle`)try{_()}catch(e){n(`pending listener`,e)}v({error:e})})},_=e=>{let t=Reflect.get(e,s)===!0;return(n,r)=>g(e,n,r,t)},y=Reflect.get(r??{},s)===!0;function b(e,n){return typeof n==`object`?r?g(l,e,n,y):Reflect.apply(l,null,[e,n]):f(a(S,t,e),o)}let x=Object.assign(b,{type:t,match:l.match,wrap:_,pending:d,success:p,fail:m,cancel:h}),S=f(x,o);return S},A=e=>typeof e==`object`&&!!e,j=e=>{if(typeof e==`string`)return e;if(A(e)||typeof e==`function`){let t=Reflect.get(e,`type`);return typeof t==`string`?t:e}return e},M=(e,t)=>{let[n,r]=e;return n===void 0||Object.is(j(n),j(t[0]))&&(r===void 0||Object.is(r,t[1]))},N=e=>{let t={on:e.on,once:e.once,off:e.off},n=[],r=!0,i=(t,n)=>Reflect.apply(t,e,n),a=(e,t)=>(...a)=>{if(!r)return i(t,a);let o={kind:e,args:a,stack:void 0,dropped:!1};return n.push(o),()=>{o.live?o.live():o.dropped=!0}},o=e=>(...t)=>{if(!r)return i(e,t);for(let e of n)e.kind!==`off`&&M(t,e.args)&&(e.dropped=!0);n.push({kind:`off`,args:t,dropped:!1})},s=a(`on`,t.on),c=a(`once`,t.once),l=o(t.off);return e.on=s,e.once=c,e.off=l,{release:()=>{r=!1,e.on===s&&(e.on=t.on),e.once===c&&(e.once=t.once),e.off===l&&(e.off=t.off)},replay:()=>{for(let e of n.splice(0))if(!e.dropped){if(e.kind===`off`)i(t.off,e.args);else{let n=e.kind===`on`?t.on:t.once;e.live=u(()=>i(n,e.args),e.stack)}}}}},P=e=>({mount:(...t)=>{let[n]=t;if(t.length===1&&n!==void 0)return e.mount(n);let r=[],i=()=>{for(let e of r.splice(0).reverse())e()};try{for(let n of t)e.isMounted(n)||r.push(e.mount(n))}catch(e){throw i(),e}return i},unmount:(...t)=>{for(let n of[...t].reverse())e.unmount(n)},isMounted:e.isMounted});function F(e,t,n){return n?{name:e,setup:t,scope:n}:{name:e,setup:t}}const I=(e,t)=>n=>t?e.fitScope?.(t,n)??n:n,L=(e,t,n)=>{n&&e.applyScope?.(t,n)},R=(e,t,n)=>[t,e(n)],z=(e,t)=>{let n=e.on;return(...e)=>{let[r,i,...a]=e;return u(()=>Reflect.apply(n,null,[r,t(i),...a]))}},B=e=>(t,...n)=>{e.run(t,...n).catch(t=>{if(!e.signal.aborted)throw t})},V=(i=[])=>a=>{let o=a.actions??x()(a),s={handle:(...e)=>{if(!o.handle)throw Error("modulePlugin: the `actions` of the bus is not a registry from `actionsPlugin()`");return Reflect.apply(o.handle,null,e)}},c=n=>{let r=/* @__PURE__ */ new Map,i=/* @__PURE__ */ new Map;return{mount:o=>{let l=r.get(o);if(l)return l;let u=i.get(o.name);u&&r.get(u)?.();let d=t(n,o.name),f=d.ctx,p=c(f),m=()=>{r.delete(o),i.get(o.name)===o&&i.delete(o.name),f.abort(),d.unlink()};r.set(o,m),i.set(o.name,o);let h=o.scope,g=I(a,h),_=(t,n)=>{let r=Reflect.apply(s.handle,null,R(g,t,n));return e(f,r),r},v=(...e)=>{let t=e.map(e=>typeof e==`function`?_(e,e):p.mount(e));return()=>{for(let e of t)e()}},y=B(f),b=z(f,g);try{L(a,f,h),o.setup(Object.assign(f,{handle:_,use:v,spawn:y,on:b}))}catch(e){throw m(),e}return m},unmount:e=>r.get(e)?.(),isMounted:e=>r.has(e)}},l=N(a),u=a.emit,d=a.get,f,p=(e=!0)=>{if(f)return f;l.release(),a.emit===m&&(a.emit=u);let t=r({event:{type:`app`,payload:void 0},bus:a,tracer:a.trace});f={root:t,apps:P(c(t.ctx))};try{e&&f.apps.mount(...i)}finally{l.replay()}return f},m=(...e)=>(p(),Reflect.apply(u,a,e));a.emit=m,a.get=((...e)=>(p(),Reflect.apply(d,a,e))),i.length>0&&queueMicrotask(()=>{try{p()}catch(e){n(`the modules of the app could not start`,e)}});let h={get actions(){return p(),o},mount:(...e)=>{let{root:t,apps:n}=p();return t.ctx.signal.throwIfAborted(),n.mount(...e)},unmount:(...e)=>p().apps.unmount(...e),isMounted:e=>p().apps.isMounted(e),dispose:()=>p(!1).root.ctx.abort(),createModule:F};return a.app=h,h};export{x as actionsPlugin,S as createAction,k as createJob,F as createModule,w as emitterPlugin,V as modulePlugin};
|
package/dist/signal.d.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import { A as HandledCall, C as Emit, E as EventLike, H as EventMap, s as EmitScope, u as WithScope, v as Bus } from "./action-creator-DprJV-pN.js";
|
|
1
|
+
import { A as HandledCall, C as Emit, E as EventLike, H as EventMap, I as WithSignal, s as EmitScope, u as WithScope, v as Bus } from "./action-creator-DprJV-pN.js";
|
|
2
2
|
import { n as FromSignal } from "./context-types-BiTMp_lW.js";
|
|
3
3
|
//#region src/signal/fetch.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* A `fetch` that
|
|
6
|
-
*
|
|
7
|
-
* `signal` of
|
|
5
|
+
* A `fetch` that stops with its task: an effect, so `ctx` is the first argument and the request carries
|
|
6
|
+
* `ctx.signal`. The caller never passes the signal. A call that has a `signal` of its own in `init` stops with
|
|
7
|
+
* whichever aborts first. Outside a task pass `{ signal }` of the source, or `{}` in plain JavaScript.
|
|
8
|
+
*
|
|
9
|
+
* With `effectsPlugin({ fetch: signalFetch })` every task has a `ctx.fetch(input, init)` bound to itself.
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
10
12
|
* ```ts
|
|
11
|
-
* const bus = createBus().use(
|
|
12
|
-
* contextPlugin({ fetch: fromSignal(signalFetch) }),
|
|
13
|
-
* );
|
|
14
|
-
*
|
|
15
13
|
* bus.on("search", latest(async (query, ctx) => {
|
|
16
|
-
* const response = await ctx
|
|
14
|
+
* const response = await signalFetch(ctx, `/api?q=${query}`); // stops when the policy cancels the handler
|
|
17
15
|
* ctx.emit("results", await response.json());
|
|
18
16
|
* }));
|
|
17
|
+
*
|
|
18
|
+
* const withFetch = createBus().use(effectsPlugin({ fetch: signalFetch }));
|
|
19
|
+
* withFetch.on("search", latest(async (query, { fetch }) => fetch(`/api?q=${query}`)));
|
|
19
20
|
* ```
|
|
20
21
|
*/
|
|
21
|
-
export declare const signalFetch: (
|
|
22
|
+
export declare const signalFetch: (ctx: WithSignal, input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
22
23
|
//#endregion
|
|
23
24
|
//#region src/signal/from-signal.d.ts
|
|
24
25
|
/**
|
package/dist/signal.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e}from"./caller-DULg_ZYV.js";import{t}from"./onAbort-BgMfspK4.js";import{n}from"./promise-CSLpzwSR.js";import{n as r}from"./bus-Cmk4U1hQ.js";import{t as i}from"./signal-field-key-CWT-3OXC.js";const a=e
|
|
1
|
+
import{i as e}from"./caller-DULg_ZYV.js";import{t}from"./onAbort-BgMfspK4.js";import{n}from"./promise-CSLpzwSR.js";import{n as r}from"./bus-Cmk4U1hQ.js";import{t as i}from"./signal-field-key-CWT-3OXC.js";const a=(e,t,n)=>{let r=n?.signal,i=r&&e.signal?AbortSignal.any([e.signal,r]):r??e.signal;return fetch(t,{...n,signal:i})},o=e=>({[i]:e}),s=(n,r)=>{let i=(e,i,a)=>{let o=a?.signal;if(!o)return n(e,i,a);let s=n(e,r?(e,t)=>(c(),i(e,t)):i,a),c=t({signal:o},s);return()=>{c(),s()}};return(t,n,r)=>e(()=>i(t,n,r))},c=e=>typeof e==`object`&&!!e,l=(e,t)=>{let n=t[0];if(!c(n)||!c(e)&&typeof e!=`function`)return;let r=Reflect.get(n,`signal`);return r instanceof AbortSignal?r:void 0},u=(e,i,a,o)=>{let s=t({signal:e},e=>{for(let{target:t}of i)t.abort(e)});return n(o)?o.then(s,s):r(a)&&s(),o},d=()=>e=>{let t,n,r=e.addContext;e.addContext=e=>{r?.(e),t?.push({target:e}),n?.aborted&&e.abort(n.reason)};let i=e.emit;e.emit=(e,...r)=>{let a=l(e,r);if(!(a||t))return i(e,...r);let o=t,s=n,c=[];t=a?c:void 0,n=a;let d;try{d=i(e,...r)}finally{t=o,n=s}return a?u(a,c,e,d):d};let a=e.on,o=e.once;return e.on=s(a,!1),e.once=s(o,!0),e};export{o as fromSignal,a as signalFetch,d as signalPlugin};
|
package/docs/README.md
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
nanoeffects moves business logic and side effects out of UI components into plain `async` functions. Policies (`latest`, `debounce`, `queue`) and effects (`retry`, `timeout`, `poll`) handle races, retries and cancellation.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
|
+
// search.js
|
|
6
7
|
import { createBus } from "nanoeffects";
|
|
7
8
|
import { latest } from "nanoeffects/policies";
|
|
8
9
|
|
|
9
|
-
const bus = createBus(); // event bus
|
|
10
|
+
export const bus = createBus(); // event bus
|
|
10
11
|
|
|
11
12
|
// The search logic lives outside the component; latest aborts the previous request
|
|
12
13
|
bus.on(
|
|
@@ -22,7 +23,33 @@ bus.on("found", (items) => render(items));
|
|
|
22
23
|
bus.emit("search", "nano");
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
In a component, the same bus needs no extra code. This is React; the handler stays in `search.js`:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
// SearchBox.jsx
|
|
30
|
+
import { useEffect, useState } from "react";
|
|
31
|
+
import { bus } from "./search";
|
|
32
|
+
|
|
33
|
+
export function SearchBox() {
|
|
34
|
+
const [items, setItems] = useState([]);
|
|
35
|
+
|
|
36
|
+
// on returns the unbind function; returning it removes the listener on unmount
|
|
37
|
+
useEffect(() => bus.on("found", setItems), []);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<input onChange={(event) => bus.emit("search", event.target.value)} />
|
|
42
|
+
<ul>
|
|
43
|
+
{items.map((item) => (
|
|
44
|
+
<li key={item.id}>{item.title}</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The same example step by step: [React](react.md), [Vue](vue.md), [Svelte](svelte.md). Ready projects: [Start a project from a template](starter.md).
|
|
26
53
|
|
|
27
54
|
## Install
|
|
28
55
|
|
package/docs/actions.md
CHANGED
|
@@ -143,15 +143,21 @@ export const sendMail = createAction("mail/send");
|
|
|
143
143
|
```js
|
|
144
144
|
// bus.js
|
|
145
145
|
import { createBus } from "nanoeffects";
|
|
146
|
-
import { actionsPlugin
|
|
147
|
-
import { sendMail } from "./mail";
|
|
146
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
148
147
|
|
|
149
148
|
export const bus = createBus();
|
|
150
149
|
|
|
151
|
-
// actionsPlugin — the registry of handlers of this bus
|
|
152
|
-
export const actions = bus.use(
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
// actionsPlugin — the registry of handlers of this bus
|
|
151
|
+
export const actions = bus.use(actionsPlugin());
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
// mail-handlers.js
|
|
156
|
+
import { actions } from "./bus";
|
|
157
|
+
import { sendMail } from "./mail";
|
|
158
|
+
|
|
159
|
+
// actions.handle — binds the work to the action on this bus
|
|
160
|
+
actions.handle(sendMail, smtpSend);
|
|
155
161
|
```
|
|
156
162
|
|
|
157
163
|
```js
|
|
@@ -165,7 +171,7 @@ unbind();
|
|
|
165
171
|
|
|
166
172
|
A handler bound in the registry makes `bus.emit(sendMail(mail))` run it and return its result, the same as for an action that carries its handler. The listeners of the type are told first. With no handler bound, `sendMail` is an event again.
|
|
167
173
|
|
|
168
|
-
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin`
|
|
174
|
+
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin` returns that registry. There is no `actions` option in `createBus`; assign a custom runner with `bus.actions = { run }`, and add `bound(type)` to it to answer for actions that have no handler of their own.
|
|
169
175
|
|
|
170
176
|
### The result type of a bound handler
|
|
171
177
|
|
|
@@ -47,7 +47,7 @@ bus.on("results", (items) => render(items));
|
|
|
47
47
|
|
|
48
48
|
What to replace: `api.search` is your own request function. It must accept `{ signal }` and give it to `fetch`. Without it, `latest` still ignores the old result, but the request keeps running.
|
|
49
49
|
|
|
50
|
-
To make the mistake impossible, bind the signal once: `
|
|
50
|
+
To make the mistake impossible, bind the signal once: `signalFetch(ctx, url)` is a `fetch` that stops with the handler, and `effectsPlugin({ fetch: signalFetch })` gives every handler a `ctx.fetch` of it ([how](pass-the-signal.md#bind-the-signal-once)). To find the requests that already forget it, use `devPlugin` while you develop, and `getLostSignalsCount(bus)` in a test ([how](pass-the-signal.md#count-lost-signals-in-a-test)).
|
|
51
51
|
|
|
52
52
|
Result: `emit("search", "ab")` followed by `emit("search", "abc")` shows only the results for `abc`, and the request for `ab` is cancelled.
|
|
53
53
|
|
package/docs/context.md
CHANGED
|
@@ -99,7 +99,7 @@ bus.on("user/open", async (id, ctx) => {
|
|
|
99
99
|
});
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
- `signalFetch` from the same entry
|
|
102
|
+
- For `fetch` you need no `fromSignal`: `signalFetch(ctx, url)` from the same entry stops with the task, and `effectsPlugin({ fetch: signalFetch })` gives every handler a `ctx.fetch`. [Bind the signal once](pass-the-signal.md#bind-the-signal-once)
|
|
103
103
|
- `make` runs at the first read, so a handler that never uses `ctx.http` pays nothing for it.
|
|
104
104
|
- A task that `ctx.run` starts builds its own value from its own signal, so aborting the child stops only the child's requests.
|
|
105
105
|
- The field is not copied by `{ ...ctx }` and `Object.keys(ctx)` does not list it. Read `ctx.http` first if you copy a context.
|
package/docs/guide.md
CHANGED
|
@@ -377,7 +377,7 @@ Use strings for the plain case. Use objects when other code already builds actio
|
|
|
377
377
|
|
|
378
378
|
**Problem.** The handler of an action is in the file of the feature, but a test needs a fake, or another layer owns the work. If the handler sits in `createAction`, the event file is tied to the work.
|
|
379
379
|
|
|
380
|
-
**What to do.** Create the action without a handler: it is an event. Bind the real handler with `
|
|
380
|
+
**What to do.** Create the action without a handler: it is an event. Bind the real handler with `actions.handle` when the bus is made. A later `actions.handle` binds a newer handler that wins until you remove it.
|
|
381
381
|
|
|
382
382
|
```js
|
|
383
383
|
// user.js
|
|
@@ -389,16 +389,16 @@ export const getUser = createAction("user/get"); // no handler yet: an event
|
|
|
389
389
|
```js
|
|
390
390
|
// bus.js
|
|
391
391
|
import { createBus } from "nanoeffects";
|
|
392
|
-
import { actionsPlugin
|
|
392
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
393
393
|
import { getUser } from "./user";
|
|
394
394
|
|
|
395
395
|
export const bus = createBus();
|
|
396
396
|
|
|
397
|
-
// setHandler — binds the real handler to the action
|
|
398
|
-
const realUser = setHandler(getUser, (id) => api.user(id));
|
|
399
|
-
|
|
400
397
|
// actionsPlugin — the registry of bound handlers of this bus
|
|
401
|
-
export const actions = bus.use(actionsPlugin(
|
|
398
|
+
export const actions = bus.use(actionsPlugin());
|
|
399
|
+
|
|
400
|
+
// actions.handle — binds the real handler to the action
|
|
401
|
+
actions.handle(getUser, (id) => api.user(id));
|
|
402
402
|
```
|
|
403
403
|
|
|
404
404
|
```js
|
package/docs/package.md
CHANGED
|
@@ -21,7 +21,7 @@ Each row is optional. The "when" column says what need brings you to it.
|
|
|
21
21
|
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` | `emit` must return a result, or domain logic leaves the UI |
|
|
22
22
|
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events | `on` must take a function that picks events |
|
|
23
23
|
| `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value | a store or client must be on every task |
|
|
24
|
-
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal`; `
|
|
24
|
+
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal`; `signalFetch(ctx, url)`: a `fetch` that stops with its task; `fromSignal`: a client made from the signal of each task | an outside `AbortSignal` (request, React Query) must stop tasks, or a handler must not pass the signal by hand |
|
|
25
25
|
| `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` | you write classes, not functions |
|
|
26
26
|
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) | one action or module needs its own fields |
|
|
27
27
|
| `nanoeffects/redux` | `createEffectsMiddleware` | you already use Redux |
|
package/docs/pass-the-signal.md
CHANGED
|
@@ -22,25 +22,36 @@ A policy such as `latest` cancels an older call by aborting its `ctx.signal`. Th
|
|
|
22
22
|
|
|
23
23
|
## Bind the signal once
|
|
24
24
|
|
|
25
|
-
The surest fix is a handler
|
|
25
|
+
The surest fix is a handler that never writes a signal. `signalFetch` is a `fetch` that takes `ctx` first, like `retry(ctx, ...)`: the request carries `ctx.signal`, so there is nothing to forget.
|
|
26
26
|
|
|
27
27
|
```js
|
|
28
|
-
//
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import axios from "axios";
|
|
28
|
+
// search-handlers.js
|
|
29
|
+
import { latest } from "nanoeffects/policies";
|
|
30
|
+
import { signalFetch } from "nanoeffects/signal";
|
|
31
|
+
import { bus } from "./bus";
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
fetch:
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
bus.on(
|
|
34
|
+
"search",
|
|
35
|
+
latest(async (query, ctx) => {
|
|
36
|
+
// signalFetch — fetch with ctx.signal: it stops when the policy cancels the handler
|
|
37
|
+
const response = await signalFetch(ctx, `/api?q=${query}`);
|
|
38
|
+
ctx.emit("results", await response.json());
|
|
40
39
|
}),
|
|
41
40
|
);
|
|
42
41
|
```
|
|
43
42
|
|
|
43
|
+
To get `ctx.fetch` in every handler, put the effect on the context once. `effectsPlugin` binds it to each task, so the handler drops the `ctx` argument:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// bus.js
|
|
47
|
+
import { createBus } from "nanoeffects";
|
|
48
|
+
import { effectsPlugin } from "nanoeffects/effects";
|
|
49
|
+
import { signalFetch } from "nanoeffects/signal";
|
|
50
|
+
|
|
51
|
+
// effectsPlugin — ctx.fetch is signalFetch bound to the task that reads it
|
|
52
|
+
export const bus = createBus().use(effectsPlugin({ fetch: signalFetch }));
|
|
53
|
+
```
|
|
54
|
+
|
|
44
55
|
```js
|
|
45
56
|
// search-handlers.js
|
|
46
57
|
import { latest } from "nanoeffects/policies";
|
|
@@ -48,10 +59,27 @@ import { bus } from "./bus";
|
|
|
48
59
|
|
|
49
60
|
bus.on(
|
|
50
61
|
"search",
|
|
51
|
-
latest(async (query,
|
|
52
|
-
// no signal
|
|
53
|
-
const response = await
|
|
54
|
-
|
|
62
|
+
latest(async (query, { fetch, emit }) => {
|
|
63
|
+
// no signal and no ctx argument: fetch stops when the policy cancels the handler
|
|
64
|
+
const response = await fetch(`/api?q=${query}`);
|
|
65
|
+
emit("results", await response.json());
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A client that is not an effect, such as an `axios` instance, is made from the signal of each task with `fromSignal`:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// bus.js
|
|
74
|
+
import { createBus } from "nanoeffects";
|
|
75
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
76
|
+
import { fromSignal } from "nanoeffects/signal";
|
|
77
|
+
import axios from "axios";
|
|
78
|
+
|
|
79
|
+
// fromSignal — ctx.http is made from the signal of the task that reads it
|
|
80
|
+
export const bus = createBus().use(
|
|
81
|
+
contextPlugin({
|
|
82
|
+
http: fromSignal((signal) => axios.create({ signal })),
|
|
55
83
|
}),
|
|
56
84
|
);
|
|
57
85
|
```
|
|
@@ -123,7 +151,7 @@ Each row is a situation where the signal is easy to lose. The right column says
|
|
|
123
151
|
| A function takes the signal and never uses it: `search(query, options) { return fetch(url); }` | Pass the signal on inside it. Only type information finds this in the linter; a test finds it for sure | types | |
|
|
124
152
|
| The signal is wired by hand: `signal.addEventListener("abort", ...)` | Nothing, the handler is not checked step by step | accepted | |
|
|
125
153
|
| `bus.emit` in a handler | Use `ctx.emit`: only it is dropped after the handler was cancelled | yes | |
|
|
126
|
-
| A request that keeps running after the cancel, on Node | Pass the signal, or use `ctx
|
|
154
|
+
| A request that keeps running after the cancel, on Node | Pass the signal, or use `signalFetch(ctx, url)` | | yes |
|
|
127
155
|
| Results emitted after the cancel | The handler ignored the signal. Pass it, or check `ctx.signal.aborted` | | yes |
|
|
128
156
|
| `ctx.on` after the cancel | The same | | yes |
|
|
129
157
|
|
package/docs/policies.md
CHANGED
|
@@ -24,7 +24,7 @@ bus.on(
|
|
|
24
24
|
);
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Pass `signal` to `fetch` (and the like) so the request cancels. To not pass it at all, bind it once: with `ctx
|
|
27
|
+
Pass `signal` to `fetch` (and the like) so the request cancels. To not pass it at all, bind it once: with `signalFetch(ctx, url)` the request stops with the handler and there is nothing to forget ([Keep the signal through a handler](pass-the-signal.md#bind-the-signal-once)).
|
|
28
28
|
|
|
29
29
|
## Choose a policy
|
|
30
30
|
|