nanoeffects 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/{notrace/action-creator-Ls3Gy3jk.d.ts → action-creator-HFA_LZOH.d.ts} +2 -2
  2. package/dist/actions.d.ts +3 -3
  3. package/dist/actions.js +1 -1
  4. package/dist/bus-duCCrqrr.js +1 -0
  5. package/dist/{notrace/context-types-DQYBFJG4.d.ts → context-types-BNcs43eY.d.ts} +1 -1
  6. package/dist/context.d.ts +2 -2
  7. package/dist/dev.d.ts +2 -2
  8. package/dist/effects.d.ts +2 -2
  9. package/dist/index.d.ts +2 -3
  10. package/dist/index.js +1 -1
  11. package/dist/match.d.ts +1 -1
  12. package/dist/{module-COv9ZSzS.d.ts → module-DTf73Wdq.d.ts} +2 -2
  13. package/dist/{action-creator-CQlFgQm4.d.ts → notrace/action-creator-CbFf398i.d.ts} +2 -24
  14. package/dist/notrace/actions.d.ts +2 -2
  15. package/dist/notrace/actions.js +1 -1
  16. package/dist/notrace/bus-BkUtjPHp.js +1 -0
  17. package/dist/notrace/effects.d.ts +2 -2
  18. package/dist/notrace/index.d.ts +2 -3
  19. package/dist/notrace/index.js +1 -1
  20. package/dist/{plugin-15-kkZp0.d.ts → notrace/plugin-C5vIjxhR.d.ts} +8 -2
  21. package/dist/notrace/policies.d.ts +1 -1
  22. package/dist/notrace/redux.d.ts +1 -1
  23. package/dist/observable.d.ts +30 -1
  24. package/dist/observable.js +1 -1
  25. package/dist/oop.d.ts +49 -1
  26. package/dist/oop.js +1 -1
  27. package/dist/{notrace/plugin-W4pZY7mM.d.ts → plugin-Sy8z9IId.d.ts} +2 -2
  28. package/dist/policies.d.ts +2 -2
  29. package/dist/react.d.ts +2 -2
  30. package/dist/redux.d.ts +1 -1
  31. package/dist/scope.d.ts +2 -2
  32. package/dist/signal.d.ts +1 -1
  33. package/dist/signal.js +1 -1
  34. package/dist/trace.d.ts +2 -2
  35. package/dist/{types-C7ukMwE-.d.ts → types-DaZT158m.d.ts} +2 -2
  36. package/dist/{with-policy-_zP3OKGy.d.ts → with-policy-C_mX7S54.d.ts} +1 -1
  37. package/docs/cancel-stale-requests.md +6 -2
  38. package/docs/debounce-and-abort.md +1 -3
  39. package/docs/effects.md +10 -3
  40. package/docs/observable.md +50 -0
  41. package/docs/oop.md +126 -4
  42. package/docs/package.md +2 -2
  43. package/docs/plugins.md +8 -2
  44. package/docs/tracing.md +32 -8
  45. package/llms-full.txt +235 -24
  46. package/llms.txt +2 -2
  47. package/package.json +2 -2
  48. package/dist/bus-Cdw5ajKF.js +0 -1
  49. package/dist/context-types-qXxr_Grf.d.ts +0 -14
  50. package/dist/notrace/bus-G2AdGObY.js +0 -1
package/llms-full.txt CHANGED
@@ -561,10 +561,10 @@ Imports are split so you only load what you use. `createBus` stays small; the re
561
561
  | `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
562
562
  | `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
563
563
  | `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
564
- | `nanoeffects/oop` | `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` |
564
+ | `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()` |
565
565
  | `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
566
566
  | `nanoeffects/redux` | `createEffectsMiddleware` |
567
- | `nanoeffects/observable` | `createObserve` |
567
+ | `nanoeffects/observable` | `createObserve`, `Subject` |
568
568
  | `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
569
569
  | `nanoeffects/dev` | `devPlugin` (no-op in production) |
570
570
  | `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
@@ -628,7 +628,9 @@ import { createBus } from "nanoeffects";
628
628
  import { delay, effectsPlugin, race } from "nanoeffects/effects";
629
629
 
630
630
  // effectsPlugin — race and delay are bound to the task of each handler
631
- const bus = createBus().use(effectsPlugin({ race, delay }));
631
+ const effects = effectsPlugin({ race, delay });
632
+
633
+ const bus = createBus().use(effects);
632
634
 
