nanoeffects 0.34.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 (57) 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 +12 -11
  28. package/dist/signal.js +1 -1
  29. package/dist/svelte.d.ts +1 -1
  30. package/dist/testing.d.ts +1 -1
  31. package/dist/trace.d.ts +1 -1
  32. package/dist/{types-DxPZDzZG.d.ts → types-ByoeMqVh.d.ts} +2 -2
  33. package/dist/vue.d.ts +1 -1
  34. package/dist/{with-policy-Y12d-yx6.d.ts → with-policy-e4yx1vhN.d.ts} +1 -1
  35. package/docs/README.md +29 -2
  36. package/docs/cancel-stale-requests.md +1 -1
  37. package/docs/context.md +1 -1
  38. package/docs/package.md +2 -2
  39. package/docs/pass-the-signal.md +45 -17
  40. package/docs/policies.md +189 -1
  41. package/docs/react.md +41 -0
  42. package/docs/recipes.md +90 -0
  43. package/docs/ru/README.md +29 -2
  44. package/docs/ru/cancel-stale-requests.md +1 -1
  45. package/docs/ru/context.md +1 -1
  46. package/docs/ru/package.md +2 -2
  47. package/docs/ru/pass-the-signal.md +46 -18
  48. package/docs/ru/policies.md +189 -1
  49. package/docs/ru/react.md +41 -0
  50. package/docs/ru/recipes.md +90 -0
  51. package/docs/ru/svelte.md +41 -0
  52. package/docs/ru/vue.md +45 -0
  53. package/docs/svelte.md +41 -0
  54. package/docs/vue.md +45 -0
  55. package/llms-full.txt +455 -22
  56. package/llms.txt +1 -1
  57. package/package.json +3 -3
@@ -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/docs/ru/svelte.md CHANGED
@@ -6,6 +6,47 @@
6
6
 
7
7
  Нужны Svelte 5 или новее и установленная `nanoeffects`. Не знакомы с шиной? Сначала прочитайте [Руководство](guide.md).
8
8
 
9
+ Самый короткий вариант: поле поиска, которое показывает найденное обработчиком. Плагин для этого не нужен; разделы ниже добавляют помощник, который сокращает компонент.
10
+
11
+ ```js
12
+ // search.js
13
+ import { createBus } from "nanoeffects";
14
+ import { latest } from "nanoeffects/policies";
15
+
16
+ export const bus = createBus(); // шина, создаётся один раз
17
+
18
+ // логика поиска вне компонента; latest прерывает предыдущий запрос
19
+ bus.on(
20
+ "search",
21
+ latest(async (query, ctx) => {
22
+ const items = await api.search(query, { signal: ctx.signal });
23
+ ctx.emit("found", items);
24
+ }),
25
+ );
26
+ ```
27
+
28
+ Обработчик ничего не знает об UI, а компонент только отправляет `search` и слушает `found`.
29
+
30
+ <!-- docs-check: skip - a .svelte file -->
31
+ ```svelte
32
+ <script>
33
+ import { bus } from "./search";
34
+
35
+ let items = $state([]);
36
+
37
+ // on возвращает функцию отписки; $effect вызывает её при очистке
38
+ $effect(() => bus.on("found", (found) => (items = found)));
39
+ </script>
40
+
41
+ <input oninput={(event) => bus.emit("search", event.target.value)} />
42
+ <ul>
43
+ {#each items as item (item.id)}
44
+ <li>{item.title}</li>
45
+ {/each}
46
+ </ul>
47
+ ```
48
+
49
+
9
50
  ## Плагин на шине
10
51
 
11
52
  **Проблема.** Компонент Svelte читает значение через `$store`, а событие шины — не store. Без помощи компонент пишет свой `readable` и `bus.on` и должен не забыть отписаться. Эти строки копируются в каждый компонент.
package/docs/ru/vue.md CHANGED
@@ -6,6 +6,51 @@
6
6
 
7
7
  Нужны Vue 3.3 или новее и установленная `nanoeffects`. Не знакомы с шиной? Сначала прочитайте [Руководство](guide.md).
8
8
 
9
+ Самый короткий вариант: поле поиска, которое показывает найденное обработчиком. Плагин для этого не нужен; разделы ниже добавляют помощник, который сокращает компонент.
10
+
11
+ ```js
12
+ // search.js
13
+ import { createBus } from "nanoeffects";
14
+ import { latest } from "nanoeffects/policies";
15
+
16
+ export const bus = createBus(); // шина, создаётся один раз
17
+
18
+ // логика поиска вне компонента; latest прерывает предыдущий запрос
19
+ bus.on(
20
+ "search",
21
+ latest(async (query, ctx) => {
22
+ const items = await api.search(query, { signal: ctx.signal });
23
+ ctx.emit("found", items);
24
+ }),
25
+ );
26
+ ```
27
+
28
+ Обработчик ничего не знает об UI, а компонент только отправляет `search` и слушает `found`.
29
+
30
+ <!-- docs-check: skip - a .vue file -->
31
+ ```vue
32
+ <script setup>
33
+ import { onUnmounted, ref } from "vue";
34
+ import { bus } from "./search";
35
+
36
+ const items = ref([]);
37
+
38
+ // on возвращает функцию отписки; при размонтировании вызываем её
39
+ const unbind = bus.on("found", (found) => {
40
+ items.value = found;
41
+ });
42
+ onUnmounted(unbind);
43
+ </script>
44
+
45
+ <template>
46
+ <input @input="bus.emit('search', $event.target.value)" />
47
+ <ul>
48
+ <li v-for="item in items" :key="item.id">{{ item.title }}</li>
49
+ </ul>
50
+ </template>
51
+ ```
52
+
53
+
9
54
  ## Плагин на шине
