@ryuzaki13/react-foundation-api 1.1.16 → 1.1.18

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 (36) hide show
  1. package/README.md +32 -43
  2. package/dist/chunks/{odataFetchFn-vnAXC-c0.js → odataFetchFn-B9wSQpUS.js} +11 -11
  3. package/dist/chunks/{odataFetchFn-vnAXC-c0.js.map → odataFetchFn-B9wSQpUS.js.map} +1 -1
  4. package/dist/odata/fetchCollectionData.d.ts +1 -1
  5. package/dist/odata/fetchCollectionData.d.ts.map +1 -1
  6. package/dist/odata/index.js +111 -112
  7. package/dist/odata/index.js.map +1 -1
  8. package/dist/odata/projectODataCollectionSort.d.ts +1 -1
  9. package/dist/odata/projectODataCollectionSort.d.ts.map +1 -1
  10. package/dist/odata/types.d.ts +1 -2
  11. package/dist/odata/types.d.ts.map +1 -1
  12. package/dist/odata/useODataCollection.d.ts +1 -1
  13. package/dist/odata/useODataCollection.d.ts.map +1 -1
  14. package/dist/odata/useODataCollectionQuery.d.ts +1 -1
  15. package/dist/odata/useODataCollectionQuery.d.ts.map +1 -1
  16. package/dist/odata/useODataEntity.d.ts +1 -1
  17. package/dist/odata/useODataEntity.d.ts.map +1 -1
  18. package/dist/persisted/index.js +1 -1
  19. package/package.json +2 -2
  20. package/src/adt/README.mdx +461 -0
  21. package/src/async/README.mdx +628 -0
  22. package/src/error-report/README.mdx +471 -0
  23. package/src/foundationApi.mdx +123 -0
  24. package/src/http/README.mdx +570 -0
  25. package/src/odata/README.mdx +5142 -0
  26. package/src/persisted/README.mdx +1080 -0
  27. package/src/resource/README.mdx +820 -0
  28. package/src/server-fn/README.mdx +596 -0
  29. package/src/transport/README.mdx +528 -0
  30. package/src/README.md +0 -937
  31. package/src/async/README.md +0 -623
  32. package/src/async/async.mdx +0 -6
  33. package/src/odata/README.md +0 -761
  34. package/src/odata/odataFetchFn.mdx +0 -6
  35. package/src/persisted/README.md +0 -598
  36. package/src/persisted/persisted.mdx +0 -6
