nanoeffects 0.27.0 → 0.29.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 +26 -24
- package/dist/{action-creator-D_c223C7.d.ts → action-creator-jeEqIKgK.d.ts} +73 -32
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-0uYn_kRj.js +1 -0
- package/dist/context-DtUnzC-1.js +1 -0
- package/dist/context-types-Bme1mzkn.d.ts +31 -0
- package/dist/context.d.ts +8 -3
- package/dist/context.js +1 -1
- package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
- package/dist/dev.d.ts +2 -2
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/event-DVcW0mME.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +3 -3
- package/dist/match.js +1 -1
- package/dist/{module-WX0Gngnt.d.ts → module-BWMW83qk.d.ts} +38 -38
- package/dist/notrace/{action-creator-Bux7Zzsl.d.ts → action-creator-xpQ_I2h8.d.ts} +73 -32
- package/dist/notrace/actions.d.ts +52 -52
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-37nlVljc.js +1 -0
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-CATXOxtj.d.ts → plugin-6LhNx0th.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +43 -1
- package/dist/observable.js +1 -1
- package/dist/onAbort-BgMfspK4.js +1 -0
- package/dist/oop.d.ts +3 -3
- package/dist/oop.js +1 -1
- package/dist/{plugin-vvkmIIz2.d.ts → plugin-BaErevLy.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +9 -9
- package/dist/signal-field-key-CWT-3OXC.js +1 -0
- package/dist/signal.d.ts +34 -6
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +20 -5
- package/dist/trace.js +1 -1
- package/dist/{types-Bf-LX_tE.d.ts → types-D5xsR37j.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-CPjx0sLm.d.ts → with-policy-BZVRBUOm.d.ts} +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-C6BIiPIi.js} +1 -1
- package/docs/README.md +14 -1
- package/docs/actions.md +28 -2
- package/docs/chat-example.md +16 -4
- package/docs/chat-widget.md +135 -0
- package/docs/context.md +33 -1
- package/docs/effects.md +20 -9
- package/docs/guide.md +21 -3
- package/docs/migrating-from-effector.md +11 -0
- package/docs/observable.md +43 -0
- package/docs/oop.md +18 -1
- package/docs/package.md +22 -13
- package/docs/plugins.md +69 -11
- package/docs/query-libraries.md +273 -5
- package/docs/react.md +12 -2
- package/docs/scope.md +14 -2
- package/docs/semantics.md +10 -0
- package/docs/server.md +3 -3
- package/docs/stability.md +2 -2
- package/docs/starter.md +3 -3
- package/docs/tracing.md +77 -9
- package/docs/typescript.md +1 -1
- package/llms-full.txt +816 -72
- package/llms.txt +8 -7
- package/package.json +6 -6
- package/skills/nanoeffects/SKILL.md +2 -2
- package/dist/bus-DQDHUN5e.js +0 -1
- package/dist/context-UcmrAIhP.js +0 -1
- package/dist/context-types-nGtzhjxD.d.ts +0 -14
- package/dist/event-0RIBGYqS.js +0 -1
- package/dist/notrace/bus-CGi2lrd5.js +0 -1
- package/dist/notrace/promise-CSLpzwSR.js +0 -1
package/dist/scope.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as EventMap, I as WithSignal, c as Scope, f as Effect, m as Signaled, p as Effects, s as EmitScope, v as Bus } from "./action-creator-
|
|
2
|
-
import { n as ScopeSlots, r as Scoped } from "./types-
|
|
1
|
+
import { H as EventMap, I as WithSignal, c as Scope, f as Effect, m as Signaled, p as Effects, s as EmitScope, v as Bus } from "./action-creator-jeEqIKgK.js";
|
|
2
|
+
import { n as ScopeSlots, r as Scoped } from "./types-D5xsR37j.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
|
|
@@ -16,7 +16,7 @@ import { n as ScopeSlots, r as Scoped } from "./types-Bf-LX_tE.js";
|
|
|
16
16
|
* ```ts
|
|
17
17
|
* const first = effect(<T>(ctx: WithSignal, items: T[]) => items[0]);
|
|
18
18
|
*
|
|
19
|
-
* const getUser =
|
|
19
|
+
* const getUser = createAction(
|
|
20
20
|
* "user/get",
|
|
21
21
|
* (id: string, { first }) => first([id]), // first: <T>(items: T[]) => T
|
|
22
22
|
* { first },
|
|
@@ -27,11 +27,11 @@ export declare function effect<A extends unknown[], R>(fn: (ctx: WithSignal, ...
|
|
|
27
27
|
//#endregion
|
|
28
28
|
//#region src/scope/plugin.d.ts
|
|
29
29
|
/**
|
|
30
|
-
* Switches scopes on for a bus: the scope given to
|
|
31
|
-
*
|
|
30
|
+
* Switches scopes on for a bus: the scope given to an action, a job
|
|
31
|
+
* or a module (an object whose keys go on `ctx`, with `$effects` bound to
|
|
32
32
|
* the task), and the second argument of `emit`, put fields on
|
|
33
33
|
* the context of the tasks they are for. A stronger layer replaces a field of
|
|
34
|
-
* the same name: `emit`, then action /
|
|
34
|
+
* the same name: `emit`, then action / job, then module, then the bus.
|
|
35
35
|
* Fields of the library (`signal`, `emit`, `run`, …) are never replaced. Returns
|
|
36
36
|
* the same bus.
|
|
37
37
|
*
|
|
@@ -41,13 +41,13 @@ export declare function effect<A extends unknown[], R>(fn: (ctx: WithSignal, ...
|
|
|
41
41
|
* @example
|
|
42
42
|
* ```ts
|
|
43
43
|
* import { createBus } from "nanoeffects";
|
|
44
|
-
* import {
|
|
44
|
+
* import { createAction } from "nanoeffects/actions";
|
|
45
45
|
* import { scopePlugin } from "nanoeffects/scope";
|
|
46
46
|
*
|
|
47
47
|
* const bus = createBus().use(scopePlugin()); // scopes work on this bus
|
|
48
48
|
*
|
|
49
|
-
* const getUser =
|
|
50
|
-
* http, // a field of this
|
|
49
|
+
* const getUser = createAction("user/get", (id: string, { http }) => http.get(id), {
|
|
50
|
+
* http, // a field of this action only
|
|
51
51
|
* });
|
|
52
52
|
* bus.emit(getUser("1"), { http: fakeHttp }); // this call only
|
|
53
53
|
* ```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=Symbol.for(`nanoeffects.signalField`);export{e as t};
|
package/dist/signal.d.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
import {
|
|
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-jeEqIKgK.js";
|
|
2
|
+
import { n as FromSignal } from "./context-types-Bme1mzkn.js";
|
|
3
|
+
//#region src/signal/from-signal.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Marks a field of `contextPlugin` as a value that needs the signal of its task: a
|
|
6
|
+
* client, a database handle. `make` runs when the task first reads the field, with the
|
|
7
|
+
* `ctx.signal` of that task, and runs once for it. A child task (`ctx.run`) makes its
|
|
8
|
+
* own with its own signal. Without `contextPlugin` the marker means nothing.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const bus = createBus().use(
|
|
13
|
+
* contextPlugin({
|
|
14
|
+
* logger, // a plain field, the same for every task
|
|
15
|
+
* http: fromSignal((signal) => axios.create({ signal })), // one client per task, aborted with it
|
|
16
|
+
* }),
|
|
17
|
+
* );
|
|
18
|
+
*
|
|
19
|
+
* bus.on("user/open", (id, { http }) => http.get(`/users/${id}`));
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare const fromSignal: <T>(make: (signal: AbortSignal) => T) => FromSignal<T>;
|
|
23
|
+
//#endregion
|
|
2
24
|
//#region src/signal/plugin.d.ts
|
|
3
25
|
/** What the second argument of `emit` takes besides the fields of a scope. */
|
|
4
26
|
type SignalOption = {
|
|
@@ -7,8 +29,8 @@ type SignalOption = {
|
|
|
7
29
|
};
|
|
8
30
|
/** `emit` of a bus with `signalPlugin`: the call forms take `{ signal }` as their second argument. */
|
|
9
31
|
type SignalEmit<E extends EventMap> = Emit<E> & {
|
|
10
|
-
<P, R, F>(call:
|
|
11
|
-
<P, R>(call:
|
|
32
|
+
<P, R, F>(call: HandledCall<P, R> & WithScope<F>, options: SignalOption & EmitScope<E, F>): R;
|
|
33
|
+
<P, R>(call: HandledCall<P, R>, options: SignalOption & EmitScope<E>): R;
|
|
12
34
|
(event: EventLike<E>, options: SignalOption & EmitScope<E>): void;
|
|
13
35
|
};
|
|
14
36
|
/** A bus with `signalPlugin`: the same bus, with the `emit` that takes a signal. */
|
|
@@ -16,16 +38,22 @@ type SignalBus<E extends EventMap> = Bus<E> & {
|
|
|
16
38
|
emit: SignalEmit<E>;
|
|
17
39
|
};
|
|
18
40
|
/**
|
|
41
|
+
* Also lets `bus.on(event, handler, { signal })` and `bus.once` remove the
|
|
42
|
+
* listener when the signal aborts, as `addEventListener` does, so a component or
|
|
43
|
+
* a script that holds a signal keeps no `unbind`. A handler that already runs is
|
|
44
|
+
* not aborted; a signal that is aborted already adds no listener. `off(event, fn)`
|
|
45
|
+
* does not end the link: the returned function does.
|
|
46
|
+
*
|
|
19
47
|
* Lets `bus.emit(call, { signal })` tie the tasks that this emit starts to an
|
|
20
48
|
* outside `AbortSignal`: when it aborts, their `ctx.signal` aborts with the same
|
|
21
49
|
* reason, so a handler, `latest`, `retry` and `timeout` stop as for any abort. It
|
|
22
50
|
* is made for libraries that own the request and give you a signal, such as the
|
|
23
51
|
* `queryFn` of React Query. The tasks that count are those made while the call
|
|
24
|
-
* to `emit` runs (the
|
|
52
|
+
* to `emit` runs (the action or job and the listeners of its type, and their
|
|
25
53
|
* `ctx.run` children); a separate `emit` that a handler makes later is not
|
|
26
54
|
* linked. `emit` returns what it returns without the plugin.
|
|
27
55
|
*
|
|
28
|
-
* For
|
|
56
|
+
* For an action or a job the link ends when its promise settles; a handler
|
|
29
57
|
* that returns without a promise is finished with the call, so it is not
|
|
30
58
|
* linked after it. For an event the link stays until the signal aborts, so
|
|
31
59
|
* give each request its own signal.
|
|
@@ -48,4 +76,4 @@ type SignalBus<E extends EventMap> = Bus<E> & {
|
|
|
48
76
|
*/
|
|
49
77
|
export declare const signalPlugin: () => <E extends EventMap>(bus: Bus<E>) => SignalBus<E>;
|
|
50
78
|
//#endregion
|
|
51
|
-
export type { SignalBus, SignalEmit, SignalOption };
|
|
79
|
+
export type { FromSignal, SignalBus, SignalEmit, SignalOption };
|
package/dist/signal.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{t as e}from"./onAbort-BgMfspK4.js";import{n as t}from"./promise-CSLpzwSR.js";import{n}from"./bus-0uYn_kRj.js";import{t as r}from"./signal-field-key-CWT-3OXC.js";const i=e=>({[r]:e}),a=(t,n)=>(r,i,a)=>{let o=a?.signal;if(!o)return t(r,i,a);let s=t(r,n?(e,t)=>(c(),i(e,t)):i,a),c=e({signal:o},s);return()=>{c(),s()}},o=e=>typeof e==`object`&&!!e,s=(e,t)=>{let n=t[0];if(!o(n)||!o(e)&&typeof e!=`function`)return;let r=Reflect.get(n,`signal`);return r instanceof AbortSignal?r:void 0},c=(r,i,a,o)=>{let s=e({signal:r},e=>{for(let{target:t}of i)t.abort(e)});return t(o)?o.then(s,s):n(a)&&s(),o},l=()=>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=s(e,r);if(!(a||t))return i(e,...r);let o=t,l=n,u=[];t=a?u:void 0,n=a;let d;try{d=i(e,...r)}finally{t=o,n=l}return a?c(a,u,e,d):d};let o=e.on,l=e.once;return e.on=a(o,!1),e.once=a(l,!0),e};export{i as fromSignal,l as signalPlugin};
|
package/dist/svelte.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { H as EventMap, T as EventKey, a as FreeActionCreator, t as ActionCreator, v as Bus } from "./action-creator-jeEqIKgK.js";
|
|
2
2
|
import { t as StateOf } from "./state-of-C19H6BoV.js";
|
|
3
3
|
import { Readable } from "svelte/store";
|
|
4
4
|
//#region src/svelte/plugin.d.ts
|
package/dist/trace.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as Tracer, H as EventMap, J as Trace, K as Phase, V as EffectName, X as TraceLogger, Y as TraceEvent, v as Bus } from "./action-creator-
|
|
1
|
+
import { B as Tracer, H as EventMap, J as Trace, K as Phase, V as EffectName, X as TraceLogger, Y as TraceEvent, v as Bus } from "./action-creator-jeEqIKgK.js";
|
|
2
2
|
//#region src/trace/console.d.ts
|
|
3
3
|
type ConsoleLoggerOptions = {
|
|
4
4
|
/** Defaults to `globalThis.console`. */
|
|
@@ -52,7 +52,7 @@ export declare const performanceLogger: (options?: PerformanceLoggerOptions) =>
|
|
|
52
52
|
/**
|
|
53
53
|
* Names and groups what the trace shows, on a bus that has a tracer:
|
|
54
54
|
*
|
|
55
|
-
* - `traceGroupId` in the scope of
|
|
55
|
+
* - `traceGroupId` in the scope of an action, a job or an action goes to every
|
|
56
56
|
* task that the call starts;
|
|
57
57
|
* - `useEmitter` and `bus.emitter` emit from a root task named by their
|
|
58
58
|
* `traceId`, so a chain says which subscription began it;
|
|
@@ -80,12 +80,21 @@ export declare const tracePlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus
|
|
|
80
80
|
//#endregion
|
|
81
81
|
//#region src/trace/recorder.d.ts
|
|
82
82
|
type Recorder<E = Record<string, unknown>> = TraceLogger<E> & {
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* The events received so far, oldest first. Without a `capacity` it is one
|
|
85
|
+
* live array; with one, each read is a new array that is a snapshot.
|
|
86
|
+
*/
|
|
84
87
|
readonly events: TraceEvent<E>[];
|
|
85
88
|
clear: () => void;
|
|
86
89
|
};
|
|
87
90
|
/**
|
|
88
|
-
* Trace logger that keeps events in memory
|
|
91
|
+
* Trace logger that keeps events in memory. With no option it keeps all of
|
|
92
|
+
* them, for tests. With `capacity` it keeps the last events, to send when
|
|
93
|
+
* something goes wrong.
|
|
94
|
+
*
|
|
95
|
+
* `payload` and `error` are kept as they are, and can hold private data: cut
|
|
96
|
+
* what you send. An event has no time, and a `parentId` can name an event that
|
|
97
|
+
* was pushed out: read it as a root.
|
|
89
98
|
*
|
|
90
99
|
* @example
|
|
91
100
|
* ```ts
|
|
@@ -95,9 +104,15 @@ type Recorder<E = Record<string, unknown>> = TraceLogger<E> & {
|
|
|
95
104
|
* bus.emit("search", "re");
|
|
96
105
|
* bus.emit("search", "redux");
|
|
97
106
|
* recorder.events.map((e) => e.phase); // ["start", "cancel", "start", "done"]
|
|
107
|
+
*
|
|
108
|
+
* const lastEvents = createTraceRecorder<Events>({ capacity: 500 });
|
|
98
109
|
* ```
|
|
99
110
|
*/
|
|
100
|
-
export declare const createTraceRecorder: <E = Record<string, unknown>>(
|
|
111
|
+
export declare const createTraceRecorder: <E = Record<string, unknown>>(
|
|
112
|
+
/** `capacity`: keep the last N events (a ring buffer); not a positive integer, or left out: keep all. */
|
|
113
|
+
options?: {
|
|
114
|
+
capacity?: number;
|
|
115
|
+
}) => Recorder<E>;
|
|
101
116
|
//#endregion
|
|
102
117
|
//#region src/trace/redux.d.ts
|
|
103
118
|
type ReduxConnection = {
|
package/dist/trace.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{n as e,t}from"./onAbort-BgMfspK4.js";import{a as n,c as r,n as i,o as a,r as o,s}from"./context-DtUnzC-1.js";import{n as c}from"./promise-CSLpzwSR.js";import{t as l}from"./origin-key-BbWN3gmN.js";import{t as u}from"./scope-key-B7l7mUS5.js";const d=(e={})=>{let t=e.console??globalThis.console,n=e.prefix??`[nanoeffects]`,r=/* @__PURE__ */ new Map;return({id:e,parentId:i,type:a,effect:o,phase:s,payload:c,error:l,traceGroupId:u})=>{r.has(e)||r.set(e,typeof i==`number`?(r.get(i)??0)+1:0);let d=` `.repeat(r.get(e)??0);s!==`start`&&r.delete(e);let f=u===void 0?``:` [${u}]`,p=`${n}${f} ${d}${a} · ${o} ${s}`;s===`error`?t?.error(p,l):t?.debug(p,c)}},f=(e={})=>{let t=e.performance??globalThis.performance,n=/* @__PURE__ */ new Map;return({id:e,parentId:r,rootId:i,traceGroupId:a,type:o,effect:s,phase:c,error:l})=>{if(!t)return;if(c===`start`){n.set(e,t.now());return}let u=`${o} · ${s} (${c})`,d={id:e,parentId:r,...i!==void 0&&{rootId:i},...a!==void 0&&{traceGroupId:a},phase:c,...c===`error`&&{error:String(l)}},f=n.get(e);n.delete(e),typeof f==`number`?t.measure(u,{start:f,end:t.now(),detail:d}):t.mark(u,{detail:d})}};let p;const m=()=>p,h=(e,t)=>{let n=p;p=e;try{return t()}finally{p=n}},g=(e,t,n={})=>{let{trace:r}=e;if(!r){e.emit(t);return}let{traceId:a,traceGroupId:o}=n,{ctx:c,unlink:l}=i({event:{type:a??t.type,payload:t,...o===void 0?{}:{traceGroupId:o}},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e.ctx,addContext:e.addContext},tracer:r});s(c,`run`,`start`);try{c.emit(t),s(c,`run`,`done`)}catch(e){throw s(c,`run`,`error`,e),e}finally{l()}},_=e=>typeof e==`string`?e:Reflect.get(Object(e),`type`),v=(e,t)=>typeof e==`object`?Reflect.get(Object(e),`payload`):t[0],y=()=>e=>{if(!(r&&e.trace))return e;let{trace:t}=e,i=(e,n)=>{let r=t.task({type:_(e),payload:v(e,n)},o());t.report(r,`emit`,`done`)},s=e.emit;e.emit=(e,...t)=>h(e,()=>(i(e,t),s(e,...t))),Reflect.set(e,l,(t,n)=>{g(e,t,n)});let c=/* @__PURE__ */ new WeakMap,u=(e,t,n)=>{let r=c.get(t)??/* @__PURE__ */ new Map,i=_(e);r.set(i,[...r.get(i)??[],n]),c.set(t,r)},d=(e,t)=>(c.get(t)?.get(_(e)))?.pop(),f=(e,t)=>(r,i)=>(n(i,t),a(i,`run`,()=>e(r,i))),p=(e,t)=>(n,r,i)=>{let a=Reflect.get(Object(i),`traceId`);if(typeof a!=`string`)return e(n,r,i);let o=t?(e,t)=>(d(n,r),f(r,a)(e,t)):f(r,a);u(n,r,o);let s=e(n,o,i);return()=>{s()}},m=e.off,y=(e,t)=>{t===void 0?m(e):m(e,d(e,t)??t)},b=e.on,x=e.once;return e.on=p(b,!1),e.once=p(x,!0),e.off=y,e},b=e=>{let t=e?.capacity??0,n=Number.isInteger(t)&&t>0?t:0,r=[],i=0,a=e=>{n===0?r.push(e):(r[i%n]=e,i+=1)},o=()=>{r.length=0,i=0},s=()=>{let e=Math.min(i,n),t=i>n?i%n:0,a=[];for(let i=0;i<e;i+=1){let e=r[(t+i)%n];e&&a.push(e)}return a},c=Object.assign(a,{clear:o,events:r});return n>0&&Object.defineProperty(c,"events",{get:s}),c},x=()=>{},S=/* @__PURE__ */ new Set([`cancel`,`drop`,`error`]),C=()=>!1,w=()=>!0,T=e=>{if(typeof e==`function`)return e;if(Array.isArray(e)){let t=new Set(e);return({phase:e})=>t.has(e)}return e?w:C},E=(e={})=>{let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;if(!t)return x;let{starts:n=!1,depth:r=0,maxAge:i}=e,a=T(e.policies),o=e.payload??`essential`,s=t.connect({name:e.name??`nanoeffects`,...i!==void 0&&{maxAge:i}}),c=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Map,u=()=>({running:Object.fromEntries(c)});s.init(u());let d=(e,t,n)=>n===0?a({effect:e,phase:t}):n<=r,f=(e,t)=>{e.phase===`start`?(l.set(e.id,t),d(e.effect,`start`,t)&&c.set(e.id,{type:e.type,effect:e.effect,...e.traceGroupId!==void 0&&{traceGroupId:e.traceGroupId}})):(l.delete(e.id),c.delete(e.id))},p=!1,m=!1,h=(e,t)=>{p||m||t!==0||e===`start`||(m=!0,globalThis.console?.warn(`[nanoeffects] reduxDevtools got a task but no emit event: add bus.use(tracePlugin()) to see the emits.`))},g=e=>{let{payload:t,...n}=e;return o===`all`||o===`essential`&&(e.effect===`emit`||S.has(e.phase))?{...n,payload:t}:n},_=(e,t)=>{s.send({...g(e),type:`${e.type}/${e.effect}/${e.phase}`,event:e.type,...e.phase===`error`&&{error:t instanceof Error?t.message:t}},u())};return({error:e,...t})=>{if(t.effect===`emit`){p=!0,s.send({...g(t),event:t.type},u());return}let r=(t.parentId===void 0?-1:l.get(t.parentId)??-1)+1;f(t,r),h(t.phase,r),(t.phase===`error`&&r===0||d(t.effect,t.phase,r))&&(t.phase!==`start`||n)&&_(t,e)}},D=t=>{let n=typeof t==`function`?[t]:[...t??[]];return n.length===0?null:t=>{for(let r of n)try{r(t)}catch(t){e(`trace logger failed`,t)}}},O=()=>{},k=e=>{if(typeof e.traceGroupId==`string`)return e.traceGroupId;let t=m();if(typeof t!=`object`&&typeof t!=`function`||t===null)return;let n=Reflect.get(t,u),r=typeof n==`object`&&n?Reflect.get(n,`traceGroupId`):void 0;return typeof r==`string`?r:void 0},A=(e,n={})=>{let r=D(e),{filter:i,sample:a=1}=n,o=0,s=e=>typeof a==`function`?a({type:e.type,payload:e.payload}):Math.random()<a,l={task:(e,t)=>({id:++o,root:t?.root??o,parent:t?.id,type:e.type,payload:e.payload,sampled:t?.sampled??s(e),group:k(e)??t?.group}),rename:(e,t)=>{e.type=t},report:(e,t,n,a)=>{if(!e.sampled)return;let o={id:e.id,type:e.type,effect:t,phase:n,payload:e.payload};typeof e.parent==`number`&&(o.parentId=e.parent),e.root!==e.id&&(o.rootId=e.root),e.group!==void 0&&(o.traceGroupId=e.group),n===`error`&&(o.error=a),(!i||i(o))&&r?.(o)},settler:(e,t)=>{if(!e.sampled)return O;let n=!1;return(r,i)=>{n||(n=!0,l.report(e,t,r,i))}},run:(e,n,r,i)=>{if(!e.sampled)return i();let a=l.settler(e,n);l.report(e,n,`start`),t({signal:r},()=>a(`cancel`));let o;try{o=i()}catch(e){throw a(`error`,e),e}return c(o)?Promise.resolve(o).then(e=>(a(`done`),e),e=>{throw a(`error`,e),e}):(a(`done`),o)}};return l};export{d as consoleLogger,b as createTraceRecorder,A as createTracer,f as performanceLogger,E as reduxDevtools,y as tracePlugin};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as EventMap, c as Scope, l as WithFields, p as Effects } from "./action-creator-
|
|
2
|
-
import { t as BoundOf } from "./plugin-
|
|
1
|
+
import { H as EventMap, c as Scope, l as WithFields, p as Effects } from "./action-creator-jeEqIKgK.js";
|
|
2
|
+
import { t as BoundOf } from "./plugin-BaErevLy.js";
|
|
3
3
|
//#region src/scope/types.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The slots `scopePlugin` fills for `modulePlugin`. They are not on `Bus`: a bus
|
package/dist/vue.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { H as EventMap, T as EventKey, a as FreeActionCreator, t as ActionCreator, v as Bus } from "./action-creator-jeEqIKgK.js";
|
|
2
2
|
import { t as OriginOptions } from "./origin-key-CxK0SkeG.js";
|
|
3
3
|
import { t as StateOf } from "./state-of-C19H6BoV.js";
|
|
4
4
|
import { ShallowRef, WatchSource, WatchStopHandle } from "vue";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as PayloadOf, H as EventMap, q as Task } from "./action-creator-
|
|
1
|
+
import { G as PayloadOf, H as EventMap, q as Task } from "./action-creator-jeEqIKgK.js";
|
|
2
2
|
//#region src/policies/with-policy.d.ts
|
|
3
3
|
/** What `withPolicy` returns: the task as a plain function that can be cancelled. */
|
|
4
4
|
type PolicyFunction<P, R> = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./context-
|
|
1
|
+
import{n as e}from"./context-DtUnzC-1.js";const t=Reflect.get(Symbol,`dispose`),n=n=>{let r=new AbortController,i=async(...t)=>{let i=t[0],{ctx:a,unlink:o}=e({event:{type:`withPolicy`,payload:i},parent:{signal:r.signal}});try{return await n(i,a)}finally{o()}},a=e=>{r.abort(e),r=new AbortController},o=Object.assign(i,{cancel:a});return t&&Reflect.set(o,t,()=>a()),o};export{n as t};
|
package/docs/README.md
CHANGED
|
@@ -8,6 +8,18 @@ Why the library exists (logic and side effects outside React, Vue and Svelte com
|
|
|
8
8
|
|
|
9
9
|
Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-checked and tested).
|
|
10
10
|
|
|
11
|
+
## Start with three imports
|
|
12
|
+
|
|
13
|
+
Most apps need only these. Everything else is optional; read it when you hit its need.
|
|
14
|
+
|
|
15
|
+
| Import | Gives you |
|
|
16
|
+
| ------ | --------- |
|
|
17
|
+
| `nanoeffects` | `createBus`: `on`, `emit`, `ctx` |
|
|
18
|
+
| `nanoeffects/policies` | `latest`, `debounce`, ... for races in async UI |
|
|
19
|
+
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, ... for an unreliable network |
|
|
20
|
+
|
|
21
|
+
The other entries and plugins: [Package entries](package.md) and [Plugins](plugins.md#pick-a-plugin-by-task).
|
|
22
|
+
|
|
11
23
|
## Jump by project need
|
|
12
24
|
|
|
13
25
|
| Project shape | First page |
|
|
@@ -19,6 +31,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
|
|
|
19
31
|
| Already on Redux / Zustand / MobX / Pinia | [Stores](stores.md) |
|
|
20
32
|
| A Node server (Fastify) whose work must stop when the client leaves | [Stop work on a server](server.md) |
|
|
21
33
|
| A React form with async checks | [Build a sign-up form in React](react.md) |
|
|
34
|
+
| A site widget that listens only while open | [Build a chat widget](chat-widget.md) |
|
|
22
35
|
| A Vue app | [Use nanoeffects in a Vue app](vue.md) |
|
|
23
36
|
| A Svelte app | [Use nanoeffects in a Svelte app](svelte.md) |
|
|
24
37
|
| Old responses overwrite new ones | [Cancel stale requests](cancel-stale-requests.md) |
|
|
@@ -39,7 +52,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
|
|
|
39
52
|
- [nanoeffects in a Svelte app](svelte.md) — `eventStore`
|
|
40
53
|
- [Context fields](context.md) — store or client on every task
|
|
41
54
|
- [Use the bus from a class](oop.md) — `extendBus`, `@WithBus`, `@On`, and `dispose()` for the subscriptions of an instance
|
|
42
|
-
- [TypeScript in many files](typescript.md) — keep the types of the bus in
|
|
55
|
+
- [TypeScript in many files](typescript.md) — keep the types of the bus in actions and modules
|
|
43
56
|
- [Errors and unheard events](errors.md)
|
|
44
57
|
- [Package entries](package.md) — which import holds what
|
|
45
58
|
- [Stability](stability.md) — which entries are stable, what is breaking, supported versions
|
package/docs/actions.md
CHANGED
|
@@ -10,6 +10,32 @@ A chat with two entities and three features, jobs under policies and effects, an
|
|
|
10
10
|
|
|
11
11
|
A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
|
|
12
12
|
|
|
13
|
+
## Pick the word: event, action, job, module
|
|
14
|
+
|
|
15
|
+
Four names, one idea each. Take the first row that fits.
|
|
16
|
+
|
|
17
|
+
| Word | What it is | Use it when |
|
|
18
|
+
| ---- | ---------- | ----------- |
|
|
19
|
+
| Event | A name and a payload; `createAction("x")` with no handler. | The UI or a handler tells that something happened. |
|
|
20
|
+
| Action with a handler | The same, plus work: `createAction("x", handler)`. `emit` returns the result. | The caller needs a value back. |
|
|
21
|
+
| Job | An action with a handler that also emits `pending`, `success`, `fail` and `cancel`. | The UI shows a loading state. |
|
|
22
|
+
| Module | A group of listeners, handlers and tasks, mounted and unmounted with one call. | One domain must start and stop as a unit. |
|
|
23
|
+
|
|
24
|
+
Start with events. Add a handler when you need a result, a job when you need a status, a module when you need a unit.
|
|
25
|
+
|
|
26
|
+
## On this page
|
|
27
|
+
|
|
28
|
+
- [Pick the word: event, action, job, module](#pick-the-word-event-action-job-module)
|
|
29
|
+
- [Return a value from `emit` with a handler](#return-a-value-from-emit-with-a-handler)
|
|
30
|
+
- [Describe events, handlers and jobs](#describe-events-handlers-and-jobs)
|
|
31
|
+
- [Actions](#actions)
|
|
32
|
+
- [Bind a handler later](#bind-a-handler-later)
|
|
33
|
+
- [Say what a bound handler returns](#say-what-a-bound-handler-returns)
|
|
34
|
+
- [Jobs](#jobs)
|
|
35
|
+
- [Show a loading state with a job](#show-a-loading-state-with-a-job)
|
|
36
|
+
- [Mount domains with modules](#mount-domains-with-modules)
|
|
37
|
+
- [Give an action or a module its own fields](#give-an-action-or-a-module-its-own-fields)
|
|
38
|
+
|
|
13
39
|
## Return a value from `emit` with a handler
|
|
14
40
|
|
|
15
41
|
Give `createAction` a handler, and `emit` runs it and returns its result. There is one creator for an event and for an action with work behind it; the handler is the only difference.
|
|
@@ -164,7 +190,7 @@ On a bus without an event map you can say it where you call: `const id = bus.emi
|
|
|
164
190
|
- `pending` / `success` / `fail` / `cancel` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/pending`, `<type>/success`, `<type>/fail` or `<type>/cancel`.
|
|
165
191
|
- `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, give `createAction` the handler.
|
|
166
192
|
- Outcome actions are emitted as the task that ran the job (also for a handler bound with `actions.handle`). `success` and `fail` are not sent after abort; `cancel` is, at the moment of the abort.
|
|
167
|
-
- A job without a handler is not an outcome: `emit` throws `No handler for
|
|
193
|
+
- A job without a handler is not an outcome: `emit` throws `No handler for action "..."` and nothing is sent. An action without a handler is an event and does not throw.
|
|
168
194
|
- Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
|
|
169
195
|
- The outcomes are events like any other: with `onUnhandled` set on the bus, it is called for a `pending` or a `cancel` that nobody listens to.
|
|
170
196
|
|
|
@@ -326,7 +352,7 @@ If the `setup` of one module throws, the modules of that call that were already
|
|
|
326
352
|
- **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
|
|
327
353
|
- **Registry.** If `bus.use(actionsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.actions` and `app.actions`.
|
|
328
354
|
- A disposed app cannot mount modules.
|
|
329
|
-
- **The given modules are mounted at the end of the block that made the app**, or at the first use of the app or the bus if that comes first (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `
|
|
355
|
+
- **The given modules are mounted at the end of the block that made the app**, or at the first use of the app or the bus if that comes first (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `actions`). Nothing needs to use the app: `bus.use(modulePlugin([a, b]))` is enough to start them. The plugins that you use after `modulePlugin` in the same block still reach them, because the modules wait until that block has finished. A plugin used after an `await` can come too late: use every plugin before the first `await`. 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).
|
|
330
356
|
- **To mount by hand instead**, pass no modules: `modulePlugin()` starts nothing, and `app.mount(...)` starts the modules when you want.
|
|
331
357
|
- Module context = task context plus `handle`, `use`, and `spawn`:
|
|
332
358
|
- `handle` — give an action a handler (one that has none, or an override).
|
package/docs/chat-example.md
CHANGED
|
@@ -14,6 +14,18 @@ npm run dev
|
|
|
14
14
|
|
|
15
15
|
`npm run dev` starts the server and Vite together. Start it with `CHAOS=off npm run dev` to turn the failures off.
|
|
16
16
|
|
|
17
|
+
## On this page
|
|
18
|
+
|
|
19
|
+
- [Read how the server fails](#read-how-the-server-fails)
|
|
20
|
+
- [See how the files are split](#see-how-the-files-are-split)
|
|
21
|
+
- [Load a history that must not race](#load-a-history-that-must-not-race)
|
|
22
|
+
- [Send a message that can fail](#send-a-message-that-can-fail)
|
|
23
|
+
- [Search where one room is slow](#search-where-one-room-is-slow)
|
|
24
|
+
- [Wait for a room before marking a result](#wait-for-a-room-before-marking-a-result)
|
|
25
|
+
- [Poll for new messages](#poll-for-new-messages)
|
|
26
|
+
- [Know where each value lives](#know-where-each-value-lives)
|
|
27
|
+
- [Where to go next](#where-to-go-next)
|
|
28
|
+
|
|
17
29
|
## Read how the server fails
|
|
18
30
|
|
|
19
31
|
The server keeps its data in memory. These rules are on purpose, and they are the same every run, so you can follow each section below by doing what the table says:
|
|
@@ -48,7 +60,7 @@ The code is in layers, and a file imports only from the layers below it. The com
|
|
|
48
60
|
|
|
49
61
|
That rule is why `show-history` exists. Selecting a room must load its history, but `room` does not know `message` and `message` does not know `room`. The feature `show-history` is the one place that knows both: it listens to `rooms/selected` and starts the job `messages/loadHistory`. The same goes for a message that arrives: `receive-messages` emits `incoming/received`, an event that the `message` entity owns and listens to.
|
|
50
62
|
|
|
51
|
-
A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its
|
|
63
|
+
A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its actions, never through its handlers or its store. All actions live in the events file. An action with no handler says that something happened, and any feature may listen: `rooms/selected` is heard by two. An action with a handler is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createAction`, so the action works wherever it is called and can return a result, and the module binds it with `module.use` for as long as it is mounted. A job is an action with a handler that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
|
|
52
64
|
|
|
53
65
|
The layers come from [Feature-Sliced Design](https://feature-sliced.design/), reduced to the three that an app of this size needs.
|
|
54
66
|
|
|
@@ -64,7 +76,7 @@ A job sends `pending` when its handler starts and exactly one of `success`, `fai
|
|
|
64
76
|
// message.js
|
|
65
77
|
import { createJob } from "nanoeffects/actions";
|
|
66
78
|
|
|
67
|
-
// createJob — a
|
|
79
|
+
// createJob — an action with a handler that also sends pending, success, fail and cancel
|
|
68
80
|
export const loadHistory = createJob("messages/loadHistory");
|
|
69
81
|
```
|
|
70
82
|
|
|
@@ -195,7 +207,7 @@ export const goToResult = async (hit, ctx) => {
|
|
|
195
207
|
ctx.emit("rooms/selected", hit.roomId);
|
|
196
208
|
await loaded;
|
|
197
209
|
}
|
|
198
|
-
// ctx.emit with a
|
|
210
|
+
// ctx.emit with an action that has a handler — the message entity marks the message
|
|
199
211
|
ctx.emit(markMessage(hit.id));
|
|
200
212
|
};
|
|
201
213
|
```
|
|
@@ -247,6 +259,6 @@ Each entity and each feature that has state keeps it in a [Zustand](https://zust
|
|
|
247
259
|
|
|
248
260
|
## Where to go next
|
|
249
261
|
|
|
250
|
-
- The API of
|
|
262
|
+
- The API of actions, jobs and modules: [Actions, jobs and modules](actions.md).
|
|
251
263
|
- Policies such as `latest` and `queue`: [Policies](policies.md). Effects such as `allSettled`, `take` and `poll`: [Effects](effects.md).
|
|
252
264
|
- A smaller project with two domains: [Start with domains](starter.md#start-with-domains). Its two domains do not talk to each other, so it needs no layers.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Build a chat widget that listens only while it is open
|
|
2
|
+
|
|
3
|
+
Use this page when a site has a chat with a bot behind a launcher button, and the chat must cost nothing while it is closed. Closed means no subscriptions on the bus, no polling, no keyboard listener, and no request still running. Opening turns all of it on, and closing turns all of it off in one call.
|
|
4
|
+
|
|
5
|
+
The code is in [`examples/chat-widget`](../examples/chat-widget): plain DOM and TypeScript, a fake bot in the browser, no server. Run it on [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat-widget), or on your machine:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd examples/chat-widget
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It is a level D example: the logic lives in one module, and the page only mounts and unmounts it. For an app with several features see [Build a chat with entities, features, modules and a small server](chat-example.md).
|
|
14
|
+
|
|
15
|
+
## Know what happens on open and on close
|
|
16
|
+
|
|
17
|
+
| Moment | What happens |
|
|
18
|
+
| ------ | ------------ |
|
|
19
|
+
| Open | The module is mounted: the bot handler is subscribed, the operator messages are polled, the `Escape` listener is added. The panel is drawn and listens to the bus. |
|
|
20
|
+
| The user sends a text | The bot is asked, one text after another, with a time limit and a retry. |
|
|
21
|
+
| `Escape` | The module emits `chat/closeRequested`; the launcher closes the chat. |
|
|
22
|
+
| Close | The panel is removed and unsubscribed, the module is unmounted. A request in flight is aborted, the polling stops, the `Escape` listener goes away. |
|
|
23
|
+
|
|
24
|
+
## Find the parts in the files
|
|
25
|
+
|
|
26
|
+
| File | Job |
|
|
27
|
+
| ---- | --- |
|
|
28
|
+
| `src/bus.ts` | The bus, and `modulePlugin()` with no module: the launcher mounts the module itself. |
|
|
29
|
+
| `src/chat.ts` | The events only: `messageSent`, `messageAdded`, `typingChanged`, `closeRequested`. |
|
|
30
|
+
| `src/chat-handlers.ts` | `askBot` (`timeout` and `retry` inside) and `watchOperator` (`poll`): plain `async` functions that know nothing about the page. |
|
|
31
|
+
| `src/chat-module.ts` | The list of what is alive while the chat is open. |
|
|
32
|
+
| `src/panel.ts` | The view: shows the lines, sends the texts, returns one function that removes it. |
|
|
33
|
+
| `src/main.ts` | The launcher: the only file that knows both the module and the panel; it listens for `closeRequested` only while the chat is open. |
|
|
34
|
+
|
|
35
|
+
## Bind everything in one module
|
|
36
|
+
|
|
37
|
+
A module is the list of what is alive while the chat is open. When it unmounts, the bus undoes every line of it.
|
|
38
|
+
|
|
39
|
+
<!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
|
|
40
|
+
```js
|
|
41
|
+
// chat-module.js
|
|
42
|
+
import { createModule } from "nanoeffects/actions";
|
|
43
|
+
import { queue } from "nanoeffects/policies";
|
|
44
|
+
import { closeRequested, messageSent } from "./chat";
|
|
45
|
+
import { askBot, watchOperator } from "./chat-handlers";
|
|
46
|
+
|
|
47
|
+
// a task that stays pending until the module unmounts; its signal then aborts
|
|
48
|
+
const untilAborted = (ctx) =>
|
|
49
|
+
new Promise((resolve) => {
|
|
50
|
+
ctx.signal.addEventListener("abort", resolve, { once: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// a DOM listener that lives as long as the module: `signal` removes it when it aborts
|
|
54
|
+
const listenToEscape = async (_, ctx) => {
|
|
55
|
+
const onKey = (event) => {
|
|
56
|
+
if (event.key === "Escape") {
|
|
57
|
+
ctx.emit(closeRequested());
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
document.addEventListener("keydown", onKey, { signal: ctx.signal });
|
|
61
|
+
await untilAborted(ctx);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const chatModule = createModule("chat", (module) => {
|
|
65
|
+
// module.run — runs askBot as a child task of the module, so an unmount aborts it
|
|
66
|
+
const answer = (text) => module.run(askBot, text);
|
|
67
|
+
// on + queue — answer the texts one after another, in the order they were sent
|
|
68
|
+
module.on(messageSent, queue(answer));
|
|
69
|
+
// spawn — a background task that lives as long as the module
|
|
70
|
+
module.spawn(watchOperator);
|
|
71
|
+
module.spawn(listenToEscape);
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Three details that make the cleanup complete:
|
|
76
|
+
|
|
77
|
+
- **`module.run`, not `askBot` directly.** A handler given to `module.on` runs as a child of whoever emitted the event (here the panel), so unmounting stops listening but does not abort a request that is already running. Running it with `module.run` makes it a child of the module.
|
|
78
|
+
- **`queue`, not `latest`.** `queue` keeps the order when the user sends two texts fast. [`latest`](policies.md) would abort the first answer, which is wrong for a conversation.
|
|
79
|
+
- **A DOM listener with `signal`.** `addEventListener` takes a `signal` option and removes the listener when it aborts. The task stays pending until then, because a task that has finished is not aborted again.
|
|
80
|
+
|
|
81
|
+
## Open and close from the launcher
|
|
82
|
+
|
|
83
|
+
`app.mount` returns the function that unmounts the module. `bus.on` returns the one that unsubscribes. The launcher puts them together with the one that removes the panel into a single `close`.
|
|
84
|
+
|
|
85
|
+
<!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
|
|
86
|
+
```js
|
|
87
|
+
// main.js
|
|
88
|
+
import { app, bus } from "./bus";
|
|
89
|
+
import { closeRequested } from "./chat";
|
|
90
|
+
import { chatModule } from "./chat-module";
|
|
91
|
+
import { renderPanel } from "./panel";
|
|
92
|
+
|
|
93
|
+
const button = document.querySelector("#chat-launcher");
|
|
94
|
+
let close = null; // set while the chat is open
|
|
95
|
+
|
|
96
|
+
const open = () => {
|
|
97
|
+
const stopModule = app.mount(chatModule); // subscriptions, polling and Escape start
|
|
98
|
+
const stopPanel = renderPanel(document.body);
|
|
99
|
+
// the module emits closeRequested on Escape; nothing does while the chat is closed
|
|
100
|
+
const stopEscape = bus.on(closeRequested, () => close?.());
|
|
101
|
+
close = () => {
|
|
102
|
+
stopEscape();
|
|
103
|
+
stopPanel();
|
|
104
|
+
stopModule(); // everything the module started ends, a running request included
|
|
105
|
+
close = null;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
button.addEventListener("click", () => {
|
|
110
|
+
if (close) {
|
|
111
|
+
close();
|
|
112
|
+
} else {
|
|
113
|
+
open();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
While the chat is closed the launcher holds no subscription at all: it only has the click listener of the button, which is a DOM listener, not a listener of the bus.
|
|
119
|
+
|
|
120
|
+
## Check that closing cleans up
|
|
121
|
+
|
|
122
|
+
1. Open the chat and send a text. The bot answers after about a second. Every third question fails, and the retry asks again.
|
|
123
|
+
2. Send a text and close the chat before the answer arrives. No line appears, also when you open the chat again.
|
|
124
|
+
3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed: every event shows `0`, and so does "all events". Open: each of the four has one listener, and "all events" is `4`. That is the proof that nothing is left subscribed.
|
|
125
|
+
4. Wait 10 s with the chat closed. The operator is not polled: no line from the operator appears.
|
|
126
|
+
5. Press `Escape` with the chat closed. Nothing happens. Open the chat and press it again: it closes.
|
|
127
|
+
6. Open the chat again. The old lines are back, and the polling runs.
|
|
128
|
+
|
|
129
|
+
`src/tests/chat-widget-example.test.ts` checks the same points without a browser.
|
|
130
|
+
|
|
131
|
+
## Where to go next
|
|
132
|
+
|
|
133
|
+
- Show an unread count on the launcher while the chat is closed: a second module that stays mounted and listens only to `chat/messageAdded`.
|
|
134
|
+
- Replace the polling by a WebSocket in a spawned task: the abort closes it the same way (see [`examples/trades`](../examples/trades) for a socket under `retry` and `latest`).
|
|
135
|
+
- Watch the whole thing in the [trace](tracing.md): the module, the bot task, the retries and the polling are tasks of one tree.
|
package/docs/context.md
CHANGED
|
@@ -21,7 +21,7 @@ Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may
|
|
|
21
21
|
|
|
22
22
|
To get `race`, `all`, `take` and the like on every task without passing `ctx`, see [`effectsPlugin`](effects.md#put-selected-effects-on-ctx).
|
|
23
23
|
|
|
24
|
-
To give fields to **one**
|
|
24
|
+
To give fields to **one** action, job or module, or to replace them for one `emit`, see [scopes](scope.md).
|
|
25
25
|
|
|
26
26
|
## Type the fields (optional)
|
|
27
27
|
|
|
@@ -45,6 +45,38 @@ bus.on("save", (draft, { store }) => store.put(draft));
|
|
|
45
45
|
- A name that the bus already has is a type error. The fields of the library (`signal`, `emit`, `run`, …) win at runtime, and a field that the context already has is not replaced.
|
|
46
46
|
- The plugin returns the same bus, so keep the result of the chain: `const bus = createBus().use(contextPlugin({ store }))`.
|
|
47
47
|
|
|
48
|
+
## Build a field from the signal of each task
|
|
49
|
+
|
|
50
|
+
Problem: an HTTP client or a database handle should stop when the task is aborted. Without help, every handler passes `ctx.signal` to it by hand, and one forgotten call leaves a request running.
|
|
51
|
+
|
|
52
|
+
`fromSignal(make)` from `nanoeffects/signal` marks a field of `contextPlugin` as a value that is built from the signal of the task. It does not run `make` for the bus: it runs it for each task that reads the field, once, with the `ctx.signal` of that task.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { createBus } from "nanoeffects";
|
|
56
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
57
|
+
import { fromSignal } from "nanoeffects/signal";
|
|
58
|
+
import axios from "axios";
|
|
59
|
+
|
|
60
|
+
// contextPlugin — every task gets these fields
|
|
61
|
+
// fromSignal — http is made from the signal of the task that reads ctx.http
|
|
62
|
+
const bus = createBus().use(
|
|
63
|
+
contextPlugin({
|
|
64
|
+
logger: console, // a plain field: the same object for every task
|
|
65
|
+
http: fromSignal((signal) => axios.create({ signal })),
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
bus.on("user/open", async (id, ctx) => {
|
|
70
|
+
const response = await ctx.http.get(`/users/${id}`); // aborts with the task
|
|
71
|
+
ctx.logger.log(response.data);
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `make` runs at the first read, so a handler that never uses `ctx.http` pays nothing for it.
|
|
76
|
+
- A task that `ctx.run` starts builds its own value from its own signal, so aborting the child stops only the child's requests.
|
|
77
|
+
- The field is not copied by `{ ...ctx }` and `Object.keys(ctx)` does not list it. Read `ctx.http` first if you copy a context.
|
|
78
|
+
- Without `contextPlugin` a `fromSignal` marker is only an object.
|
|
79
|
+
|
|
48
80
|
### Name the fields in the event map instead
|
|
49
81
|
|
|
50
82
|
`createBus<Events>({ context: { store } })` also types the fields, but through the event map: `WithContext` is checker-only and says which fields the handlers of this bus have. Use it when you cannot call `use`, for example for the bus that `createEffectsMiddleware` makes.
|