10
55
 
11
56
  **Проблема.** Без помощи компонент пишет `watch(query, (value) => bus.emit(queryChanged(value)))` и собственную подписку в `onMounted` и `onUnmounted`. Эти строки копируются в каждый компонент, и в каждой копии что-нибудь забыто: очистка, проверка равенства или имя подписки в трассировке.
package/docs/svelte.md CHANGED
@@ -6,6 +6,47 @@ Use this page when your app is in Svelte 5 and a component has to show the resul
6
6
 
7
7
  You need Svelte 5 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
8
8
 
9
+ The shortest version: a search box that shows what the handler found. No plugin is needed for this; the sections below add a helper that shortens the component.
10
+
11
+ ```js
12
+ // search.js
13
+ import { createBus } from "nanoeffects";
14
+ import { latest } from "nanoeffects/policies";
15
+
16
+ export const bus = createBus(); // the bus, created once
17
+
18
+ // the search logic, outside the component; latest aborts the previous request
19
+ bus.on(
20
+ "search",
21
+ latest(async (query, ctx) => {
22
+ const items = await api.search(query, { signal: ctx.signal });
23
+ ctx.emit("found", items);
24
+ }),
25
+ );
26
+ ```
27
+
28
+ The handler knows nothing about the UI, and the component only sends `search` and listens to `found`.
29
+
30
+ <!-- docs-check: skip - a .svelte file -->
31
+ ```svelte
32
+ <script>
33
+ import { bus } from "./search";
34
+
35
+ let items = $state([]);
36
+
37
+ // on returns the unbind function; $effect calls it on cleanup
38
+ $effect(() => bus.on("found", (found) => (items = found)));
39
+ </script>
40
+
41
+ <input oninput={(event) => bus.emit("search", event.target.value)} />
42
+ <ul>
43
+ {#each items as item (item.id)}
44
+ <li>{item.title}</li>
45
+ {/each}
46
+ </ul>
47
+ ```
48
+
49
+
9
50
  ## Add the plugin to the bus
10
51
 
11
52
  **Problem.** A Svelte component reads a value with `$store`. An event of the bus is not a store, so without help the component writes its own `readable` and its own `bus.on`, and has to remember to stop listening. The same few lines are copied into every component.
package/docs/vue.md CHANGED
@@ -6,6 +6,51 @@ Use this page when your app is in Vue 3 and a component has to talk to the bus:
6
6
 
7
7
  You need Vue 3.3 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
8
8
 
9
+ The shortest version: a search box that shows what the handler found. No plugin is needed for this; the sections below add a helper that shortens the component.
10
+
11
+ ```js
12
+ // search.js
13
+ import { createBus } from "nanoeffects";
14
+ import { latest } from "nanoeffects/policies";
15
+
16
+ export const bus = createBus(); // the bus, created once
17
+
18
+ // the search logic, outside the component; latest aborts the previous request
19
+ bus.on(
20
+ "search",
21
+ latest(async (query, ctx) => {
22
+ const items = await api.search(query, { signal: ctx.signal });
23
+ ctx.emit("found", items);
24
+ }),
25
+ );
26
+ ```
27
+
28
+ The handler knows nothing about the UI, and the component only sends `search` and listens to `found`.
29
+
30
+ <!-- docs-check: skip - a .vue file -->
31
+ ```vue
32
+ <script setup>
33
+ import { onUnmounted, ref } from "vue";
34
+ import { bus } from "./search";
35
+
36
+ const items = ref([]);
37
+
38
+ // on returns the unbind function; unmounting calls it
39
+ const unbind = bus.on("found", (found) => {
40
+ items.value = found;
41
+ });
42
+ onUnmounted(unbind);
43
+ </script>
44
+
45
+ <template>
46
+ <input @input="bus.emit('search', $event.target.value)" />
47
+ <ul>
48
+ <li v-for="item in items" :key="item.id">{{ item.title }}</li>
49
+ </ul>
50
+ </template>
51
+ ```
52
+
53
+
9
54
  ## Add the plugin to the bus
10
55
 
11
56
  **Problem.** Without help, a component writes `watch(query, (value) => bus.emit(queryChanged(value)))` and its own subscription in `onMounted` and `onUnmounted`. The same few lines are copied into every component, and each copy forgets something: the cleanup, the equality check, or the name of the subscription in the trace.