@@ -0,0 +1,628 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation API/Orchestration/Async Tasks" />
4
+
5
+ # Асинхронные задачи через `@ryuzaki13/react-foundation-api/async`
6
+
7
+ Модуль запускает независимые Promise-задачи, сохраняет связь результата с исходным элементом и формирует удобную сводку для UI/логов.
8
+
9
+ ## Какой runner выбрать
10
+
11
+ | Сценарий | Функция |
12
+ | --- | --- |
13
+ | Задач немного, можно начать все одновременно, любая ошибка должна завершить batch исключением | `runAsyncTasks` |
14
+ | Нужно ограничить число одновременных запросов, показать progress или поддержать abort | `runConcurrentTasks` |
15
+
16
+ Обе функции выполняют задачи параллельно. Они не создают web workers и не ускоряют CPU-bound JavaScript: Promise-concurrency полезна прежде всего для I/O — HTTP, чтения storage и других async операций.
17
+
18
+ ### Ментальная модель concurrency
19
+
20
+ ```text
21
+ Параллельно в Promise-смысле:
22
+ request A ───────────────►
23
+ request B ─────────►
24
+ request C ─────────────────────►
25
+
26
+ JavaScript thread при этом остаётся один.
27
+ ```
28
+
29
+ Пока request ждёт сеть, event loop может продолжить другую задачу. Но три тяжёлых синхронных вычисления всё равно блокируют один thread по очереди.
30
+
31
+ ## Импорт
32
+
33
+ ```ts
34
+ import {
35
+ AsyncTasksError,
36
+ ConcurrencyPartialError,
37
+ runAsyncTasks,
38
+ runConcurrentTasks
39
+ } from "@ryuzaki13/react-foundation-api/async";
40
+
41
+ import type {
42
+ AsyncTask,
43
+ RunAsyncTasksResult,
44
+ RunConcurrentTasksOptions,
45
+ RunConcurrentTasksResult
46
+ } from "@ryuzaki13/react-foundation-api/async";
47
+ ```
48
+
49
+ Модуль не требует React или TanStack Query.
50
+
51
+ ## База: `async`, `Promise` и `await`
52
+
53
+ `async`-функция всегда возвращает `Promise`. `await` приостанавливает только текущую async-функцию, а не весь browser thread:
54
+
55
+ ```ts
56
+ async function loadName() {
57
+ const response = await fetch("/api/me");
58
+ return response.text();
59
+ }
60
+
61
+ const namePromise = loadName(); // Promise<string>.
62
+ const name = await namePromise; // string.
63
+ ```
64
+
65
+ Ошибку Promise обрабатывают через `try/catch` вокруг `await`.
66
+
67
+ ## `runAsyncTasks`
68
+
69
+ ### Быстрый старт
70
+
71
+ ```ts
72
+ const result = await runAsyncTasks([
73
+ { name: "profile", run: () => fetchProfile() },
74
+ { name: "settings", run: () => fetchSettings() }
75
+ ]);
76
+
77
+ console.log(result.summary);
78
+ // { total: 2, succeeded: 2, failed: 0 }
79
+ ```
80
+
81
+ Все `run` запускаются сразу через `Promise.all`. Важное отличие от обычного `Promise.all`: ошибка одной задачи перехватывается внутри runner-а, поэтому остальные задачи успевают завершиться. После завершения всего batch функция либо возвращает полный результат, либо бросает `AsyncTasksError` с тем же полным результатом.
82
+
83
+ ### Обработка частичного успеха
84
+
85
+ ```ts
86
+ try {
87
+ const result = await runAsyncTasks(tasks, {
88
+ errorMessage: "Не все стартовые данные загрузились"
89
+ });
90
+ useData(result.successes);
91
+ } catch (error) {
92
+ if (error instanceof AsyncTasksError) {
93
+ usePartialData(error.result.successes);
94
+ showErrors(error.result.errors);
95
+ return;
96
+ }
97
+
98
+ throw error;
99
+ }
100
+ ```
101
+
102
+ `AsyncTasksError.result` содержит:
103
+
104
+ ```ts
105
+ type RunAsyncTasksResult<TResult> = {
106
+ settled: readonly AsyncTaskSettled<TResult>[];
107
+ successes: readonly AsyncTaskSuccess<TResult>[];
108
+ errors: readonly AsyncTaskFailure[];
109
+ summary: {
110
+ total: number;
111
+ succeeded: number;
112
+ failed: number;
113
+ };
114
+ };
115
+ ```
116
+
117
+ Элементы `settled`, `successes` и `errors` сохраняют порядок исходного массива. Каждый элемент содержит `index` и `name`.
118
+
119
+ ```ts
120
+ if (item.status === "fulfilled") {
121
+ console.log(item.data);
122
+ } else {
123
+ console.error(item.error);
124
+ }
125
+ ```
126
+
127
+ TypeScript сужает union по `status`.
128
+
129
+ Пустой список возвращает успешный пустой результат. `runAsyncTasks` не имеет встроенного concurrency limit и AbortSignal.
130
+
131
+ ### Полные типы task/result
132
+
133
+ ```ts
134
+ type AsyncTask<TResult = unknown> = {
135
+ readonly name: string;
136
+ readonly run: () => Promise<TResult>;
137
+ };
138
+
139
+ type AsyncTaskSuccess<TResult> = {
140
+ readonly status: "fulfilled";
141
+ readonly index: number;
142
+ readonly name: string;
143
+ readonly data: TResult;
144
+ };
145
+
146
+ type AsyncTaskFailure = {
147
+ readonly status: "rejected";
148
+ readonly index: number;
149
+ readonly name: string;
150
+ readonly error: unknown;
151
+ };
152
+ ```
153
+
154
+ Имена не обязаны быть уникальными. Для надёжной identity используйте `index`, если caller допускает повторяющиеся names.
155
+
156
+ ### Синхронный throw внутри `run`
157
+
158
+ Хотя тип обещает Promise, runtime callback может бросить сразу:
159
+
160
+ ```ts
161
+ {
162
+ name: "invalid",
163
+ run: () => {
164
+ throw new Error("Ошибка до Promise");
165
+ }
166
+ }
167
+ ```
168
+
169
+ Runner ловит такой throw так же, как rejected Promise, и помещает его в `AsyncTasksError.result.errors`.
170
+
171
+ ### Все задачи действительно стартуют сразу
172
+
173
+ ```ts
174
+ await runAsyncTasks(tenThousandRequests);
175
+ ```
176
+
177
+ создаст попытку запуска всех 10 000 operations. Это может перегрузить browser, backend или connection pool. Для большого массива используйте `runConcurrentTasks`.
178
+
179
+ ### Homogeneous result type
180
+
181
+ Один вызов использует общий `TResult`. Если задачи возвращают принципиально разные shapes, создайте union/discriminated result либо выполняйте независимые batches:
182
+
183
+ ```ts
184
+ type StartupResult =
185
+ | { kind: "profile"; value: Profile }
186
+ | { kind: "settings"; value: Settings };
187
+ ```
188
+
189
+ ### Default error message
190
+
191
+ Если `options.errorMessage` отсутствует:
192
+
193
+ ```text
194
+ Не удалось выполнить все асинхронные задачи.
195
+ ```
196
+
197
+ `AsyncTasksError.name` равен `"AsyncTasksError"`, а `result` содержит полный batch snapshot.
198
+
199
+ ## `runConcurrentTasks`
200
+
201
+ ### Ограничение одновременных запросов
202
+
203
+ ```ts
204
+ const result = await runConcurrentTasks(
205
+ userIds,
206
+ async (userId, { signal }) => {
207
+ const response = await fetch(`/api/users/${userId}`, { signal });
208
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
209
+ return response.json();
210
+ },
211
+ { concurrency: 4 }
212
+ );
213
+ ```
214
+
215
+ Не более четырёх задач будут находиться в работе одновременно. Значение по умолчанию — `4`. Если элементов меньше, workers создаются только по числу элементов.
216
+
217
+ `concurrency` обязан быть целым числом не меньше `1`:
218
+
219
+ | Значение | Результат |
220
+ | --- | --- |
221
+ | `4` | Работает |
222
+ | `1` | Последовательное выполнение |
223
+ | `0`, `-1` | `RangeError` |
224
+ | `1.5`, `NaN` | `TypeError` |
225
+
226
+ ### Ошибки отдельных элементов
227
+
228
+ Runner не бросает ошибки отдельных задач. Он помещает их в `result.errors` и продолжает обработку:
229
+
230
+ ```ts
231
+ const result = await runConcurrentTasks(inputs, task);
232
+
233
+ if (result.errors.length > 0) {
234
+ for (const failure of result.errors) {
235
+ console.error(failure.index, failure.input, failure.error);
236
+ }
237
+ }
238
+ ```
239
+
240
+ Исключение самой функции возможно при невалидном `concurrency` или если callback `onProgress`/`onItemSuccess`/`onItemError` сам бросил ошибку. Callbacks должны быть безопасными и быстрыми.
241
+
242
+ ### Progress
243
+
244
+ ```ts
245
+ await runConcurrentTasks(files, uploadFile, {
246
+ concurrency: 3,
247
+ onProgress(progress) {
248
+ setProgress(progress.percentage);
249
+ },
250
+ onItemSuccess(item) {
251
+ console.log("Загружен", item.input.name);
252
+ },
253
+ onItemError(item) {
254
+ console.error("Ошибка", item.input.name, item.error);
255
+ }
256
+ });
257
+ ```
258
+
259
+ `onProgress` вызывается после каждой завершённой задачи. В первом вызове процент уже больше нуля; отдельного начального события `0%` нет.
260
+
261
+ ```ts
262
+ type ConcurrentTaskProgress<TInput, TResult> = {
263
+ total: number;
264
+ completed: number;
265
+ succeeded: number;
266
+ failed: number;
267
+ running: number;
268
+ pending: number;
269
+ percentage: number;
270
+ aborted: boolean;
271
+ lastSettled?: ConcurrentTaskSettled<TInput, TResult>;
272
+ };
273
+ ```
274
+
275
+ `percentage` округляется через `Math.round`.
276
+
277
+ ### Точная последовательность callbacks
278
+
279
+ Для успешной task:
280
+
281
+ ```text
282
+ task resolved
283
+ → сохранить fulfilled item
284
+ → увеличить succeeded
285
+ → onItemSuccess(item)
286
+ → уменьшить running
287
+ → увеличить completed
288
+ → onProgress(progress)
289
+ ```
290
+
291
+ Для rejected task аналогично вызывается `onItemError`, затем `onProgress`.
292
+
293
+ Callbacks синхронные: runner не `await`-ит Promise, который callback мог случайно вернуть. Делайте их быстрыми state/logging callbacks.
294
+
295
+ Критически важно не бросать из callbacks. Если `onItemSuccess` бросит, worker попадёт в `catch`, перезапишет item как rejected и вызовет `onItemError`. Если затем бросит `onItemError` или `onProgress`, весь runner может reject. Кроме того, промежуточные progress counters могут стать нелогичными. Оборачивайте необязательную telemetry:
296
+
297
+ ```ts
298
+ onItemSuccess(item) {
299
+ try {
300
+ analytics.track("uploaded", { index: item.index });
301
+ } catch (error) {
302
+ console.warn("Telemetry недоступна", error);
303
+ }
304
+ }
305
+ ```
306
+
307
+ ### Abort
308
+
309
+ ```ts
310
+ const controller = new AbortController();
311
+
312
+ const promise = runConcurrentTasks(urls, async (url, { signal }) => {
313
+ const response = await fetch(url, { signal });
314
+ return response.text();
315
+ }, {
316
+ concurrency: 2,
317
+ signal: controller.signal
318
+ });
319
+
320
+ controller.abort();
321
+ const result = await promise;
322
+ ```
323
+
324
+ После abort новые задачи не стартуют. Уже начатая операция прервётся только если сама использует переданный `signal`. Runner ждёт завершения начатых tasks и возвращает:
325
+
326
+ - `aborted: true`;
327
+ - обработанные элементы в `settled`;
328
+ - не начатые элементы в `unprocessed`.
329
+
330
+ Abort не является ошибкой runner-а и сам по себе не приводит к `throw`.
331
+
332
+ ### Signal уже aborted до запуска
333
+
334
+ Для непустого input ни одна task не стартует:
335
+
336
+ ```ts
337
+ const controller = new AbortController();
338
+ controller.abort();
339
+
340
+ const result = await runConcurrentTasks([1, 2, 3], task, {
341
+ signal: controller.signal
342
+ });
343
+ ```
344
+
345
+ ```ts
346
+ result.aborted; // true
347
+ result.settled; // []
348
+ result.unprocessed; // все три input с индексами
349
+ ```
350
+
351
+ Initial progress event `0%` не генерируется.
352
+
353
+ ### Abort уже начатых tasks
354
+
355
+ Runner только прекращает выдавать новые inputs workers. Реальная отмена зависит от task:
356
+
357
+ ```ts
358
+ // Signal используется — fetch может прерваться.
359
+ async (url, { signal }) => fetch(url, { signal })
360
+ ```
361
+
362
+ ```ts
363
+ // Signal игнорируется — task завершится несмотря на abort batch-а.
364
+ async (url) => fetch(url)
365
+ ```
366
+
367
+ Если task с signal отклоняется AbortError, она попадёт в `errors`, а ещё не стартовавшие inputs — в `unprocessed`.
368
+
369
+ ### Порядок результата
370
+
371
+ Tasks могут завершаться в любом порядке, callbacks идут в порядке фактического завершения. Итоговые `settled`, `successes`, `errors` и `unprocessed` собираются по исходным индексам, поэтому результат детерминирован относительно input.
372
+
373
+ ### Полный result
374
+
375
+ ```ts
376
+ type RunConcurrentTasksResult<TInput, TResult> = {
377
+ settled: ConcurrentTaskSettled<TInput, TResult>[];
378
+ successes: ConcurrentTaskSuccess<TInput, TResult>[];
379
+ errors: ConcurrentTaskError<TInput>[];
380
+ unprocessed: ConcurrentTaskUnprocessed<TInput>[];
381
+ aborted: boolean;
382
+ summary: {
383
+ total: number;
384
+ completed: number;
385
+ succeeded: number;
386
+ failed: number;
387
+ unprocessed: number;
388
+ };
389
+ };
390
+ ```
391
+
392
+ Invariant после нормального завершения:
393
+
394
+ ```text
395
+ summary.total = summary.completed + summary.unprocessed
396
+ summary.completed = summary.succeeded + summary.failed
397
+ ```
398
+
399
+ ### Пустой input
400
+
401
+ Возвращается empty result. Если signal уже aborted, `aborted` будет `true`; иначе `false`. `percentage` отсутствует в final summary, а progress callback не вызывается.
402
+
403
+ ## Практический пример: upload batch
404
+
405
+ ```ts
406
+ type UploadInput = {
407
+ id: string;
408
+ file: File;
409
+ };
410
+
411
+ const result = await runConcurrentTasks(
412
+ files,
413
+ async ({ id, file }, { signal }) => {
414
+ const body = new FormData();
415
+ body.set("file", file);
416
+
417
+ const response = await fetch(`/api/files/${encodeURIComponent(id)}`, {
418
+ method: "PUT",
419
+ body,
420
+ signal
421
+ });
422
+
423
+ if (!response.ok) {
424
+ throw new Error(`HTTP ${response.status}`);
425
+ }
426
+
427
+ return { id, uploaded: true };
428
+ },
429
+ {
430
+ concurrency: 3,
431
+ signal: controller.signal,
432
+ onProgress: ({ completed, total, percentage }) => {
433
+ console.log(`${completed}/${total}: ${percentage}%`);
434
+ }
435
+ }
436
+ );
437
+ ```
438
+
439
+ ### Retry только failures
440
+
441
+ ```ts
442
+ const retryInputs = result.errors.map((item) => item.input);
443
+
444
+ const retryResult = await runConcurrentTasks(retryInputs, uploadTask, {
445
+ concurrency: 2
446
+ });
447
+ ```
448
+
449
+ У write operations retry безопасен только при idempotent server contract. Потерянный response не доказывает, что первая запись не произошла.
450
+
451
+ ### Продолжить unprocessed после abort
452
+
453
+ ```ts
454
+ const remainingInputs = result.unprocessed.map((item) => item.input);
455
+ ```
456
+
457
+ Не добавляйте автоматически `errors` к `unprocessed`: rejected task уже запускалась и могла частично изменить backend.
458
+
459
+ ## `ConcurrencyPartialError`
460
+
461
+ `runConcurrentTasks` сам не создаёт этот error. Класс нужен, если слой приложения хочет преобразовать частичный результат в исключение:
462
+
463
+ ```ts
464
+ const result = await runConcurrentTasks(inputs, task);
465
+
466
+ if (result.errors.length > 0 || result.aborted) {
467
+ throw new ConcurrencyPartialError("Batch выполнен частично", result);
468
+ }
469
+ ```
470
+
471
+ Не путайте его с `AsyncTasksError`: первый создаёт consumer по своей policy, второй автоматически бросает `runAsyncTasks`.
472
+
473
+ `ConcurrencyPartialError.name` равен `"ConcurrencyPartialError"`, field `result` сохраняет переданный snapshot без преобразований.
474
+
475
+ ```ts
476
+ try {
477
+ const result = await runConcurrentTasks(inputs, task);
478
+
479
+ if (result.errors.length || result.aborted) {
480
+ throw new ConcurrencyPartialError(
481
+ "Не все файлы обработаны",
482
+ result
483
+ );
484
+ }
485
+ } catch (error) {
486
+ if (error instanceof ConcurrencyPartialError) {
487
+ showPartialResult(error.result);
488
+ return;
489
+ }
490
+
491
+ throw error;
492
+ }
493
+ ```
494
+
495
+ ## Выбор `concurrency`
496
+
497
+ Нет универсального максимума:
498
+
499
+ | Тип работы | Стартовая оценка |
500
+ | --- | --- |
501
+ | Тяжёлые SAP write requests | 2–4 |
502
+ | Небольшие independent reads | 4–8 |
503
+ | Local async storage | зависит от adapter-а |
504
+ | CPU-bound parsing | runner не решает задачу |
505
+
506
+ Начните с малого значения, измерьте latency/error rate и согласуйте лимит с backend.
507
+
508
+ `concurrency: 1` полезен для последовательной обработки независимых элементов с единым result contract. Но если шаг B зависит от output A, обычный последовательный workflow будет понятнее.
509
+
510
+ ## Тестирование
511
+
512
+ ### `runAsyncTasks`
513
+
514
+ ```ts
515
+ it("сохраняет partial result в AsyncTasksError", async () => {
516
+ const promise = runAsyncTasks([
517
+ { name: "ok", run: async () => 1 },
518
+ { name: "failed", run: async () => { throw new Error("boom"); } }
519
+ ]);
520
+
521
+ await expect(promise).rejects.toMatchObject({
522
+ name: "AsyncTasksError",
523
+ result: {
524
+ summary: { total: 2, succeeded: 1, failed: 1 }
525
+ }
526
+ });
527
+ });
528
+ ```
529
+
530
+ ### Проверка лимита concurrency
531
+
532
+ ```ts
533
+ it("не запускает больше двух tasks одновременно", async () => {
534
+ let running = 0;
535
+ let maximum = 0;
536
+
537
+ await runConcurrentTasks(
538
+ [1, 2, 3, 4],
539
+ async () => {
540
+ running += 1;
541
+ maximum = Math.max(maximum, running);
542
+ await Promise.resolve();
543
+ running -= 1;
544
+ return true;
545
+ },
546
+ { concurrency: 2 }
547
+ );
548
+
549
+ expect(maximum).toBe(2);
550
+ });
551
+ ```
552
+
553
+ Покройте:
554
+
555
+ - empty input;
556
+ - validation `concurrency`;
557
+ - sync throw и rejected Promise;
558
+ - input order result-а;
559
+ - completion order callbacks;
560
+ - abort до старта;
561
+ - abort во время работы;
562
+ - task, игнорирующую signal;
563
+ - unprocessed indices;
564
+ - custom partial-error policy.
565
+
566
+ ## Частые ошибки
567
+
568
+ ### Использовать `runAsyncTasks` для тысяч requests
569
+
570
+ Он стартует всё сразу. Нужен bounded runner.
571
+
572
+ ### Считать abort мгновенной отменой
573
+
574
+ Task обязана использовать signal.
575
+
576
+ ### Бросать из progress callback
577
+
578
+ Callbacks являются частью worker execution и могут сломать batch.
579
+
580
+ ### Считать individual errors исключением bounded runner-а
581
+
582
+ Читайте `result.errors`. Сам runner их не бросает.
583
+
584
+ ### Мутировать input
585
+
586
+ Result сохраняет ссылку на исходный input. Mutation может неожиданно изменить UI/result history.
587
+
588
+ ### Использовать batch как Query cache
589
+
590
+ Runner orchestrates promises, но не заменяет query identity, stale policy и cache ownership.
591
+
592
+ ## Практические рекомендации
593
+
594
+ - Не ставьте слишком большой concurrency: browser, proxy и backend имеют свои лимиты.
595
+ - Для write-операций продумайте идемпотентность: retry может повторить уже выполненное действие.
596
+ - Не меняйте input objects внутри task, если те же значения использует UI.
597
+ - Обрабатывайте `unknown` error через безопасный normalizer.
598
+ - Не используйте runner вместо TanStack Query cache для одного и того же resource.
599
+ - Если задача зависит от результата предыдущей, это не независимый batch: выполните зависимые шаги последовательно.
600
+
601
+ ## FAQ
602
+
603
+ ### Сохраняется ли порядок?
604
+
605
+ В final arrays — да, по input index. В callbacks — нет, они идут по завершению.
606
+
607
+ ### Останавливается ли batch на первой task error?
608
+
609
+ Нет. Оба runner-а дают уже запущенным tasks завершиться. `runAsyncTasks` бросает aggregated error после всего batch, bounded runner возвращает errors.
610
+
611
+ ### Можно ли менять concurrency во время запуска?
612
+
613
+ Нет. Значение фиксируется при старте.
614
+
615
+ ### Есть ли automatic retry?
616
+
617
+ Нет. Он должен быть явным и безопасным для operation.
618
+
619
+ ### Почему `error` имеет type `unknown`?
620
+
621
+ Promise может reject любым JS value. Сужайте/нормализуйте его перед UI/logging.
622
+
623
+ ## Полный API
624
+
625
+ | Группа | Exports |
626
+ | --- | --- |
627
+ | All-at-once | `runAsyncTasks`, `AsyncTasksError`, `AsyncTask`, `AsyncTaskSuccess`, `AsyncTaskFailure`, `AsyncTaskSettled`, `RunAsyncTasksResult`, `RunAsyncTasksOptions` |
628
+ | Bounded concurrency | `runConcurrentTasks`, `ConcurrencyPartialError`, `ConcurrentTaskContext`, `ConcurrentTaskFn`, `ConcurrentTaskSuccess`, `ConcurrentTaskError`, `ConcurrentTaskSettled`, `ConcurrentTaskUnprocessed`, `ConcurrentTaskProgress`, `RunConcurrentTasksOptions`, `RunConcurrentTasksResult` |