633
635
  bus.on("load", async (id, { race, delay }) => {
634
636
  const result = await race({
@@ -734,8 +736,12 @@ With a tracer on the bus, the plugin lets a trace say which subscription began a
734
736
  import { createBus } from "nanoeffects";
735
737
  import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
736
738
 
739
+ // createTracer: sends each trace event to the logger; consoleLogger: prints one line each
740
+ const trace = createTracer(consoleLogger());
741
+
737
742
  // tracePlugin — names the roots, groups and listeners of the trace
738
- const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
743
+ const bus = createBus({ trace })
744
+ .use(tracePlugin());
739
745
 
740
746
  bus.on("name/changed", saveName, { traceId: "saveNotifier" }); // a listener you can find in the log
741
747
  ```
@@ -1124,7 +1130,7 @@ const bus = createBus(); // the bus: events and handlers
1124
1130
  // latest: a newer search aborts the request in flight
1125
1131
  const searchLatest = latest(async (query, { signal }) => {
1126
1132
  const items = await api.search(query, { signal });
1127
- bus.emit("results", items);
1133
+ render(items);
1128
1134
  });
1129
1135
 
1130
1136
  // debounce: run after 300 ms without a new "typed"
@@ -1148,7 +1154,11 @@ const searchOnce = timeout(5000, (query, { signal }) => api.search(query, { sign
1148
1154
  // retry: up to 4 attempts, backoff makes the pause longer each time
1149
1155
  const searchWithRetry = retry(searchOnce, {
1150
1156
  times: 4,
1151
- delay: backoff({ base: 200, max: 5000, jitter: true }),
1157
+ delay: backoff({
1158
+ base: 200,
1159
+ max: 5000,
1160
+ jitter: true,
1161
+ }),
1152
1162
  });
1153
1163
 
1154
1164
  // latest: a newer search aborts the whole retry chain
@@ -1204,13 +1214,11 @@ const bus = createBus(); // the bus: events and handlers
1204
1214
  const searchLatest = latest(async (query, { signal }) => {
1205
1215
  // signal: pass it on so the browser cancels the request itself
1206
1216
  const items = await api.search(query, { signal });
1207
- bus.emit("results", items);
1217
+ render(items);
1208
1218
  });
1209
1219
 
1210
1220
  // debounce: run searchLatest after 300 ms without a new "typed"
1211
1221
  bus.on("typed", debounce(300, searchLatest));
1212
-
1213
- bus.on("results", (items) => render(items));
1214
1222
  ```
1215
1223
 
1216
1224
  What to replace: `api.search` is your own request function. It must accept `{ signal }` and give it to `fetch`. `render` shows the items.
@@ -1545,7 +1553,11 @@ import { backoff, retry } from "nanoeffects/effects";
1545
1553
  const loadWithRetry = retry(loadUser, {
1546
1554
  times: 5,
1547
1555
  // backoff — pause 200, 400, 800, … ms, never over 5 s, a random part of it
1548
- delay: backoff({ base: 200, max: 5000, jitter: true }),
1556
+ delay: backoff({
1557
+ base: 200,
1558
+ max: 5000,
1559
+ jitter: true,
1560
+ }),
1549
1561
  });
1550
1562
  ```
1551
1563
 
@@ -1568,8 +1580,11 @@ If handlers call the same helpers often, put them on the task once. A handler th
1568
1580
  import { createBus } from "nanoeffects";
1569
1581
  import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
1570
1582
 
1571
- // effectsPlugin — add race, all and delay to the context of every task; returns the same bus
1572
- const bus = createBus().use(effectsPlugin({ race, all, delay }));
1583
+ // effectsPlugin — add race, all and delay to the context of every task
1584
+ const effects = effectsPlugin({ race, all, delay });
1585
+
1586
+ // use returns the same bus, typed with the new fields
1587
+ const bus = createBus().use(effects);
1573
1588
 
1574
1589
  bus.on("load", async (id, { race, all, delay }) => {
1575
1590
  // race — same as race(ctx, …)
@@ -3414,7 +3429,12 @@ Source: docs/oop.md
3414
3429
 
3415
3430
  # Use the bus from a class
3416
3431
 
3417
- Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods. A class made with `extendBus` has `on`, `emit` and the other methods of the bus, and remembers what each instance subscribed to, so `dispose()` ends it all.
3432
+ Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods. There are two ways to give a class the methods of a bus, and both remember what each instance subscribed to, so `dispose()` ends it all:
3433
+
3434
+ | You want | Use |
3435
+ | -------- | --- |
3436
+ | every `new Service()` to be an independent emitter | `extends Emitter` |
3437
+ | the instances to share a bus, or to use plugins | `extends extendBus(bus)` |
3418
3438
 
3419
3439
  If your logic is plain functions, stay with [modules](actions.md#mount-domains-with-modules): a module is the same idea without a class.
3420
3440
 
@@ -3422,7 +3442,68 @@ If your logic is plain functions, stay with [modules](actions.md#mount-domains-w
3422
3442
 
3423
3443
  A class that listens to the bus keeps a reference to it, and keeps every unsubscribe function in a list to call when the object goes away. Forget one and the old object keeps handling events. `extendBus` does both for you.
3424
3444
 
3425
- ## Give a class the methods of a bus
3445
+ ## Make an independent emitter with `Emitter`
3446
+
3447
+ `Emitter` is a base class. Every instance of a class that extends it has a bus of its own: the events of one instance never reach another. It is the shape of Node's `EventEmitter`.
3448
+
3449
+ ```js
3450
+ // video-service.js
3451
+ import { Emitter } from "nanoeffects/oop";
3452
+
3453
+ // Emitter — a base class: each new VideoService() gets on, once, off, emit, dispose and its own bus
3454
+ export class VideoService extends Emitter {
3455
+ constructor() {
3456
+ super();
3457
+ // this.on — a subscription of this instance; dispose() removes it
3458
+ this.on("played", () => console.log("playing"));
3459
+ }
3460
+ }
3461
+ ```
3462
+
3463
+ ```js
3464
+ import { VideoService } from "./video-service";
3465
+
3466
+ const first = new VideoService();
3467
+ const second = new VideoService();
3468
+ first.on("paused", () => console.log("first paused"));
3469
+ second.emit("paused"); // nothing: the instances do not share a bus
3470
+ first.emit("paused"); // logs "first paused"
3471
+ first.dispose(); // its subscriptions are gone
3472
+ ```
3473
+
3474
+ What you get:
3475
+
3476
+ - `on`, `once`, `off`, `emit`, `dispose` and `bus` (the bus of that instance). `instance[Symbol.dispose]()` does the same as `dispose()`, so `using video = new VideoService()` works where the runtime supports it.
3477
+ - `@On` and `@Once` work on its methods (see below). They are subscribed when the base constructor runs, so before the fields of your subclass.
3478
+ - `on`, `once`, `off` and `emit` are bound to the instance, so `const { emit } = video` works. A subclass replaces one with an arrow property (`emit = (…) => …`); TypeScript does not let it redeclare one as a method. `dispose` is a method and can be overridden as usual.
3479
+ - No methods of plugins: the bus is made inside, so no plugin is on it. For plugins, or for a bus that several instances or classes share, use `extendBus` (next sections).
3480
+ - For a stream of one event, see [`Subject`](observable.md#read-and-write-one-event-with-a-subject) and [Stream an event with a `Subject`](#stream-an-event-with-a-subject).
3481
+
3482
+ ### Type the events (optional)
3483
+
3484
+ `Emitter` takes the events of the class as a type parameter.
3485
+
3486
+ ```ts
3487
+ // video-service.ts
3488
+ import { Emitter } from "nanoeffects/oop";
3489
+
3490
+ // VideoEvents — checker only: the payload of each event of this class
3491
+ type VideoEvents = { "video/time": number; "video/paused": void };
3492
+
3493
+ export class VideoService extends Emitter<VideoEvents> {
3494
+ constructor() {
3495
+ super();
3496
+ // seconds is a number because "video/time" carries one
3497
+ this.on("video/time", (seconds) => console.log(seconds));
3498
+ }
3499
+ }
3500
+
3501
+ const video = new VideoService();
3502
+ video.emit("video/time", 12); // checked against VideoEvents
3503
+ video.emit("video/paused"); // no payload
3504
+ ```
3505
+
3506
+ ## Give a class the methods of a shared bus
3426
3507
 
3427
3508
  `extendBus(bus, Base)` returns a subclass of `Base`. Its instances have the methods of the bus, and the arguments of `new` go to the constructor of `Base`.
3428
3509
 
@@ -3483,9 +3564,33 @@ What you get:
3483
3564
  - `bus` is the bus, and `Class.bus` is the same one.
3484
3565
  - `this.on` works in the constructor of `Base`. The methods exist on the prototype of the subclass, which is the prototype of the instance already.
3485
3566
 
3567
+ ### Extend the result with no class of your own
3568
+
3569
+ `extendBus(bus)` without a class returns a class with the methods of that bus and nothing else. Extend it as it is.
3570
+
3571
+ ```js
3572
+ // service.js
3573
+ import { createBus } from "nanoeffects";
3574
+ import { extendBus } from "nanoeffects/oop";
3575
+
3576
+ const bus = createBus(); // the bus that the services share
3577
+
3578
+ // extendBus(bus) — a class with on, once, off, emit and dispose over this bus
3579
+ export class AudioService extends extendBus(bus) {
3580
+ constructor() {
3581
+ super();
3582
+ this.on("muted", () => console.log("muted")); // ends with dispose()
3583
+ }
3584
+ }
3585
+
3586
+ const audio = new AudioService();
3587
+ bus.emit("muted"); // the instances share this bus: logs "muted"
3588
+ audio.dispose();
3589
+ ```
3590
+
3486
3591
  ## Let the class make its bus
3487
3592
 
3488
- Leave the bus out and the class gets its own, shared by all of its instances.
3593
+ Leave the bus out and the class gets its own, shared by all of its instances. That is one bus for each class; `Emitter` makes one for each instance.
3489
3594
 
3490
3595
  ```js
3491
3596
  // notifier.js
@@ -3527,6 +3632,37 @@ stop(); // the function on returned still works, and does nothing now
3527
3632
 
3528
3633
  A subscription that you removed yourself (the function `on` returned, `this.off`, or a `once` that fired) is forgotten, so the instance holds nothing for it.
3529
3634
 
3635
+ ## Stream an event with a `Subject`
3636
+
3637
+ Problem: a service hands one of its events to code written for streams, and the stream subscription should end with the service. `createObserve(this.bus)` would not: it subscribes through the bus, so `dispose()` does not know it.
3638
+
3639
+ Pass `this` to `Subject` as the source. The subject subscribes through `this.on`, so the subscription belongs to the instance ([Read and write one event with a Subject](observable.md#read-and-write-one-event-with-a-subject)).
3640
+
3641
+ ```js
3642
+ // typing.js
3643
+ import { createBus } from "nanoeffects";
3644
+ import { Subject } from "nanoeffects/observable";
3645
+ import { extendBus } from "nanoeffects/oop";
3646
+
3647
+ const bus = createBus(); // the bus: events and handlers
3648
+
3649
+ class TypingBase {
3650
+ constructor() {
3651
+ this.typed = new Subject(this, "typed"); // Subject — the event "typed" as a stream; this is the source
3652
+ this.typed.subscribe((text) => console.log(text));
3653
+ }
3654
+ }
3655
+
3656
+ const Typing = extendBus(bus, TypingBase); // extendBus — a class with on, emit and dispose
3657
+
3658
+ const typing = new Typing();
3659
+ typing.typed.next("a"); // logs "a"
3660
+ typing.dispose(); // ends the subscription of the Subject too
3661
+ typing.typed.next("b"); // nothing is logged
3662
+ ```
3663
+
3664
+ In TypeScript, declare `on` and `emit` in the class (`declare on: BusMethods<typeof bus>["on"]`, see [Type the instances](#type-the-instances-optional)) so that `this` fits as the source.
3665
+
3530
3666
  ## Keep a method of your own
3531
3667
 
3532
3668
  The class wins any clash of names. If `Base` has `emit`, `dispose`, `on` or `bus`, yours stays and the bus method is not added. A `Base` with its own `dispose` would never end the subscriptions, so call `disposeBus(this)` from there.
@@ -3699,7 +3835,8 @@ service.emit("order/placed", { id: "o1" }); // checked against Events
3699
3835
 
3700
3836
  - `this` is not bound in a handler that you pass by hand (see above).
3701
3837
  - The methods of a plugin used after `extendBus` are not on the class. Use the plugins first, then `extendBus`.
3702
- - One bus for each instance, mixing the methods into an object that exists, and decorators for fields or parameters are not part of this entry.
3838
+ - `Emitter` has no methods of plugins, because its bus is made inside. Use `extendBus(bus)` for them.
3839
+ - Mixing the methods into an object that exists, and decorators for fields or parameters are not part of this entry.
3703
3840
 
3704
3841
  ---
3705
3842
  Source: docs/typescript.md
@@ -4613,7 +4750,10 @@ bus.on("load", async (id, ctx) => {
4613
4750
  import { createBus } from "nanoeffects";
4614
4751
  import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
4615
4752
 
4616
- export const bus = createBus({ trace: createTracer(consoleLogger()) })
4753
+ // createTracer: sends each trace event to the logger; consoleLogger: prints one line each
4754
+ const trace = createTracer(consoleLogger());
4755
+
4756
+ export const bus = createBus({ trace })
4617
4757
  .use(tracePlugin()); // tracePlugin: reports each emit as an event
4618
4758
  ```
4619
4759
 
@@ -4642,7 +4782,9 @@ import { createAction, emitterPlugin } from "nanoeffects/actions";
4642
4782
  import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
4643
4783
 
4644
4784
  // createTracer: reports every task phase to the console
4645
- const bus = createBus({ trace: createTracer(consoleLogger()) })
4785
+ const trace = createTracer(consoleLogger());
4786
+
4787
+ const bus = createBus({ trace })
4646
4788
  .use(tracePlugin()) // tracePlugin: makes the root task that traceId names
4647
4789
  .use(emitterPlugin());
4648
4790
 
@@ -4683,8 +4825,12 @@ import { createBus } from "nanoeffects";
4683
4825
  import { createCommand } from "nanoeffects/actions";
4684
4826
  import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
4685
4827
 
4828
+ // createTracer: sends each trace event to the logger; consoleLogger: prints one line each
4829
+ const trace = createTracer(consoleLogger());
4830
+
4686
4831
  // tracePlugin: reads traceGroupId from the scope of a call
4687
- const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
4832
+ const bus = createBus({ trace })
4833
+ .use(tracePlugin());
4688
4834
 
4689
4835
  // the scope names the flow; traceGroupId is for the tracer, not a field of ctx
4690
4836
  const pay = createCommand("checkout/pay", async () => {}, {
@@ -4710,7 +4856,11 @@ Give the listener a `traceId` when you register it. The plugin makes each call o
4710
4856
  import { createBus } from "nanoeffects";
4711
4857
  import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
4712
4858
 
4713
- const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
4859
+ // createTracer: sends each trace event to the logger; consoleLogger: prints one line each
4860
+ const trace = createTracer(consoleLogger());
4861
+
4862
+ const bus = createBus({ trace })
4863
+ .use(tracePlugin());
4714
4864
 
4715
4865
  // traceId: the name of this listener in the trace
4716
4866
  bus.on("name/changed", saveName, { traceId: "saveNotifier" });
@@ -4817,7 +4967,10 @@ export const bus = createBus({ trace: createTracer(logger) })
4817
4967
  import { reduxDevtools } from "nanoeffects/trace";
4818
4968
 
4819
4969
  // debugging a race: which calls did a policy cancel or drop?
4820
- reduxDevtools({ name: "app", policies: ["cancel", "drop"] });
4970
+ reduxDevtools({
4971
+ name: "app",
4972
+ policies: ["cancel", "drop"],
4973
+ });
4821
4974
 
4822
4975
  // every stage of every policy, one function for the choice
4823
4976
  reduxDevtools({
@@ -4826,10 +4979,18 @@ reduxDevtools({
4826
4979
  });
4827
4980
 
4828
4981
  // the old log: every stage, and the effects below the handlers
4829
- reduxDevtools({ name: "app", policies: true, depth: 1, payload: "all" });
4982
+ reduxDevtools({
4983
+ name: "app",
4984
+ policies: true,
4985
+ depth: 1,
4986
+ payload: "all",
4987
+ });
4830
4988
 
4831
4989
  // keep the payloads out of the extension
4832
- reduxDevtools({ name: "app", payload: "none" });
4990
+ reduxDevtools({
4991
+ name: "app",
4992
+ payload: "none",
4993
+ });
4833
4994
  ```
4834
4995
 
4835
4996
  With two quick `search` emits and a `latest` handler, the default list is `search`, `search`. With `policies: true` it adds the `latest` stages of each call, for example `search/latest/cancel` for the first one.
@@ -4837,7 +4998,7 @@ With two quick `search` emits and a `latest` handler, the default list is `searc
4837
4998
  ### Write your own logger
4838
4999
 
4839
5000
  ```ts
4840
- import type { TraceLogger } from "nanoeffects";
5001
+ import type { TraceLogger } from "nanoeffects/trace";
4841
5002
 
4842
5003
  const errorsOnly = (): TraceLogger => (event) => {
4843
5004
  if (event.phase === "error") {
@@ -4938,6 +5099,56 @@ bus.on(queryChanged, (query) => console.log("search for", query));
4938
5099
 
4939
5100
  The Observable can come from anywhere, not only from `createObserve`. `subscription.unsubscribe()` stops it; the bus does not know about it. Pass the callback to `subscribe` as it is, and do not call `bus.emitter(...)` inside the operators on every value.
4940
5101
 
5102
+ ## Read and write one event with a Subject
5103
+
5104
+ Problem: `createObserve` only reads. Code written for streams often also needs to push a value into the same event, and passing a bus and an event name around for that is clumsy.
5105
+
5106
+ `Subject` is one event of a bus as one object. `subscribe` listens, `next` emits. It has no `complete`, no `error` and no replay: an event of a bus does not end, and the bus keeps no values.
5107
+
5108
+ ```js
5109
+ import { createBus } from "nanoeffects";
5110
+ import { Subject } from "nanoeffects/observable";
5111
+
5112
+ const bus = createBus(); // the bus: events and handlers
5113
+
5114
+ const typed = new Subject(bus, "typed"); // Subject — the event "typed" as a stream you can also write to
5115
+
5116
+ const subscription = typed.subscribe((text) => console.log(text)); // like bus.on("typed", …)
5117
+ typed.next("hello"); // like bus.emit("typed", "hello"): bus.on handlers and subscribers both run
5118
+ subscription.unsubscribe(); // removes this subscription only
5119
+ ```
5120
+
5121
+ - `next` works when you take it off the subject, so `input.addEventListener("input", (event) => typed.next(event.target.value))` and `store.subscribe(typed.next)` need no wrapper.
5122
+ - A subscriber that is added later does not get a value emitted before. Add `startWith` or `shareReplay` in RxJS when it should.
5123
+ - RxJS takes a `Subject` as it takes `createObserve`: `from(typed)`. It is not an RxJS `Subject` (no `error`, `complete` or `pipe`), so it does not fit where that type is wanted.
5124
+
5125
+ ### Extend it for a named stream
5126
+
5127
+ Problem: the event name is repeated wherever the stream is made. A subclass fixes it once.
5128
+
5129
+ ```js
5130
+ import { createBus } from "nanoeffects";
5131
+ import { Subject } from "nanoeffects/observable";
5132
+
5133
+ class TypedText extends Subject {
5134
+ constructor(bus) {
5135
+ super(bus, "typed"); // the event is fixed by the class
5136
+ }
5137
+
5138
+ next(text) {
5139
+ super.next(text.trim()); // a method of the subclass replaces the one of Subject
5140
+ }
5141
+ }
5142
+
5143
+ const bus = createBus(); // the bus: events and handlers
5144
+ const typed = new TypedText(bus);
5145
+ typed.next(" hello "); // emits "typed" with "hello"
5146
+ ```
5147
+
5148
+ ### Use it inside a class with a bus
5149
+
5150
+ A class made with `extendBus` has its own `on` and `emit`. Pass `this` as the source and the subscription belongs to the instance, so `dispose()` ends it ([Classes with a bus](oop.md)).
5151
+
4941
5152
  ## Do the work in a handler, not in `switchMap`
4942
5153
 
4943
5154
  An Observable is good for shaping values. A request is better in a handler with a policy, because the handler has a `signal` and the trace shows it. Split it: RxJS ends at the value, the bus starts the work.
package/llms.txt CHANGED
@@ -36,7 +36,7 @@ Rules that generated code often gets wrong:
36
36
  - [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `usePolicy` keeps a debounced handler between renders
37
37
  - [Context fields](docs/context.md): put a store or client on every task
38
38
  - [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
39
- - [Use the bus from a class](docs/oop.md): `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
39
+ - [Use the bus from a class](docs/oop.md): `Emitter`, `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
40
40
 
41
41
  ## TypeScript
42
42
 
@@ -53,5 +53,5 @@ Rules that generated code often gets wrong:
53
53
 
54
54
  - [Semantics](docs/semantics.md): exact rules of emit, abort and ordering
55
55
  - [Trace tasks](docs/tracing.md): loggers, Redux DevTools, the `notrace` build
56
- - [Turn events into Observables](docs/observable.md): `createObserve`
56
+ - [Turn events into Observables](docs/observable.md): `createObserve`, `Subject`
57
57
  - [Hot reload and development diagnostics](docs/hmr.md): `devPlugin`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanoeffects",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
5
5
  "keywords": [
6
6
  "event-bus",
@@ -226,7 +226,7 @@
226
226
  {
227
227
  "name": "oop (class entry)",
228
228
  "path": "dist/oop.js",
229
- "limit": "2.5 kB"
229
+ "limit": "2.6 kB"
230
230
  },
231
231
  {
232
232
  "name": "scope (plugin and effect)",
@@ -1 +0,0 @@
1
- import{d as e,n as t,o as n,r}from"./context-CJGW6OWj.js";import{n as i}from"./promise-CSLpzwSR.js";import{n as a,t as o}from"./event-0RIBGYqS.js";const s=`*`,c=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{e={},t={}}}},l=Symbol(),u=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,l,{value:e,enumerable:!1}),r},d=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&l in e)return e[l]},f=(e,t,n)=>Reflect.apply(e,null,[t,n]),p=(s={})=>{let l=c(),u=(e,t)=>l.add(o(e),t),p=(e,t)=>{let n=u(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(o(e),t)},h=(n,a)=>{let{onError:o}=s,{ctx:c}=t({event:a,bus:g,tracer:s.trace,parent:{ref:r()}});if(!o){n(a.payload,c);return}let l=t=>{try{o(t,a)}catch(t){e(`onError failed`,t)}};try{let e=n(a.payload,c);i(e)&&Promise.resolve(e).catch(l)}catch(e){l(e)}},g={on:u,once:p,off:m,emit:(e,i)=>{let c=o(e),u=a(e,i),p={type:c,payload:u},m=d(e),_=[...l.get(c),...l.get(`*`)];_.length===0&&!m&&s.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:e}=t({event:p,bus:g,tracer:s.trace,parent:{ref:r()}}),i=g.commands?.run??f;return n(e,`command`,()=>i(m,u,e))}},use:e=>e(g),ctx:s.context,trace:s.trace};return g};export{s as a,u as i,d as n,p as r,l as t};
@@ -1,14 +0,0 @@
1
- import { B as EventMap, x as ContextOf } from "./action-creator-CQlFgQm4.js";
2
- //#region src/core/context-types.d.ts
3
- /** What a function that makes the fields of a task gets to see. */
4
- type ContextTask = {
5
- type: string;
6
- payload?: unknown;
7
- };
8
- /**
9
- * `unknown` when none of the `fields` is already a context field of the bus,
10
- * else `never`: the call does not compile. Used as `bus: Bus<E> & NoClash<…>`.
11
- */
12
- type NoClash<Fields extends object, E extends EventMap> = [keyof Fields & keyof ContextOf<E>] extends [never] ? unknown : never;
13
- //#endregion
14
- export { NoClash as n, ContextTask as t };
@@ -1 +0,0 @@
1
- import{a as e,l as t,n,r}from"./context-yYU3_9lq.js";import{n as i}from"./promise-CSLpzwSR.js";const a=e=>typeof e==`string`?e:e.type,o=(e,t)=>typeof e==`object`?e.payload:t,s=`*`,c=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{e={},t={}}}},l=Symbol(),u=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,l,{value:e,enumerable:!1}),r},d=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&l in e)return e[l]},f=(e,t,n)=>Reflect.apply(e,null,[t,n]),p=(s={})=>{let l=c(),u=(e,t)=>l.add(a(e),t),p=(e,t)=>{let n=u(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(a(e),t)},h=(e,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:g,tracer:s.trace,parent:{ref:r()}});if(!o){e(a.payload,c);return}let l=e=>{try{o(e,a)}catch(e){t(`onError failed`,e)}};try{let t=e(a.payload,c);i(t)&&Promise.resolve(t).catch(l)}catch(e){l(e)}},g={on:u,once:p,off:m,emit:(t,i)=>{let c=a(t),u=o(t,i),p={type:c,payload:u},m=d(t),_=[...l.get(c),...l.get(`*`)];_.length===0&&!m&&s.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:t}=n({event:p,bus:g,tracer:s.trace,parent:{ref:r()}}),i=g.commands?.run??f;return e(t,`command`,()=>i(m,u,t))}},use:e=>e(g),ctx:s.context,trace:s.trace};return g};export{a,s as i,p as n,u as r,l as t};