nanoeffects 0.35.0 → 0.36.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 (42) hide show
  1. package/dist/{action-creator-DprJV-pN.d.ts → action-creator-CXIq6GWK.d.ts} +1 -1
  2. package/dist/actions.d.ts +3 -3
  3. package/dist/{context-types-BiTMp_lW.d.ts → context-types-BO1QE6W2.d.ts} +1 -1
  4. package/dist/context.d.ts +2 -2
  5. package/dist/dev.d.ts +2 -2
  6. package/dist/effects.d.ts +2 -2
  7. package/dist/guard.d.ts +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/match.d.ts +1 -1
  10. package/dist/{module-Fk4OtkZk.d.ts → module-BXNaEXE9.d.ts} +2 -2
  11. package/dist/notrace/{action-creator-BWd9FWt6.d.ts → action-creator-JUwIsFUl.d.ts} +1 -1
  12. package/dist/notrace/actions.d.ts +2 -2
  13. package/dist/notrace/effects.d.ts +2 -2
  14. package/dist/notrace/index.d.ts +1 -1
  15. package/dist/notrace/{plugin-DmAYT3Xk.d.ts → plugin-dLdTM2JL.d.ts} +1 -1
  16. package/dist/notrace/policies.d.ts +158 -2
  17. package/dist/notrace/policies.js +1 -1
  18. package/dist/notrace/redux.d.ts +1 -1
  19. package/dist/observable.d.ts +1 -1
  20. package/dist/oop.d.ts +1 -1
  21. package/dist/{plugin-2d3uuW7o.d.ts → plugin-B8La3K9m.d.ts} +2 -2
  22. package/dist/policies.d.ts +159 -3
  23. package/dist/policies.js +1 -1
  24. package/dist/react.d.ts +2 -2
  25. package/dist/redux.d.ts +1 -1
  26. package/dist/scope.d.ts +2 -2
  27. package/dist/signal.d.ts +2 -2
  28. package/dist/svelte.d.ts +1 -1
  29. package/dist/testing.d.ts +1 -1
  30. package/dist/trace.d.ts +1 -1
  31. package/dist/{types-DxPZDzZG.d.ts → types-ByoeMqVh.d.ts} +2 -2
  32. package/dist/vue.d.ts +1 -1
  33. package/dist/{with-policy-Y12d-yx6.d.ts → with-policy-e4yx1vhN.d.ts} +1 -1
  34. package/docs/package.md +1 -1
  35. package/docs/policies.md +188 -0
  36. package/docs/recipes.md +90 -0
  37. package/docs/ru/package.md +1 -1
  38. package/docs/ru/policies.md +188 -0
  39. package/docs/ru/recipes.md +90 -0
  40. package/llms-full.txt +279 -1
  41. package/llms.txt +1 -1
  42. package/package.json +2 -2
@@ -16,6 +16,8 @@
16
16
  - [Второй клик отправляет второй запрос](#второй-клик-отправляет-второй-запрос)
17
17
  - [Сообщения должны уходить по порядку](#сообщения-должны-уходить-по-порядку)
18
18
  - [Кнопка «Удалить» в списке блокирует соседние строки](#кнопка-удалить-в-списке-блокирует-соседние-строки)
19
+ - [Несколько компонентов запрашивают одни и те же данные](#несколько-компонентов-запрашивают-одни-и-те-же-данные)
20
+ - [Форма сохраняет один и тот же текст дважды](#форма-сохраняет-один-и-тот-же-текст-дважды)
19
21
  - [Ненадёжная сеть и длинные сценарии](#ненадёжная-сеть-и-длинные-сценарии)
20
22
  - [Запрос иногда падает: retry и backoff](#запрос-иногда-падает-retry-и-backoff)
21
23
  - [Запрос зависает: timeout](#запрос-зависает-timeout)
@@ -23,6 +25,8 @@
23
25
  - [Дождаться другого события внутри обработчика: take](#дождаться-другого-события-внутри-обработчика-take)
24
26
  - [Загрузить несколько ресурсов сразу: all](#загрузить-несколько-ресурсов-сразу-all)
25
27
  - [Взять самый быстрый ответ: race](#взять-самый-быстрый-ответ-race)
28
+ - [API разрешает только N запросов в секунду](#api-разрешает-только-n-запросов-в-секунду)
29
+ - [Много мелких событий, один запрос](#много-мелких-событий-один-запрос)
26
30
  - [Фичи и доменная логика](#фичи-и-доменная-логика)
27
31
  - [Получить результат от emit](#получить-результат-от-emit)
28
32
  - [Сгруппировать фичу с её обработчиками: модули](#сгруппировать-фичу-с-её-обработчиками-модули)
@@ -238,6 +242,49 @@ bus.on(
238
242
 
239
243
  Подробнее: [Политики, `key`](policies.md#key-разделение-state-по-ключу).
240
244
 
245
+ ### Несколько компонентов запрашивают одни и те же данные
246
+
247
+ Страница показывает пользователя в шапке, в сайдбаре и в комментарии, и все три запрашивают пользователя `1` в одном такте. Это три одинаковых запроса. `dedupe` заставляет вызовы с одним id делить один запрос. Используйте его для чтения, а не для записи.
248
+
249
+ ```js
250
+ import { createBus } from "nanoeffects";
251
+ import { dedupe } from "nanoeffects/policies";
252
+
253
+ const bus = createBus();
254
+
255
+ // dedupe — вызов для id, который уже загружается, ждёт этот запрос
256
+ bus.on(
257
+ "user/needed",
258
+ dedupe(async (id, ctx) => {
259
+ const user = await api.getUser(id, { signal: ctx.signal });
260
+ ctx.emit("user/loaded", user);
261
+ }),
262
+ );
263
+ ```
264
+
265
+ Подробнее: [Политики, `dedupe`](policies.md#dedupe-общий-запуск-для-одинаковых-вызовов).
266
+
267
+ ### Форма сохраняет один и тот же текст дважды
268
+
269
+ Форма сохраняется при каждом изменении. Пользователь набрал букву и стёр её, и тот же текст снова уходит на сервер. `distinct` пропускает вызов, чей payload равен последнему.
270
+
271
+ ```js
272
+ import { createBus } from "nanoeffects";
273
+ import { distinct } from "nanoeffects/policies";
274
+
275
+ const bus = createBus();
276
+
277
+ // distinct — текст, равный последнему сохранённому, пропускается
278
+ bus.on(
279
+ "draft/changed",
280
+ distinct(async (text, { signal }) => {
281
+ await api.saveDraft(text, { signal });
282
+ }),
283
+ );
284
+ ```
285
+
286
+ Подробнее: [Политики, `distinct`](policies.md#distinct-пропуск-повторного-payload).
287
+
241
288
  ## Ненадёжная сеть и длинные сценарии
242
289
 
243
290
  ### Запрос иногда падает: retry и backoff
@@ -369,6 +416,49 @@ bus.on("user/opened", async (id, ctx) => {
369
416
 
370
417
  Подробнее: [`all`](effects.md#all), [`race`](effects.md#race).
371
418
 
419
+ ### API разрешает только N запросов в секунду
420
+
421
+ API геокодирования разрешает 5 запросов в секунду. Список из 40 адресов отправляет 40 запросов сразу, и большинство падает с `429`. `rateLimit` запускает пять и держит остальные, пока окно не сдвинется.
422
+
423
+ ```js
424
+ import { createBus } from "nanoeffects";
425
+ import { rateLimit } from "nanoeffects/policies";
426
+
427
+ const bus = createBus();
428
+
429
+ // rateLimit — за секунду могут начаться пять запросов, остальные ждут своей очереди
430
+ bus.on(
431
+ "address/geocode",
432
+ rateLimit(5, 1000, async (address, ctx) => {
433
+ const place = await api.geocode(address, { signal: ctx.signal });
434
+ ctx.emit("address/found", place);
435
+ }),
436
+ );
437
+ ```
438
+
439
+ Подробнее: [Политики, `rateLimit`](policies.md#ratelimit-соблюдение-квоты).
440
+
441
+ ### Много мелких событий, один запрос
442
+
443
+ Страница отправляет событие аналитики на каждый клик, 30 запросов в минуту. Серверу удобнее один запрос со всеми. `batch` собирает события, пришедшие за окно, и отправляет их вместе.
444
+
445
+ ```js
446
+ import { createBus } from "nanoeffects";
447
+ import { batch } from "nanoeffects/policies";
448
+
449
+ const bus = createBus();
450
+
451
+ // batch — всё, что пришло за 500 мс, уходит одним запросом
452
+ bus.on(
453
+ "analytics/event",
454
+ batch(500, async (events, { signal }) => {
455
+ await api.sendEvents(events, { signal });
456
+ }),
457
+ );
458
+ ```
459
+
460
+ Подробнее: [Политики, `batch`](policies.md#batch-отправка-нескольких-вызовов-одним).
461
+
372
462
  ## Фичи и доменная логика
373
463
 
374
464
  ### Получить результат от emit
package/llms-full.txt CHANGED
@@ -568,6 +568,8 @@ Each task below starts with the problem, then a short solution. Where the page h
568
568
  - [A second click sends a second request](#a-second-click-sends-a-second-request)
569
569
  - [Messages must go out in order](#messages-must-go-out-in-order)
570
570
  - [A "Delete" button in a list blocks the other rows](#a-delete-button-in-a-list-blocks-the-other-rows)
571
+ - [Several components ask for the same data](#several-components-ask-for-the-same-data)
572
+ - [A form saves the same text twice](#a-form-saves-the-same-text-twice)
571
573
  - [An unreliable network and long flows](#an-unreliable-network-and-long-flows)
572
574
  - [A request fails sometimes: retry and backoff](#a-request-fails-sometimes-retry-and-backoff)
573
575
  - [A request hangs: timeout](#a-request-hangs-timeout)
@@ -575,6 +577,8 @@ Each task below starts with the problem, then a short solution. Where the page h
575
577
  - [Wait for another event inside a handler: take](#wait-for-another-event-inside-a-handler-take)
576
578
  - [Load several things at once: all](#load-several-things-at-once-all)
577
579
  - [Take the fastest answer: race](#take-the-fastest-answer-race)
580
+ - [An API allows only N requests per second](#an-api-allows-only-n-requests-per-second)
581
+ - [Many small events, one request](#many-small-events-one-request)
578
582
  - [Features and domain logic](#features-and-domain-logic)
579
583
  - [Get the result of emit](#get-the-result-of-emit)
580
584
  - [Group a feature with its handlers: modules](#group-a-feature-with-its-handlers-modules)
@@ -790,6 +794,49 @@ bus.on(
790
794
 
791
795
  More: [Policies, `key`](policies.md#key-isolate-state-by-key).
792
796
 
797
+ ### Several components ask for the same data
798
+
799
+ A page shows a user in the header, in a sidebar and in a comment, and all three ask for user `1` in the same tick. That is three identical requests. `dedupe` makes the calls for one id share a single request. Use it for reads, not for writes.
800
+
801
+ ```js
802
+ import { createBus } from "nanoeffects";
803
+ import { dedupe } from "nanoeffects/policies";
804
+
805
+ const bus = createBus();
806
+
807
+ // dedupe — a call for an id that is loading already waits for that request
808
+ bus.on(
809
+ "user/needed",
810
+ dedupe(async (id, ctx) => {
811
+ const user = await api.getUser(id, { signal: ctx.signal });
812
+ ctx.emit("user/loaded", user);
813
+ }),
814
+ );
815
+ ```
816
+
817
+ More: [Policies, `dedupe`](policies.md#dedupe-share-one-run-between-equal-calls).
818
+
819
+ ### A form saves the same text twice
820
+
821
+ A form saves on every change. A user types a letter and deletes it, and the same text goes to the server again. `distinct` skips a call whose payload equals the last one.
822
+
823
+ ```js
824
+ import { createBus } from "nanoeffects";
825
+ import { distinct } from "nanoeffects/policies";
826
+
827
+ const bus = createBus();
828
+
829
+ // distinct — a text equal to the last saved one is skipped
830
+ bus.on(
831
+ "draft/changed",
832
+ distinct(async (text, { signal }) => {
833
+ await api.saveDraft(text, { signal });
834
+ }),
835
+ );
836
+ ```
837
+
838
+ More: [Policies, `distinct`](policies.md#distinct-skip-a-repeated-payload).
839
+
793
840
  ## An unreliable network and long flows
794
841
 
795
842
  ### A request fails sometimes: retry and backoff
@@ -921,6 +968,49 @@ bus.on("user/opened", async (id, ctx) => {
921
968
 
922
969
  More: [`all`](effects.md#all), [`race`](effects.md#race).
923
970
 
971
+ ### An API allows only N requests per second
972
+
973
+ A geocoding API allows 5 requests per second. A list of 40 addresses sends 40 requests at once, and most fail with `429`. `rateLimit` starts five and holds the rest until the window slides.
974
+
975
+ ```js
976
+ import { createBus } from "nanoeffects";
977
+ import { rateLimit } from "nanoeffects/policies";
978
+
979
+ const bus = createBus();
980
+
981
+ // rateLimit — five requests may start per second, the others wait their turn
982
+ bus.on(
983
+ "address/geocode",
984
+ rateLimit(5, 1000, async (address, ctx) => {
985
+ const place = await api.geocode(address, { signal: ctx.signal });
986
+ ctx.emit("address/found", place);
987
+ }),
988
+ );
989
+ ```
990
+
991
+ More: [Policies, `rateLimit`](policies.md#ratelimit-stay-under-a-quota).
992
+
993
+ ### Many small events, one request
994
+
995
+ A page sends an analytics event on every click, 30 requests a minute. The server prefers one request with all of them. `batch` collects the events that come within a window and sends them together.
996
+
997
+ ```js
998
+ import { createBus } from "nanoeffects";
999
+ import { batch } from "nanoeffects/policies";
1000
+
1001
+ const bus = createBus();
1002
+
1003
+ // batch — everything that comes within 500 ms goes in one request
1004
+ bus.on(
1005
+ "analytics/event",
1006
+ batch(500, async (events, { signal }) => {
1007
+ await api.sendEvents(events, { signal });
1008
+ }),
1009
+ );
1010
+ ```
1011
+
1012
+ More: [Policies, `batch`](policies.md#batch-send-many-calls-as-one).
1013
+
924
1014
  ## Features and domain logic
925
1015
 
926
1016
  ### Get the result of emit
@@ -1300,7 +1390,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
1300
1390
  | Import | What is in it |
1301
1391
  | ------ | ------------- |
1302
1392
  | `nanoeffects` | `createBus`, the types |
1303
- | `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
1393
+ | `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue`, `dedupe`, `distinct`, `rateLimit`, `batch` |
1304
1394
  | `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
1305
1395
 
1306
1396
  ## Add the rest when you need it
@@ -1754,6 +1844,30 @@ Use this page when handlers overlap in a SPA (search-as-you-type, double submit,
1754
1844
 
1755
1845
  Start with `latest`. The table and sections below are the rest of the reference.
1756
1846
 
1847
+ ## On this page
1848
+
1849
+ - [`latest`: abort the previous run](#latest-abort-the-previous-run)
1850
+ - [Choose a policy](#choose-a-policy)
1851
+ - [Look up a policy](#look-up-a-policy)
1852
+ - [`latest`](#latest)
1853
+ - [`leading`](#leading)
1854
+ - [`debounce`](#debounce)
1855
+ - [`throttle`](#throttle)
1856
+ - [`queue`](#queue)
1857
+ - [`dedupe`](#dedupe)
1858
+ - [`distinct`](#distinct)
1859
+ - [`rateLimit`](#ratelimit)
1860
+ - [`batch`](#batch)
1861
+ - [`key`](#key)
1862
+ - [Configure debounce and throttle](#configure-debounce-and-throttle)
1863
+ - [`key`: isolate state by key](#key-isolate-state-by-key)
1864
+ - [Run calls one after another with `queue`](#run-calls-one-after-another-with-queue)
1865
+ - [`dedupe`: share one run between equal calls](#dedupe-share-one-run-between-equal-calls)
1866
+ - [`distinct`: skip a repeated payload](#distinct-skip-a-repeated-payload)
1867
+ - [`rateLimit`: stay under a quota](#ratelimit-stay-under-a-quota)
1868
+ - [`batch`: send many calls as one](#batch-send-many-calls-as-one)
1869
+ - [`withPolicy`: call a policy outside a bus](#withpolicy-call-a-policy-outside-a-bus)
1870
+
1757
1871
  ## `latest`: abort the previous run
1758
1872
 
1759
1873
  ```js
@@ -1785,6 +1899,10 @@ Every policy accepts `key` in its options (see [`key`: isolate state by key](#ke
1785
1899
  | `debounce(ms, fn, options?)` | run after `ms` of quiet |
1786
1900
  | `throttle(ms, fn, options?)` | at most one run per `ms` |
1787
1901
  | `queue(fn, options?)` | one after another, in order |
1902
+ | `dedupe(fn, options?)` | calls with the same key share the run that is going |
1903
+ | `distinct(fn, options?)` | a call with the same payload as the last one is skipped |
1904
+ | `rateLimit(limit, ms, fn, options?)` | at most `limit` runs start per `ms`, the rest wait |
1905
+ | `batch(ms, fn, options?)` | payloads of one window go to `fn` as one array |
1788
1906
 
1789
1907
  Each policy has a short entry in [Look up a policy](#look-up-a-policy).
1790
1908
 
@@ -1832,6 +1950,38 @@ Signature: `queue(fn, options?)`.
1832
1950
 
1833
1951
  More: [Run calls one after another with `queue`](#run-calls-one-after-another-with-queue).
1834
1952
 
1953
+ ### `dedupe`
1954
+
1955
+ Calls with the same key that come while a run is going wait for that run and get its result. Nothing is cached.
1956
+
1957
+ Signature: `dedupe(fn, options?)`.
1958
+
1959
+ More: [`dedupe`: share one run between equal calls](#dedupe-share-one-run-between-equal-calls).
1960
+
1961
+ ### `distinct`
1962
+
1963
+ A call whose payload equals the last one is skipped and resolves with `undefined`.
1964
+
1965
+ Signature: `distinct(fn, options?)`.
1966
+
1967
+ More: [`distinct`: skip a repeated payload](#distinct-skip-a-repeated-payload).
1968
+
1969
+ ### `rateLimit`
1970
+
1971
+ At most `limit` runs start in any window of `ms`. The other calls wait for their turn.
1972
+
1973
+ Signature: `rateLimit(limit, ms, fn, options?)`.
1974
+
1975
+ More: [`rateLimit`: stay under a quota](#ratelimit-stay-under-a-quota).
1976
+
1977
+ ### `batch`
1978
+
1979
+ Collects the payloads that come in a window of `ms` and calls `fn` once with all of them.
1980
+
1981
+ Signature: `batch(ms, fn, options?)`.
1982
+
1983
+ More: [`batch`: send many calls as one](#batch-send-many-calls-as-one).
1984
+
1835
1985
  ### `key`
1836
1986
 
1837
1987
  An option of every policy. It splits the state of the policy by a property, for example by id: calls with different keys do not affect each other.
@@ -1917,6 +2067,134 @@ Every call returns a promise with the result of its own run. An error rejects on
1917
2067
  - When a running call aborts, its slot is freed at once and its result is dropped.
1918
2068
  - Pass `signal` into what `fn` does. A `fn` that ignores it can overlap with the next run.
1919
2069
 
2070
+ ## `dedupe`: share one run between equal calls
2071
+
2072
+ > **Level B or C.** Use it for reads that several parts of the screen start at the same time.
2073
+
2074
+ **Problem.** Three components ask for user `1` in the same tick. Without a policy that is three identical requests.
2075
+
2076
+ **When.** A read that is safe to repeat: a profile, a config, a list by id. For a write use [`leading`](prevent-double-submit.md) or [`queue`](#run-calls-one-after-another-with-queue): see the abort rule below.
2077
+
2078
+ `dedupe` starts `fn` for the first call. A call with the same key that comes while that run is going waits for it and gets its result, or its error. When the run ends, `dedupe` forgets it, and the next call starts `fn` again. It is not a cache: it never returns an old value.
2079
+
2080
+ ```js
2081
+ import { dedupe, withPolicy } from "nanoeffects/policies";
2082
+
2083
+ // dedupe — callers with the same id share one request
2084
+ const policy = dedupe((id, { signal }) => api.getUser(id, { signal }));
2085
+
2086
+ // withPolicy — a plain function, so the example shows the result of each call
2087
+ const loadUser = withPolicy(policy);
2088
+
2089
+ // two callers, one request: both get the same user
2090
+ const [first, second] = await Promise.all([loadUser(1), loadUser(1)]);
2091
+ ```
2092
+
2093
+ On a bus it looks the same, `bus.on("user/load", dedupe(handler))`. To get the result of `emit` use an action or a job ([Actions](actions.md)).
2094
+
2095
+ | Option | Meaning |
2096
+ | ------ | ------- |
2097
+ | `key` | what makes two calls "the same". Default: the payload itself, compared like a `Map` key. Objects never match unless `key` says how. `key: () => "all"` shares one run between every call |
2098
+
2099
+ **Abort:**
2100
+
2101
+ - A caller whose task aborts resolves with `undefined`. The others go on.
2102
+ - The run belongs to the task of the first caller. If that task aborts while others still wait, the run is aborted and started again with the next caller, so `fn` can run twice for one request. That is fine for a read and wrong for a write.
2103
+ - Pass `signal` into what `fn` does, as for any policy.
2104
+
2105
+ ## `distinct`: skip a repeated payload
2106
+
2107
+ > **Level B.** Use it when the same value keeps arriving and repeating the work is a waste.
2108
+
2109
+ **Problem.** A form saves on every change. A user types a letter and deletes it, and the same text goes to the server again.
2110
+
2111
+ `distinct` compares the payload of each call with the payload of the last call it let through. An equal payload is skipped: `fn` does not run and the call resolves with `undefined`. It remembers one payload, never a result.
2112
+
2113
+ ```js
2114
+ import { distinct, withPolicy } from "nanoeffects/policies";
2115
+
2116
+ // distinct — runs only when the text differs from the last one it let through
2117
+ const policy = distinct(async (text) => {
2118
+ await api.save(text);
2119
+ });
2120
+ const save = withPolicy(policy);
2121
+
2122
+ await save("hello"); // runs
2123
+ await save("hello"); // skipped, resolves undefined
2124
+ await save("hello!"); // runs: the text changed
2125
+ ```
2126
+
2127
+ | Option | Meaning |
2128
+ | ------ | ------- |
2129
+ | `equals(previous, next)` | tells whether two payloads are the same. Default: `Object.is` |
2130
+ | `key` | each key remembers its own payload |
2131
+
2132
+ - The payload counts as seen from the moment its run starts, so the same payload that arrives while the run goes is skipped too.
2133
+ - It is forgotten when the run fails or its task aborts, so a retry with the same payload runs.
2134
+ - The memory of a key stays after the run. Unlike other policies, `distinct` does not drop idle keys, so give `key` values from a bounded set (a form id, not a request id).
2135
+
2136
+ ## `rateLimit`: stay under a quota
2137
+
2138
+ > **Level C.** Use it when an API allows N requests per period.
2139
+
2140
+ **Problem.** A geocoding API allows 5 requests per second. A list of 40 addresses sends 40 requests at once, and most of them fail with `429`.
2141
+
2142
+ `rateLimit(limit, ms, fn)` lets at most `limit` runs start in any window of `ms` milliseconds. The window slides: it is counted back from each call. A call over the limit waits and starts, in order, when the oldest start leaves the window.
2143
+
2144
+ ```js
2145
+ import { rateLimit, withPolicy } from "nanoeffects/policies";
2146
+
2147
+ // rateLimit — 5 runs may start per second, the rest wait their turn
2148
+ const policy = rateLimit(5, 1000, (address, { signal }) => {
2149
+ return api.geocode(address, { signal });
2150
+ });
2151
+ const geocode = withPolicy(policy);
2152
+
2153
+ const addresses = ["10 Main St", "22 Oak Ave", "5 Pine Rd", "8 Elm St", "3 Lake Dr", "9 Hill Ln"];
2154
+
2155
+ // the first five start at once, the sixth a second later
2156
+ const results = await Promise.all(addresses.map(geocode));
2157
+ ```
2158
+
2159
+ | Option | Meaning |
2160
+ | ------ | ------- |
2161
+ | `overflow` | `"wait"` (default) keeps a call until it can start. `"drop"` drops a call that finds the window full: it resolves with `undefined` |
2162
+ | `key` | each key has its own window, for example one per user |
2163
+
2164
+ - It limits **starts**, not how many runs go at the same time, and it does not wait for a run to end. For both limits write `queue(rateLimit(5, 1000, fn), { concurrency: 2 })`: a call keeps its slot in the queue while it waits for the window.
2165
+ - A call whose task aborts while it waits leaves the line. Aborting a run that has started does not give its place in the window back: the request was sent.
2166
+ - A `limit` below 1 is 1.
2167
+
2168
+ ## `batch`: send many calls as one
2169
+
2170
+ > **Level C.** Use it when work is cheaper in bulk: events for analytics, log lines, "get these ids".
2171
+
2172
+ **Problem.** A page sends an analytics event for every click, 30 requests a minute. The server prefers one request with all of them.
2173
+
2174
+ `batch(ms, fn)` opens a window of `ms` at the first call and collects the payloads that come in it. When the window closes, `fn` runs once with an array of them, in the order of the calls. The window does not move when more calls come, so a steady stream is sent every `ms`, not only when it stops.
2175
+
2176
+ ```js
2177
+ import { batch, withPolicy } from "nanoeffects/policies";
2178
+
2179
+ // batch — fn gets an array of everything that came within 500 ms
2180
+ const policy = batch(500, async (events) => {
2181
+ await api.sendEvents(events);
2182
+ });
2183
+ const track = withPolicy(policy);
2184
+
2185
+ track({ name: "open" });
2186
+ track({ name: "click" }); // one request with both events, 500 ms after the first
2187
+ ```
2188
+
2189
+ | Option | Meaning |
2190
+ | ------ | ------- |
2191
+ | `maxSize` | send the batch at once when it holds this many payloads |
2192
+ | `key` | each key collects its own batch, for example one per user |
2193
+
2194
+ - Every call resolves with what `fn` returned for its batch: the same value for all, or the same error. `batch` does not split an array result between callers.
2195
+ - A call whose task aborts while the batch is collecting leaves it and resolves with `undefined`. When every call has left, nothing runs.
2196
+ - `fn` gets the context of the first caller that is still waiting. If that task aborts while `fn` runs, the run is aborted and every caller of that batch resolves with `undefined`. The run is not repeated: part of a bulk call may have happened.
2197
+
1920
2198
  ## `withPolicy`: call a policy outside a bus
1921
2199
 
1922
2200
  **Problem.** You want a debounced input handler or a "latest request wins" fetch for your own function, but the code has no bus: a DOM handler, a click callback, a function in a service. A policy needs a context on every call, so `debounce(300, search)("re")` does not work on its own.
package/llms.txt CHANGED
@@ -23,7 +23,7 @@ Rules that generated code often gets wrong:
23
23
 
24
24
  ## Async UI and unreliable networks
25
25
 
26
- - [Policies](docs/policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, and `key`; `withPolicy` makes one a plain function without a bus
26
+ - [Policies](docs/policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, `dedupe`, `distinct`, `rateLimit`, `batch`, and `key`; `withPolicy` makes one a plain function without a bus
27
27
  - [Cancel stale requests and fix race conditions](docs/cancel-stale-requests.md): an older response overwrites a newer one; `latest`, `debounce`, `retry`, `timeout` in one recipe
28
28
  - [Pause between requests](docs/debounce-and-abort.md): `debounce` outside, `latest` inside, and when to use `throttle`
29
29
  - [Prevent a double submit](docs/prevent-double-submit.md): `leading`, one run per item with `key`, retry only on network errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanoeffects",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Business logic and side effects outside UI components, as plain async functions. Tiny, no dependencies.",
5
5
  "keywords": [
6
6
  "event-bus",
@@ -226,7 +226,7 @@
226
226
  {
227
227
  "name": "policies (all)",
228
228
  "path": "dist/policies.js",
229
- "limit": "2.71 kB"
229
+ "limit": "3.71 kB"
230
230
  },
231
231
  {
232
232
  "name": "effects (all)",