react-inp-chunker 0.2.0 → 0.4.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 (38) hide show
  1. package/README.md +51 -7
  2. package/dist/core/index.cjs +1 -1
  3. package/dist/core/index.d.cts +2 -2
  4. package/dist/core/index.d.ts +2 -2
  5. package/dist/core/index.js +1 -1
  6. package/dist/{deferred-task-CwjZA3K-.js → deferred-task-Cai4fF4m.js} +87 -20
  7. package/dist/deferred-task-Cai4fF4m.js.map +1 -0
  8. package/dist/{deferred-task-EWH55ZyL.cjs → deferred-task-DRigxydg.cjs} +87 -20
  9. package/dist/deferred-task-DRigxydg.cjs.map +1 -0
  10. package/dist/{index-DShDP2VN.d.ts → index-DuUMqnsA.d.cts} +137 -27
  11. package/dist/index-DuUMqnsA.d.cts.map +1 -0
  12. package/dist/{index-DShDP2VN.d.cts → index-DuUMqnsA.d.ts} +137 -27
  13. package/dist/index-DuUMqnsA.d.ts.map +1 -0
  14. package/dist/index.cjs +32 -5
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +101 -12
  17. package/dist/index.d.cts.map +1 -1
  18. package/dist/index.d.ts +101 -12
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +32 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/production/core/index.cjs +1 -1
  23. package/dist/production/core/index.js +1 -1
  24. package/dist/production/{deferred-task-BGsfee-L.js → deferred-task-MsTNINAd.js} +61 -22
  25. package/dist/production/deferred-task-MsTNINAd.js.map +1 -0
  26. package/dist/production/{deferred-task-zVnq18IN.cjs → deferred-task-j8KF7lk7.cjs} +61 -22
  27. package/dist/production/deferred-task-j8KF7lk7.cjs.map +1 -0
  28. package/dist/production/index.cjs +32 -5
  29. package/dist/production/index.cjs.map +1 -1
  30. package/dist/production/index.js +32 -5
  31. package/dist/production/index.js.map +1 -1
  32. package/package.json +15 -9
  33. package/dist/deferred-task-CwjZA3K-.js.map +0 -1
  34. package/dist/deferred-task-EWH55ZyL.cjs.map +0 -1
  35. package/dist/index-DShDP2VN.d.cts.map +0 -1
  36. package/dist/index-DShDP2VN.d.ts.map +0 -1
  37. package/dist/production/deferred-task-BGsfee-L.js.map +0 -1
  38. package/dist/production/deferred-task-zVnq18IN.cjs.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { E as ChunkStatus, T as ChunkMachineState, a as DeferredTaskSnapshot, c as ChunkSchedulerOptions, i as DeferredTaskResult, l as ChunkSchedulerResult, m as YieldStrategy, n as DeferredTask, r as DeferredTaskOptions, s as ChunkScheduler, t as DeferredGenerator, u as ChunkSnapshot, w as ChunkEvent } from "./index-DShDP2VN.js";
1
+ import { D as ChunkStatus, E as ChunkMachineState, T as ChunkEvent, a as DeferredTaskSnapshot, c as ChunkSchedulerOptions, d as SkippedChunkItem, h as YieldStrategy, i as DeferredTaskResult, l as ChunkSchedulerResult, n as DeferredTask, r as DeferredTaskOptions, s as ChunkScheduler, t as DeferredGenerator, u as ChunkSnapshot } from "./index-DuUMqnsA.js";
2
2
  interface UseChunkedTaskOptions<T, R> {
3
3
  /** Max ms of uninterrupted work before yielding. Default 5. */
4
4
  readonly timeSliceMs?: number;
@@ -24,14 +24,44 @@ interface UseChunkedTaskOptions<T, R> {
24
24
  * without paying for an array copy on every yield checkpoint.
25
25
  */
26
26
  readonly onItem?: (result: R, item: T, index: number) => void;
27
+ /**
28
+ * Called when `processItem` throws. Return `'skip'` to drop that item and
29
+ * keep going — it shows up in `skipped` — or `'abort'` to end the task as
30
+ * an error, carrying whatever was collected before the throw. Omitting
31
+ * this defaults to `'abort'`, so existing callers see no behavior change.
32
+ */
33
+ readonly onError?: (error: unknown, item: T, index: number) => 'skip' | 'abort';
34
+ /**
35
+ * Called every time a slice ends and the task is about to yield, with how
36
+ * long that slice ran and how many items are done so far. Ships in the
37
+ * production build, unlike this package's own dev-only diagnostics — wire
38
+ * it to your own telemetry to verify, on real traffic, that this task is
39
+ * actually staying under `timeSliceMs`.
40
+ */
41
+ readonly onSlice?: (durationMs: number, processed: number) => void;
42
+ /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */
43
+ readonly onYield?: () => void;
44
+ /**
45
+ * External cancellation source — aborting it is equivalent to calling
46
+ * `cancel()`, and its `reason` comes through on `reason` below, so a
47
+ * cancellation caused by the signal is distinguishable from a direct
48
+ * `cancel()` call. A new reference restarts the task the same way a new
49
+ * `items` array does, so memoize it (e.g. from a ref or a parent-owned
50
+ * `AbortController`) unless you mean to restart on every render.
51
+ */
52
+ readonly signal?: AbortSignal;
27
53
  }
28
- interface UseChunkedTaskState<R> {
29
- readonly status: ChunkStatus;
54
+ /** Fields every status shares, independent of the `status`/`results`/`error` narrowing below. */
55
+ interface UseChunkedTaskShared {
30
56
  readonly processed: number;
31
57
  readonly total: number;
32
- readonly error: unknown;
33
- /** The full results array once the task has settled — `undefined` until then. */
34
- readonly results: readonly R[] | undefined;
58
+ /** Items `onError` has skipped so far, growing live as skips happen. Empty when unused. */
59
+ readonly skipped: readonly SkippedChunkItem[];
60
+ /**
61
+ * The `signal` option's `reason`, present only when cancellation came
62
+ * from that signal aborting rather than a direct `cancel()` call.
63
+ */
64
+ readonly reason: unknown;
35
65
  readonly isIdle: boolean;
36
66
  readonly isProcessing: boolean;
37
67
  readonly isPaused: boolean;
@@ -43,6 +73,26 @@ interface UseChunkedTaskState<R> {
43
73
  readonly resume: () => void;
44
74
  readonly cancel: () => void;
45
75
  }
76
+ /**
77
+ * Discriminated on `status`. Narrowing to `'done'` or `'cancelled'` narrows
78
+ * `results` to a defined array, no `=== undefined` check needed alongside
79
+ * `isDone`; narrowing to `'error'` narrows `error` to defined instead. The
80
+ * `is*` booleans above remain the simple way to branch when you only care
81
+ * about one status and don't need the narrowing.
82
+ */
83
+ type UseChunkedTaskState<R> = UseChunkedTaskShared & ({
84
+ readonly status: Exclude<ChunkStatus, 'done' | 'cancelled' | 'error'>;
85
+ readonly results: undefined;
86
+ readonly error: undefined;
87
+ } | {
88
+ readonly status: 'done' | 'cancelled';
89
+ readonly results: readonly R[];
90
+ readonly error: undefined;
91
+ } | {
92
+ readonly status: 'error';
93
+ readonly results: readonly R[];
94
+ readonly error: unknown;
95
+ });
46
96
  /**
47
97
  * Chunks a heavy, synchronous per-item computation over `items` so it never
48
98
  * blocks the main thread for longer than a time slice — the thing that
@@ -70,12 +120,32 @@ interface UseDeferredTaskOptions {
70
120
  readonly batchSize?: number;
71
121
  /** Start processing automatically when the task is (re)created. Default `true`. */
72
122
  readonly autoStart?: boolean;
123
+ /**
124
+ * Called every time a segment-batch ends and the task is about to yield,
125
+ * with how long that slice ran and how many segments are done so far.
126
+ * Ships in the production build — see the identical option on
127
+ * `useChunkedTask`/`createChunkScheduler` for why.
128
+ */
129
+ readonly onSlice?: (durationMs: number, segmentsDone: number) => void;
130
+ /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */
131
+ readonly onYield?: () => void;
132
+ /**
133
+ * External cancellation source — aborting it is equivalent to calling
134
+ * `cancel()`, and its `reason` comes through on `reason` below, so a
135
+ * cancellation caused by the signal is distinguishable from a direct
136
+ * `cancel()` call. A new reference restarts the task the same way a new
137
+ * entry in `deps` does, so memoize it unless you mean to restart on
138
+ * every render.
139
+ */
140
+ readonly signal?: AbortSignal;
73
141
  }
74
- interface UseDeferredTaskState<T> {
75
- readonly status: ChunkStatus;
76
- readonly error: unknown;
77
- /** The returned value once the task settles with `done` — `undefined` until then. */
78
- readonly value: T | undefined;
142
+ /** Fields every status shares, independent of the `status`/`value`/`error` narrowing below. */
143
+ interface UseDeferredTaskShared {
144
+ /**
145
+ * The `signal` option's `reason`, present only when cancellation came
146
+ * from that signal aborting rather than a direct `cancel()` call.
147
+ */
148
+ readonly reason: unknown;
79
149
  readonly isIdle: boolean;
80
150
  readonly isProcessing: boolean;
81
151
  readonly isPaused: boolean;
@@ -87,6 +157,25 @@ interface UseDeferredTaskState<T> {
87
157
  readonly resume: () => void;
88
158
  readonly cancel: () => void;
89
159
  }
160
+ /**
161
+ * Discriminated on `status`. Narrowing to `'done'` narrows `value` to `T`
162
+ * itself — no `=== undefined` check needed alongside `isDone`, even when
163
+ * `T` allows `undefined` as a genuine return value. Narrowing to `'error'`
164
+ * narrows `error` to defined instead.
165
+ */
166
+ type UseDeferredTaskState<T> = UseDeferredTaskShared & ({
167
+ readonly status: Exclude<ChunkStatus, 'done' | 'error'>;
168
+ readonly value: undefined;
169
+ readonly error: undefined;
170
+ } | {
171
+ readonly status: 'done';
172
+ readonly value: T;
173
+ readonly error: undefined;
174
+ } | {
175
+ readonly status: 'error';
176
+ readonly value: undefined;
177
+ readonly error: unknown;
178
+ });
90
179
  /**
91
180
  * Runs an arbitrary heavy, synchronous generator function in chunks, so it
92
181
  * never produces a long task on its own. Where `useChunkedTask` chunks "one
@@ -99,5 +188,5 @@ interface UseDeferredTaskState<T> {
99
188
  * you would for `useMemo` or `useEffect`.
100
189
  */
101
190
  export declare function useDeferredTask<T>(task: () => DeferredGenerator<T>, deps: readonly unknown[], options?: UseDeferredTaskOptions): UseDeferredTaskState<T>;
102
- export type { ChunkEvent, ChunkMachineState, ChunkScheduler, ChunkSchedulerOptions, ChunkSchedulerResult, ChunkSnapshot, ChunkStatus, DeferredGenerator, DeferredTask, DeferredTaskOptions, DeferredTaskResult, DeferredTaskSnapshot, UseChunkedTaskOptions, UseChunkedTaskState, UseDeferredTaskOptions, UseDeferredTaskState, YieldStrategy };
191
+ export type { ChunkEvent, ChunkMachineState, ChunkScheduler, ChunkSchedulerOptions, ChunkSchedulerResult, ChunkSnapshot, ChunkStatus, DeferredGenerator, DeferredTask, DeferredTaskOptions, DeferredTaskResult, DeferredTaskSnapshot, SkippedChunkItem, UseChunkedTaskOptions, UseChunkedTaskState, UseDeferredTaskOptions, UseDeferredTaskState, YieldStrategy };
103
192
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/react/useChunkedTask.ts","../src/react/useDeferredTask.ts"],"mappings":";UAQiB,sBAAsB,GAAG;;WAE/B;;;;;;;WAOA;;;;;;WAMA;;WAEA;;;;;;;WAOA,UAAU,QAAQ,GAAG,MAAM,GAAG;;UAGxB,oBAAoB;WAC1B,QAAQ;WACR;WACA;WACA;;WAEA,kBAAkB;WAClB;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;;;;;;;;;;;;;;;;wBAoBK,eAAe,GAAG,GAChC,gBAAgB,KAChB,cAAc,MAAM,GAAG,kBAAkB,GACzC,UAAU,sBAAsB,GAAG,KAClC,oBAAoB;UC/DN;;WAEN;;;;;;WAMA;;WAEA;;UAGM,qBAAqB;WAC3B,QAAQ;WACR;;WAEA,OAAO;WACP;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;;;;;;;;;;;;wBAgBK,gBAAgB,GAC9B,YAAY,kBAAkB,IAC9B,0BACA,UAAU,yBACT,qBAAqB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/react/useChunkedTask.ts","../src/react/useDeferredTask.ts"],"mappings":";UAeiB,sBAAsB,GAAG;;WAE/B;;;;;;;WAOA;;;;;;WAMA;;WAEA;;;;;;;WAOA,UAAU,QAAQ,GAAG,MAAM,GAAG;;;;;;;WAO9B,WAAW,gBAAgB,MAAM,GAAG;;;;;;;;WAQpC,WAAW,oBAAoB;;WAE/B;;;;;;;;;WASA,SAAS;;;UAIV;WACC;WACA;;WAEA,kBAAkB;;;;;WAKlB;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;;;;;;;;KAUC,oBAAoB,KAAK;WAGpB,QAAQ,QAAQ;WAChB;WACA;;WAGA;WACA,kBAAkB;WAClB;;WAGA;WACA,kBAAkB;WAClB;;;;;;;;;;;;;;;;;wBAwBD,eAAe,GAAG,GAChC,gBAAgB,KAChB,cAAc,MAAM,GAAG,kBAAkB,GACzC,UAAU,sBAAsB,GAAG,KAClC,oBAAoB;UCjIN;;WAEN;;;;;;WAMA;;WAEA;;;;;;;WAOA,WAAW,oBAAoB;;WAE/B;;;;;;;;;WASA,SAAS;;;UAIV;;;;;WAKC;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;;;;;;;KASC,qBAAqB,KAAK;WAGrB,QAAQ,QAAQ;WAChB;WACA;;WAEA;WAAyB,OAAO;WAAY;;WAC5C;WAA0B;WAA2B;;;;;;;;;;;;;wBAkBtD,gBAAgB,GAC9B,YAAY,kBAAkB,IAC9B,0BACA,UAAU,yBACT,qBAAqB"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as createChunkScheduler, t as createDeferredTask } from "./deferred-task-CwjZA3K-.js";
1
+ import { n as createChunkScheduler, t as createDeferredTask } from "./deferred-task-Cai4fF4m.js";
2
2
  import * as React from "react";
3
3
  import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  // src/react/task-controller.ts
@@ -36,6 +36,8 @@ function useEffectEventFallback(callback) {
36
36
  }
37
37
  const useEffectEvent = React.useEffectEvent ?? useEffectEventFallback;
38
38
  // src/react/useChunkedTask.ts
39
+ /** Stable empty array so a never-skipped task doesn't hand out a fresh `[]` every render. */
40
+ const NO_SKIPPED = [];
39
41
  const IDLE_STATUS = "idle";
40
42
  /**
41
43
  * Chunks a heavy, synchronous per-item computation over `items` so it never
@@ -53,11 +55,18 @@ const IDLE_STATUS = "idle";
53
55
  * same as any other React dependency.
54
56
  */
55
57
  function useChunkedTask(items, processItem, options) {
56
- const { timeSliceMs, batchSize, collectResults, autoStart = true, onItem } = options ?? {};
58
+ const { timeSliceMs, batchSize, collectResults, autoStart = true, onItem, onError, onSlice, onYield, signal } = options ?? {};
57
59
  const stableProcessItem = useEffectEvent((item, index) => processItem(item, index));
58
60
  const stableOnItem = useEffectEvent((result, item, index) => {
59
61
  onItem?.(result, item, index);
60
62
  });
63
+ const stableOnError = useEffectEvent((error, item, index) => onError?.(error, item, index) ?? "abort");
64
+ const stableOnSlice = useEffectEvent((durationMs, processed) => {
65
+ onSlice?.(durationMs, processed);
66
+ });
67
+ const stableOnYield = useEffectEvent(() => {
68
+ onYield?.();
69
+ });
61
70
  const [controller] = useState(() => createTaskController());
62
71
  const idleSnapshot = useMemo(() => ({
63
72
  status: IDLE_STATUS,
@@ -70,9 +79,13 @@ function useChunkedTask(items, processItem, options) {
70
79
  items,
71
80
  processItem: stableProcessItem,
72
81
  onItem: stableOnItem,
82
+ onError: stableOnError,
83
+ onSlice: stableOnSlice,
84
+ onYield: stableOnYield,
73
85
  ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
74
86
  ...batchSize !== void 0 ? { batchSize } : {},
75
- ...collectResults !== void 0 ? { collectResults } : {}
87
+ ...collectResults !== void 0 ? { collectResults } : {},
88
+ ...signal !== void 0 ? { signal } : {}
76
89
  });
77
90
  controller.replace(scheduler);
78
91
  if (autoStart) scheduler.start();
@@ -86,6 +99,7 @@ function useChunkedTask(items, processItem, options) {
86
99
  batchSize,
87
100
  collectResults,
88
101
  autoStart,
102
+ signal,
89
103
  controller
90
104
  ]);
91
105
  const start = useCallback(() => controller.getCurrent()?.start(), [controller]);
@@ -98,6 +112,8 @@ function useChunkedTask(items, processItem, options) {
98
112
  total: snapshot.total,
99
113
  error: snapshot.error,
100
114
  results: snapshot.results,
115
+ skipped: snapshot.skipped ?? NO_SKIPPED,
116
+ reason: snapshot.reason,
101
117
  isIdle: snapshot.status === "idle",
102
118
  isProcessing: snapshot.status === "processing",
103
119
  isPaused: snapshot.status === "paused",
@@ -124,15 +140,24 @@ const IDLE_SNAPSHOT = { status: "idle" };
124
140
  * you would for `useMemo` or `useEffect`.
125
141
  */
126
142
  function useDeferredTask(task, deps, options) {
127
- const { timeSliceMs, batchSize, autoStart = true } = options ?? {};
143
+ const { timeSliceMs, batchSize, autoStart = true, onSlice, onYield, signal } = options ?? {};
128
144
  const stableTask = useEffectEvent(() => task());
145
+ const stableOnSlice = useEffectEvent((durationMs, segmentsDone) => {
146
+ onSlice?.(durationMs, segmentsDone);
147
+ });
148
+ const stableOnYield = useEffectEvent(() => {
149
+ onYield?.();
150
+ });
129
151
  const [controller] = useState(() => createTaskController());
130
152
  const snapshot = useSyncExternalStore(controller.subscribe, () => controller.getSnapshot() ?? IDLE_SNAPSHOT, () => IDLE_SNAPSHOT);
131
153
  useEffect(() => {
132
154
  const deferredTask = createDeferredTask({
133
155
  task: stableTask,
156
+ onSlice: stableOnSlice,
157
+ onYield: stableOnYield,
134
158
  ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
135
- ...batchSize !== void 0 ? { batchSize } : {}
159
+ ...batchSize !== void 0 ? { batchSize } : {},
160
+ ...signal !== void 0 ? { signal } : {}
136
161
  });
137
162
  controller.replace(deferredTask);
138
163
  if (autoStart) deferredTask.start();
@@ -145,6 +170,7 @@ function useDeferredTask(task, deps, options) {
145
170
  timeSliceMs,
146
171
  batchSize,
147
172
  autoStart,
173
+ signal,
148
174
  controller
149
175
  ]);
150
176
  const start = useCallback(() => controller.getCurrent()?.start(), [controller]);
@@ -155,6 +181,7 @@ function useDeferredTask(task, deps, options) {
155
181
  status: snapshot.status,
156
182
  error: snapshot.error,
157
183
  value: snapshot.value,
184
+ reason: snapshot.reason,
158
185
  isIdle: snapshot.status === "idle",
159
186
  isProcessing: snapshot.status === "processing",
160
187
  isPaused: snapshot.status === "paused",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/react/task-controller.ts","../src/react/use-effect-event.ts","../src/react/useChunkedTask.ts","../src/react/useDeferredTask.ts"],"sourcesContent":["/**\n * The glue between a task handle (`ChunkScheduler<R>`, `DeferredTask<T>` —\n * anything with `subscribe`/`getSnapshot`) recreated from scratch whenever a\n * hook's deps change, inside an effect, and `useSyncExternalStore`, which\n * wants one stable `subscribe` function for a component's whole lifetime.\n *\n * `useSyncExternalStore` resubscribes whenever the `subscribe` function\n * identity changes — fine for correctness, but it means the naive approach\n * (pass the current task's own `.subscribe` directly) either churns\n * subscriptions every render or, worse, drifts: the component keeps\n * `subscribe`d to whichever task was current when it last resubscribed, not\n * necessarily the one `getSnapshot` is reading from after a swap.\n *\n * A controller sidesteps that: it's created once per hook instance (lazy\n * `useState` initializer in the hook, so it's stable without touching a\n * ref), hands `useSyncExternalStore` a `subscribe`/`getSnapshot` pair that\n * never change identity, and internally forwards to whichever task is\n * \"current\" — re-pointing itself via `replace()` every time the effect\n * swaps in a new one. Generic over the task shape so both `useChunkedTask`\n * and `useDeferredTask` share this one implementation.\n *\n * Every property below is an arrow function, deliberately — not method\n * shorthand — so none of them close over `this`, and `controller.subscribe`\n * can be handed to `useSyncExternalStore` directly as a plain function\n * reference without an unbound-method footgun.\n */\nexport interface SubscribableTask<Snapshot> {\n subscribe(listener: () => void): () => void;\n getSnapshot(): Snapshot;\n}\n\nexport interface TaskController<Task extends SubscribableTask<unknown>> {\n subscribe: (listener: () => void) => () => void;\n getSnapshot: () => ReturnType<Task['getSnapshot']> | undefined;\n getCurrent: () => Task | null;\n /** Swaps in a new task (or none), detaching from the previous one first. */\n replace: (task: Task | null) => void;\n}\n\nexport function createTaskController<\n Task extends SubscribableTask<unknown>,\n>(): TaskController<Task> {\n let current: Task | null = null;\n let detachFromCurrent: (() => void) | undefined;\n const listeners = new Set<() => void>();\n\n const notify = (): void => {\n for (const listener of listeners) listener();\n };\n\n return {\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n getSnapshot: () =>\n current?.getSnapshot() as ReturnType<Task['getSnapshot']> | undefined,\n getCurrent: () => current,\n replace: (task) => {\n detachFromCurrent?.();\n current = task;\n detachFromCurrent = task?.subscribe(notify);\n notify();\n },\n };\n}\n","import * as React from 'react';\nimport { useCallback, useInsertionEffect, useRef } from 'react';\n\n/**\n * `useEffectEvent` only became a stable React export in 19.2. On React 18\n * and 19.0–19.1 it doesn't exist at all — and a named\n * `import { useEffectEvent } from 'react'` then fails outright: at module\n * link time under Node ESM (\"Named export 'useEffectEvent' not found\"), or\n * as \"useEffectEvent is not a function\" on first render in a bundler. Both\n * versions are inside this package's declared peer range, so that has to\n * work, not just type-check.\n *\n * So the native hook is read as a property of the namespace object, where a\n * missing export is plain `undefined` rather than a link error, and used\n * whenever the running React has it. Otherwise the fallback keeps the same\n * contract: a stable function identity that always calls the latest\n * callback. The latest callback is committed from an insertion effect —\n * after render, before any layout or passive effect of the same commit can\n * observe it — so effects always see the current closure, without the\n * render-time ref mutation the React Compiler rejects.\n */\ntype EffectEventHook = <Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n) => (...args: Args) => Return;\n\nexport function useEffectEventFallback<Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n): (...args: Args) => Return {\n const latest = useRef(callback);\n useInsertionEffect(() => {\n latest.current = callback;\n });\n return useCallback((...args: Args) => latest.current(...args), []);\n}\n\n// Through `unknown` on purpose: React 18's types have no `useEffectEvent`\n// member at all, so a direct assertion wouldn't compile against them.\nconst nativeUseEffectEvent = (\n React as unknown as { readonly useEffectEvent?: EffectEventHook }\n).useEffectEvent;\n\nexport const useEffectEvent: EffectEventHook =\n nativeUseEffectEvent ?? useEffectEventFallback;\n","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createChunkScheduler } from '../core/chunk-scheduler.js';\nimport type { ChunkScheduler, ChunkSnapshot } from '../core/chunk-scheduler.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\nexport interface UseChunkedTaskOptions<T, R> {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Items to process between clock checks. Default 1. Raise it only for\n * items measured in nanoseconds, where reading the clock costs more than\n * the work; a slice can then run up to `batchSize` items past its budget.\n * See the `batchSize` note on `createChunkScheduler`.\n */\n readonly batchSize?: number;\n /**\n * Keep every result in `results`. Default true. Set false when results\n * are consumed through `onItem`, to avoid retaining one result per item;\n * `results` is then an empty array once the task settles.\n */\n readonly collectResults?: boolean;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n /**\n * Called synchronously after each item is processed. This is the cheap\n * way to consume results incrementally while the task is still running —\n * write into a ref-backed buffer, a canvas, a chart, whatever you need —\n * without paying for an array copy on every yield checkpoint.\n */\n readonly onItem?: (result: R, item: T, index: number) => void;\n}\n\nexport interface UseChunkedTaskState<R> {\n readonly status: ChunkStatus;\n readonly processed: number;\n readonly total: number;\n readonly error: unknown;\n /** The full results array once the task has settled — `undefined` until then. */\n readonly results: readonly R[] | undefined;\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\nconst IDLE_STATUS: ChunkStatus = 'idle';\n\n/**\n * Chunks a heavy, synchronous per-item computation over `items` so it never\n * blocks the main thread for longer than a time slice — the thing that\n * costs you Interaction to Next Paint. Starts automatically by default;\n * pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.\n * behind a button).\n *\n * `processItem` and `onItem` can be fresh closures every render — the hook\n * always calls the latest version (via `useEffectEvent`) without that\n * causing the task to restart. What *does* restart the task is `items`\n * changing identity (a new array reference), or `timeSliceMs`/`autoStart`\n * changing. That's deliberate: a new `items` array means new work, and\n * memoizing it when you don't want that is the caller's call to make, the\n * same as any other React dependency.\n */\nexport function useChunkedTask<T, R>(\n items: readonly T[],\n processItem: (item: T, index: number) => R,\n options?: UseChunkedTaskOptions<T, R>,\n): UseChunkedTaskState<R> {\n const {\n timeSliceMs,\n batchSize,\n collectResults,\n autoStart = true,\n onItem,\n } = options ?? {};\n\n // Stable identity, always runs the latest processItem/onItem passed this\n // render — the React-Compiler-safe replacement for a ref mutated during\n // render. Effect Events are meant to be called from inside an effect\n // (here: from the scheduler's async loop, which the effect below owns),\n // never during render itself.\n const stableProcessItem = useEffectEvent((item: T, index: number) =>\n processItem(item, index),\n );\n const stableOnItem = useEffectEvent((result: R, item: T, index: number) => {\n onItem?.(result, item, index);\n });\n\n // Created once, lazily, via useState rather than a ref mutated during\n // render — the controller itself is inert (no subscriptions, no timers)\n // until an effect calls replace() on it, so a discarded render (double\n // render in dev, or one React throws away) never leaks anything.\n const [controller] = useState(() => createTaskController<ChunkScheduler<R>>());\n\n // What getSnapshot() reports before the effect below has ever run —\n // including during SSR, where effects never run at all.\n const idleSnapshot = useMemo<ChunkSnapshot<R>>(\n () => ({ status: IDLE_STATUS, processed: 0, total: items.length }),\n [items.length],\n );\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? idleSnapshot,\n () => idleSnapshot,\n );\n\n useEffect(() => {\n const scheduler = createChunkScheduler<T, R>({\n items,\n processItem: stableProcessItem,\n onItem: stableOnItem,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n ...(collectResults !== undefined ? { collectResults } : {}),\n });\n controller.replace(scheduler);\n if (autoStart) scheduler.start();\n\n return () => {\n // Safe even if the task already finished on its own — cancel() on a\n // terminal scheduler is a documented no-op, not an error.\n scheduler.cancel();\n controller.replace(null);\n };\n // stableProcessItem/stableOnItem are Effect Events: stable by\n // construction, intentionally excluded from the reactive dependency set\n // (that's the whole point of useEffectEvent) rather than omitted by\n // oversight.\n }, [items, timeSliceMs, batchSize, collectResults, autoStart, controller]);\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n return {\n status: snapshot.status,\n processed: snapshot.processed,\n total: snapshot.total,\n error: snapshot.error,\n // From the snapshot, not a direct read of the scheduler: the engine\n // publishes results before it announces a terminal status, so the two\n // always arrive together and a concurrent render can never tear them\n // apart.\n results: snapshot.results,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n };\n}\n","import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createDeferredTask } from '../core/deferred-task.js';\nimport type {\n DeferredGenerator,\n DeferredTask,\n DeferredTaskSnapshot,\n} from '../core/deferred-task.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\nexport interface UseDeferredTaskOptions {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Generator segments to run between clock checks. Default 1. Raise it\n * only for segments measured in nanoseconds; a slice can then run up to\n * `batchSize` segments past its budget.\n */\n readonly batchSize?: number;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n}\n\nexport interface UseDeferredTaskState<T> {\n readonly status: ChunkStatus;\n readonly error: unknown;\n /** The returned value once the task settles with `done` — `undefined` until then. */\n readonly value: T | undefined;\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\nconst IDLE_SNAPSHOT: DeferredTaskSnapshot<never> = { status: 'idle' };\n\n/**\n * Runs an arbitrary heavy, synchronous generator function in chunks, so it\n * never produces a long task on its own. Where `useChunkedTask` chunks \"one\n * function applied to every item of an array\" automatically, this hook\n * leaves the chunk boundaries to you — put `yield` wherever your function\n * can safely pause (see `DeferredGenerator`'s doc comment for an example).\n *\n * `task` is only called when the effect (re)creates the underlying task, not\n * on every render — list whatever it closes over in `deps`, the same way\n * you would for `useMemo` or `useEffect`.\n */\nexport function useDeferredTask<T>(\n task: () => DeferredGenerator<T>,\n deps: readonly unknown[],\n options?: UseDeferredTaskOptions,\n): UseDeferredTaskState<T> {\n const { timeSliceMs, batchSize, autoStart = true } = options ?? {};\n\n // Stable identity, always runs the latest `task` passed this render —\n // see useChunkedTask.ts for why this is useEffectEvent rather than a ref\n // mutated during render.\n const stableTask = useEffectEvent(() => task());\n\n const [controller] = useState(() => createTaskController<DeferredTask<T>>());\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? IDLE_SNAPSHOT,\n () => IDLE_SNAPSHOT,\n );\n\n useEffect(\n () => {\n const deferredTask = createDeferredTask<T>({\n task: stableTask,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n });\n controller.replace(deferredTask);\n if (autoStart) deferredTask.start();\n\n return () => {\n deferredTask.cancel();\n controller.replace(null);\n };\n },\n // `deps` plays the role `items` plays for useChunkedTask: it's the\n // caller's declaration of \"this is new work\". `stableTask` is an Effect\n // Event — stable by construction, intentionally excluded from the\n // reactive dependency set rather than omitted by oversight. `deps` is a\n // caller-supplied array, the same pattern (and the same reason it can't\n // be statically verified) as useMemo/useEffect's own deps parameter.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [...deps, timeSliceMs, batchSize, autoStart, controller],\n );\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n return {\n status: snapshot.status,\n error: snapshot.error,\n // From the snapshot: the engine publishes the value before it\n // announces `done`, so a concurrent render can never see one without\n // the other. See useChunkedTask.ts.\n value: snapshot.value,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/react/task-controller.ts","../src/react/use-effect-event.ts","../src/react/useChunkedTask.ts","../src/react/useDeferredTask.ts"],"sourcesContent":["/**\n * The glue between a task handle (`ChunkScheduler<R>`, `DeferredTask<T>` —\n * anything with `subscribe`/`getSnapshot`) recreated from scratch whenever a\n * hook's deps change, inside an effect, and `useSyncExternalStore`, which\n * wants one stable `subscribe` function for a component's whole lifetime.\n *\n * `useSyncExternalStore` resubscribes whenever the `subscribe` function\n * identity changes — fine for correctness, but it means the naive approach\n * (pass the current task's own `.subscribe` directly) either churns\n * subscriptions every render or, worse, drifts: the component keeps\n * `subscribe`d to whichever task was current when it last resubscribed, not\n * necessarily the one `getSnapshot` is reading from after a swap.\n *\n * A controller sidesteps that: it's created once per hook instance (lazy\n * `useState` initializer in the hook, so it's stable without touching a\n * ref), hands `useSyncExternalStore` a `subscribe`/`getSnapshot` pair that\n * never change identity, and internally forwards to whichever task is\n * \"current\" — re-pointing itself via `replace()` every time the effect\n * swaps in a new one. Generic over the task shape so both `useChunkedTask`\n * and `useDeferredTask` share this one implementation.\n *\n * Every property below is an arrow function, deliberately — not method\n * shorthand — so none of them close over `this`, and `controller.subscribe`\n * can be handed to `useSyncExternalStore` directly as a plain function\n * reference without an unbound-method footgun.\n */\nexport interface SubscribableTask<Snapshot> {\n subscribe(listener: () => void): () => void;\n getSnapshot(): Snapshot;\n}\n\nexport interface TaskController<Task extends SubscribableTask<unknown>> {\n subscribe: (listener: () => void) => () => void;\n getSnapshot: () => ReturnType<Task['getSnapshot']> | undefined;\n getCurrent: () => Task | null;\n /** Swaps in a new task (or none), detaching from the previous one first. */\n replace: (task: Task | null) => void;\n}\n\nexport function createTaskController<\n Task extends SubscribableTask<unknown>,\n>(): TaskController<Task> {\n let current: Task | null = null;\n let detachFromCurrent: (() => void) | undefined;\n const listeners = new Set<() => void>();\n\n const notify = (): void => {\n for (const listener of listeners) listener();\n };\n\n return {\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n getSnapshot: () =>\n current?.getSnapshot() as ReturnType<Task['getSnapshot']> | undefined,\n getCurrent: () => current,\n replace: (task) => {\n detachFromCurrent?.();\n current = task;\n detachFromCurrent = task?.subscribe(notify);\n notify();\n },\n };\n}\n","import * as React from 'react';\nimport { useCallback, useInsertionEffect, useRef } from 'react';\n\n/**\n * `useEffectEvent` only became a stable React export in 19.2. On React 18\n * and 19.0–19.1 it doesn't exist at all — and a named\n * `import { useEffectEvent } from 'react'` then fails outright: at module\n * link time under Node ESM (\"Named export 'useEffectEvent' not found\"), or\n * as \"useEffectEvent is not a function\" on first render in a bundler. Both\n * versions are inside this package's declared peer range, so that has to\n * work, not just type-check.\n *\n * So the native hook is read as a property of the namespace object, where a\n * missing export is plain `undefined` rather than a link error, and used\n * whenever the running React has it. Otherwise the fallback keeps the same\n * contract: a stable function identity that always calls the latest\n * callback. The latest callback is committed from an insertion effect —\n * after render, before any layout or passive effect of the same commit can\n * observe it — so effects always see the current closure, without the\n * render-time ref mutation the React Compiler rejects.\n */\ntype EffectEventHook = <Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n) => (...args: Args) => Return;\n\nexport function useEffectEventFallback<Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n): (...args: Args) => Return {\n const latest = useRef(callback);\n useInsertionEffect(() => {\n latest.current = callback;\n });\n return useCallback((...args: Args) => latest.current(...args), []);\n}\n\n// Through `unknown` on purpose: React 18's types have no `useEffectEvent`\n// member at all, so a direct assertion wouldn't compile against them.\nconst nativeUseEffectEvent = (\n React as unknown as { readonly useEffectEvent?: EffectEventHook }\n).useEffectEvent;\n\nexport const useEffectEvent: EffectEventHook =\n nativeUseEffectEvent ?? useEffectEventFallback;\n","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createChunkScheduler } from '../core/chunk-scheduler.js';\nimport type {\n ChunkScheduler,\n ChunkSnapshot,\n SkippedChunkItem,\n} from '../core/chunk-scheduler.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\n/** Stable empty array so a never-skipped task doesn't hand out a fresh `[]` every render. */\nconst NO_SKIPPED: readonly SkippedChunkItem[] = [];\n\nexport interface UseChunkedTaskOptions<T, R> {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Items to process between clock checks. Default 1. Raise it only for\n * items measured in nanoseconds, where reading the clock costs more than\n * the work; a slice can then run up to `batchSize` items past its budget.\n * See the `batchSize` note on `createChunkScheduler`.\n */\n readonly batchSize?: number;\n /**\n * Keep every result in `results`. Default true. Set false when results\n * are consumed through `onItem`, to avoid retaining one result per item;\n * `results` is then an empty array once the task settles.\n */\n readonly collectResults?: boolean;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n /**\n * Called synchronously after each item is processed. This is the cheap\n * way to consume results incrementally while the task is still running —\n * write into a ref-backed buffer, a canvas, a chart, whatever you need —\n * without paying for an array copy on every yield checkpoint.\n */\n readonly onItem?: (result: R, item: T, index: number) => void;\n /**\n * Called when `processItem` throws. Return `'skip'` to drop that item and\n * keep going — it shows up in `skipped` — or `'abort'` to end the task as\n * an error, carrying whatever was collected before the throw. Omitting\n * this defaults to `'abort'`, so existing callers see no behavior change.\n */\n readonly onError?: (error: unknown, item: T, index: number) => 'skip' | 'abort';\n /**\n * Called every time a slice ends and the task is about to yield, with how\n * long that slice ran and how many items are done so far. Ships in the\n * production build, unlike this package's own dev-only diagnostics — wire\n * it to your own telemetry to verify, on real traffic, that this task is\n * actually staying under `timeSliceMs`.\n */\n readonly onSlice?: (durationMs: number, processed: number) => void;\n /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */\n readonly onYield?: () => void;\n /**\n * External cancellation source — aborting it is equivalent to calling\n * `cancel()`, and its `reason` comes through on `reason` below, so a\n * cancellation caused by the signal is distinguishable from a direct\n * `cancel()` call. A new reference restarts the task the same way a new\n * `items` array does, so memoize it (e.g. from a ref or a parent-owned\n * `AbortController`) unless you mean to restart on every render.\n */\n readonly signal?: AbortSignal;\n}\n\n/** Fields every status shares, independent of the `status`/`results`/`error` narrowing below. */\ninterface UseChunkedTaskShared {\n readonly processed: number;\n readonly total: number;\n /** Items `onError` has skipped so far, growing live as skips happen. Empty when unused. */\n readonly skipped: readonly SkippedChunkItem[];\n /**\n * The `signal` option's `reason`, present only when cancellation came\n * from that signal aborting rather than a direct `cancel()` call.\n */\n readonly reason: unknown;\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\n/**\n * Discriminated on `status`. Narrowing to `'done'` or `'cancelled'` narrows\n * `results` to a defined array, no `=== undefined` check needed alongside\n * `isDone`; narrowing to `'error'` narrows `error` to defined instead. The\n * `is*` booleans above remain the simple way to branch when you only care\n * about one status and don't need the narrowing.\n */\nexport type UseChunkedTaskState<R> = UseChunkedTaskShared &\n (\n | {\n readonly status: Exclude<ChunkStatus, 'done' | 'cancelled' | 'error'>;\n readonly results: undefined;\n readonly error: undefined;\n }\n | {\n readonly status: 'done' | 'cancelled';\n readonly results: readonly R[];\n readonly error: undefined;\n }\n | {\n readonly status: 'error';\n readonly results: readonly R[];\n readonly error: unknown;\n }\n );\n\n// Narrowed to the literal, not widened to `ChunkStatus`, so the object\n// literal below satisfies the discriminated `ChunkSnapshot` union on its\n// own merits — no assertion needed for a status this function knows for sure.\nconst IDLE_STATUS = 'idle';\n\n/**\n * Chunks a heavy, synchronous per-item computation over `items` so it never\n * blocks the main thread for longer than a time slice — the thing that\n * costs you Interaction to Next Paint. Starts automatically by default;\n * pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.\n * behind a button).\n *\n * `processItem` and `onItem` can be fresh closures every render — the hook\n * always calls the latest version (via `useEffectEvent`) without that\n * causing the task to restart. What *does* restart the task is `items`\n * changing identity (a new array reference), or `timeSliceMs`/`autoStart`\n * changing. That's deliberate: a new `items` array means new work, and\n * memoizing it when you don't want that is the caller's call to make, the\n * same as any other React dependency.\n */\nexport function useChunkedTask<T, R>(\n items: readonly T[],\n processItem: (item: T, index: number) => R,\n options?: UseChunkedTaskOptions<T, R>,\n): UseChunkedTaskState<R> {\n const {\n timeSliceMs,\n batchSize,\n collectResults,\n autoStart = true,\n onItem,\n onError,\n onSlice,\n onYield,\n signal,\n } = options ?? {};\n\n // Stable identity, always runs the latest processItem/onItem passed this\n // render — the React-Compiler-safe replacement for a ref mutated during\n // render. Effect Events are meant to be called from inside an effect\n // (here: from the scheduler's async loop, which the effect below owns),\n // never during render itself.\n const stableProcessItem = useEffectEvent((item: T, index: number) =>\n processItem(item, index),\n );\n const stableOnItem = useEffectEvent((result: R, item: T, index: number) => {\n onItem?.(result, item, index);\n });\n const stableOnError = useEffectEvent(\n (error: unknown, item: T, index: number): 'skip' | 'abort' =>\n onError?.(error, item, index) ?? 'abort',\n );\n const stableOnSlice = useEffectEvent((durationMs: number, processed: number) => {\n onSlice?.(durationMs, processed);\n });\n const stableOnYield = useEffectEvent(() => {\n onYield?.();\n });\n\n // Created once, lazily, via useState rather than a ref mutated during\n // render — the controller itself is inert (no subscriptions, no timers)\n // until an effect calls replace() on it, so a discarded render (double\n // render in dev, or one React throws away) never leaks anything.\n const [controller] = useState(() => createTaskController<ChunkScheduler<R>>());\n\n // What getSnapshot() reports before the effect below has ever run —\n // including during SSR, where effects never run at all.\n const idleSnapshot = useMemo<ChunkSnapshot<R>>(\n () => ({ status: IDLE_STATUS, processed: 0, total: items.length }),\n [items.length],\n );\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? idleSnapshot,\n () => idleSnapshot,\n );\n\n useEffect(() => {\n const scheduler = createChunkScheduler<T, R>({\n items,\n processItem: stableProcessItem,\n onItem: stableOnItem,\n onError: stableOnError,\n onSlice: stableOnSlice,\n onYield: stableOnYield,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n ...(collectResults !== undefined ? { collectResults } : {}),\n ...(signal !== undefined ? { signal } : {}),\n });\n controller.replace(scheduler);\n if (autoStart) scheduler.start();\n\n return () => {\n // Safe even if the task already finished on its own — cancel() on a\n // terminal scheduler is a documented no-op, not an error.\n scheduler.cancel();\n controller.replace(null);\n };\n // stableProcessItem/stableOnItem/stableOnError/stableOnSlice/stableOnYield\n // are Effect Events: stable by construction, intentionally excluded\n // from the reactive dependency set\n // (that's the whole point of useEffectEvent) rather than omitted by\n // oversight.\n }, [items, timeSliceMs, batchSize, collectResults, autoStart, signal, controller]);\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n // Built from `snapshot`'s own wide-`status` fields, so — same as the core\n // engine this wraps — it can't be checked structurally against the\n // discriminated union above; `snapshot` already carries that invariant.\n return {\n status: snapshot.status,\n processed: snapshot.processed,\n total: snapshot.total,\n error: snapshot.error,\n // From the snapshot, not a direct read of the scheduler: the engine\n // publishes results before it announces a terminal status, so the two\n // always arrive together and a concurrent render can never tear them\n // apart.\n results: snapshot.results,\n skipped: snapshot.skipped ?? NO_SKIPPED,\n reason: snapshot.reason,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n } as UseChunkedTaskState<R>;\n}\n","import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createDeferredTask } from '../core/deferred-task.js';\nimport type {\n DeferredGenerator,\n DeferredTask,\n DeferredTaskSnapshot,\n} from '../core/deferred-task.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\nexport interface UseDeferredTaskOptions {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Generator segments to run between clock checks. Default 1. Raise it\n * only for segments measured in nanoseconds; a slice can then run up to\n * `batchSize` segments past its budget.\n */\n readonly batchSize?: number;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n /**\n * Called every time a segment-batch ends and the task is about to yield,\n * with how long that slice ran and how many segments are done so far.\n * Ships in the production build — see the identical option on\n * `useChunkedTask`/`createChunkScheduler` for why.\n */\n readonly onSlice?: (durationMs: number, segmentsDone: number) => void;\n /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */\n readonly onYield?: () => void;\n /**\n * External cancellation source — aborting it is equivalent to calling\n * `cancel()`, and its `reason` comes through on `reason` below, so a\n * cancellation caused by the signal is distinguishable from a direct\n * `cancel()` call. A new reference restarts the task the same way a new\n * entry in `deps` does, so memoize it unless you mean to restart on\n * every render.\n */\n readonly signal?: AbortSignal;\n}\n\n/** Fields every status shares, independent of the `status`/`value`/`error` narrowing below. */\ninterface UseDeferredTaskShared {\n /**\n * The `signal` option's `reason`, present only when cancellation came\n * from that signal aborting rather than a direct `cancel()` call.\n */\n readonly reason: unknown;\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\n/**\n * Discriminated on `status`. Narrowing to `'done'` narrows `value` to `T`\n * itself — no `=== undefined` check needed alongside `isDone`, even when\n * `T` allows `undefined` as a genuine return value. Narrowing to `'error'`\n * narrows `error` to defined instead.\n */\nexport type UseDeferredTaskState<T> = UseDeferredTaskShared &\n (\n | {\n readonly status: Exclude<ChunkStatus, 'done' | 'error'>;\n readonly value: undefined;\n readonly error: undefined;\n }\n | { readonly status: 'done'; readonly value: T; readonly error: undefined }\n | { readonly status: 'error'; readonly value: undefined; readonly error: unknown }\n );\n\n// Narrowed to the literal, not widened to `ChunkStatus`, so this satisfies\n// the discriminated `DeferredTaskSnapshot` union on its own merits.\nconst IDLE_SNAPSHOT: DeferredTaskSnapshot<never> = { status: 'idle' };\n\n/**\n * Runs an arbitrary heavy, synchronous generator function in chunks, so it\n * never produces a long task on its own. Where `useChunkedTask` chunks \"one\n * function applied to every item of an array\" automatically, this hook\n * leaves the chunk boundaries to you — put `yield` wherever your function\n * can safely pause (see `DeferredGenerator`'s doc comment for an example).\n *\n * `task` is only called when the effect (re)creates the underlying task, not\n * on every render — list whatever it closes over in `deps`, the same way\n * you would for `useMemo` or `useEffect`.\n */\nexport function useDeferredTask<T>(\n task: () => DeferredGenerator<T>,\n deps: readonly unknown[],\n options?: UseDeferredTaskOptions,\n): UseDeferredTaskState<T> {\n const {\n timeSliceMs,\n batchSize,\n autoStart = true,\n onSlice,\n onYield,\n signal,\n } = options ?? {};\n\n // Stable identity, always runs the latest `task` passed this render —\n // see useChunkedTask.ts for why this is useEffectEvent rather than a ref\n // mutated during render.\n const stableTask = useEffectEvent(() => task());\n const stableOnSlice = useEffectEvent((durationMs: number, segmentsDone: number) => {\n onSlice?.(durationMs, segmentsDone);\n });\n const stableOnYield = useEffectEvent(() => {\n onYield?.();\n });\n\n const [controller] = useState(() => createTaskController<DeferredTask<T>>());\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? IDLE_SNAPSHOT,\n () => IDLE_SNAPSHOT,\n );\n\n useEffect(\n () => {\n const deferredTask = createDeferredTask<T>({\n task: stableTask,\n onSlice: stableOnSlice,\n onYield: stableOnYield,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n ...(signal !== undefined ? { signal } : {}),\n });\n controller.replace(deferredTask);\n if (autoStart) deferredTask.start();\n\n return () => {\n deferredTask.cancel();\n controller.replace(null);\n };\n },\n // `deps` plays the role `items` plays for useChunkedTask: it's the\n // caller's declaration of \"this is new work\". `stableTask` is an Effect\n // Event — stable by construction, intentionally excluded from the\n // reactive dependency set rather than omitted by oversight. `deps` is a\n // caller-supplied array, the same pattern (and the same reason it can't\n // be statically verified) as useMemo/useEffect's own deps parameter.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [...deps, timeSliceMs, batchSize, autoStart, signal, controller],\n );\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n // Built from `snapshot`'s own wide-`status` fields — see the identical\n // note in useChunkedTask.ts; `snapshot` already carries the invariant.\n return {\n status: snapshot.status,\n error: snapshot.error,\n // From the snapshot: the engine publishes the value before it\n // announces `done`, so a concurrent render can never see one without\n // the other. See useChunkedTask.ts.\n value: snapshot.value,\n reason: snapshot.reason,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n } as UseDeferredTaskState<T>;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_deferred_task = require("../deferred-task-zVnq18IN.cjs");
2
+ const require_deferred_task = require("../deferred-task-j8KF7lk7.cjs");
3
3
  exports.DEFAULT_TIME_SLICE_MS = require_deferred_task.DEFAULT_TIME_SLICE_MS;
4
4
  exports.createChunkScheduler = require_deferred_task.createChunkScheduler;
5
5
  exports.createDeferredTask = require_deferred_task.createDeferredTask;
@@ -1 +1 @@
1
- export { r as DEFAULT_TIME_SLICE_MS, n as createChunkScheduler, t as createDeferredTask, f as initialChunkState, i as isInputPending, p as isTerminal, a as now, o as resolveYieldStrategy, s as shouldYieldNow, c as supportsInputPending, l as supportsMessageChannel, u as supportsSchedulerYield, m as transition, d as yieldToMain } from "../deferred-task-BGsfee-L.js";
1
+ export { r as DEFAULT_TIME_SLICE_MS, n as createChunkScheduler, t as createDeferredTask, f as initialChunkState, i as isInputPending, p as isTerminal, a as now, o as resolveYieldStrategy, s as shouldYieldNow, c as supportsInputPending, l as supportsMessageChannel, u as supportsSchedulerYield, m as transition, d as yieldToMain } from "../deferred-task-MsTNINAd.js";
@@ -205,7 +205,7 @@ function at(items, index) {
205
205
  * scheduler, which is the idiomatic React data-flow anyway.
206
206
  */
207
207
  function createChunkScheduler(options) {
208
- const { items, processItem, onItem, signal, yieldStrategy } = options;
208
+ const { items, processItem, onItem, onError, signal, yieldStrategy, onSlice, onYield } = options;
209
209
  const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
210
210
  const batchSize = normalizeBatchSize(options.batchSize);
211
211
  const collectResults = options.collectResults ?? true;
@@ -217,6 +217,8 @@ function createChunkScheduler(options) {
217
217
  let settledFlag = false;
218
218
  const results = [];
219
219
  let finalResults;
220
+ const skipped = [];
221
+ let cancelReason;
220
222
  let snapshot = {
221
223
  status: machineState.status,
222
224
  processed,
@@ -252,7 +254,9 @@ function createChunkScheduler(options) {
252
254
  processed,
253
255
  total,
254
256
  ...machineState.error !== void 0 ? { error: machineState.error } : {},
255
- ...finalResults !== void 0 ? { results: finalResults } : {}
257
+ ...finalResults !== void 0 ? { results: finalResults } : {},
258
+ ...skipped.length > 0 ? { skipped: skipped.slice() } : {},
259
+ ...cancelReason !== void 0 ? { reason: cancelReason } : {}
256
260
  };
257
261
  }
258
262
  return snapshot;
@@ -287,13 +291,12 @@ function createChunkScheduler(options) {
287
291
  return finalResults;
288
292
  }
289
293
  function settleWith(status, published, error) {
290
- settle(status === "error" ? {
294
+ settle({
291
295
  status,
292
- error,
293
- results: published
294
- } : {
295
- status,
296
- results: published
296
+ results: published,
297
+ ...error !== void 0 ? { error } : {},
298
+ ...skipped.length > 0 ? { skipped: skipped.slice() } : {},
299
+ ...status === "cancelled" && cancelReason !== void 0 ? { reason: cancelReason } : {}
297
300
  });
298
301
  }
299
302
  /**
@@ -313,13 +316,16 @@ function createChunkScheduler(options) {
313
316
  finishCancelled();
314
317
  }
315
318
  function handleAbort() {
319
+ cancelReason = signal?.reason;
316
320
  cancel();
317
321
  }
318
322
  function detachSignal() {
319
323
  signal?.removeEventListener("abort", handleAbort);
320
324
  }
321
- if (signal?.aborted) dispatch({ type: "CANCEL" });
322
- else signal?.addEventListener("abort", handleAbort);
325
+ if (signal?.aborted) {
326
+ cancelReason = signal.reason;
327
+ dispatch({ type: "CANCEL" });
328
+ } else signal?.addEventListener("abort", handleAbort);
323
329
  async function runLoop() {
324
330
  let sliceStartedAt = now();
325
331
  let index = 0;
@@ -335,17 +341,33 @@ function createChunkScheduler(options) {
335
341
  const batchEnd = index + batchSize <= total ? index + batchSize : total;
336
342
  while (index < batchEnd) {
337
343
  const item = at(items, index);
338
- const result = processItem(item, index);
344
+ let result;
345
+ try {
346
+ result = processItem(item, index);
347
+ } catch (caught) {
348
+ if ((onError?.(caught, item, index) ?? "abort") !== "skip") throw caught;
349
+ skipped.push({
350
+ index,
351
+ error: caught
352
+ });
353
+ processed = index + 1;
354
+ snapshotStale = true;
355
+ index += 1;
356
+ continue;
357
+ }
339
358
  if (collectResults) results.push(result);
340
359
  processed = index + 1;
341
360
  onItem?.(result, item, index);
342
361
  index += 1;
343
362
  }
344
363
  snapshotStale = true;
345
- if (now() - sliceStartedAt >= timeSliceMs || inputPending()) {
364
+ const checkedAt = now();
365
+ if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
366
+ onSlice?.(checkedAt - sliceStartedAt, index);
346
367
  await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
347
368
  notify();
348
369
  sliceStartedAt = now();
370
+ onYield?.();
349
371
  }
350
372
  }
351
373
  } catch (error) {
@@ -410,7 +432,7 @@ function createChunkScheduler(options) {
410
432
  * here unchanged.
411
433
  */
412
434
  function createDeferredTask(options) {
413
- const { task, signal, yieldStrategy } = options;
435
+ const { task, signal, yieldStrategy, onSlice, onYield } = options;
414
436
  const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
415
437
  const batchSize = normalizeBatchSize(options.batchSize);
416
438
  const inputPending = resolveInputPending();
@@ -418,6 +440,8 @@ function createDeferredTask(options) {
418
440
  let loopStarted = false;
419
441
  let settledFlag = false;
420
442
  let settledValue;
443
+ let hasSettledValue = false;
444
+ let cancelReason;
421
445
  let snapshot = { status: machineState.status };
422
446
  let snapshotStale = false;
423
447
  const listeners = /* @__PURE__ */ new Set();
@@ -447,7 +471,8 @@ function createDeferredTask(options) {
447
471
  snapshot = {
448
472
  status: machineState.status,
449
473
  ...machineState.error !== void 0 ? { error: machineState.error } : {},
450
- ...settledValue !== void 0 ? { value: settledValue } : {}
474
+ ...hasSettledValue ? { value: settledValue } : {},
475
+ ...cancelReason !== void 0 ? { reason: cancelReason } : {}
451
476
  };
452
477
  }
453
478
  return snapshot;
@@ -475,20 +500,28 @@ function createDeferredTask(options) {
475
500
  */
476
501
  function publishValue(resolvedValue) {
477
502
  settledValue = resolvedValue;
503
+ hasSettledValue = true;
478
504
  snapshotStale = true;
479
505
  }
480
506
  function settleWith(status, resolvedValue, error) {
481
507
  if (status === "done") {
482
508
  settle({
483
509
  status,
484
- ...resolvedValue !== void 0 ? { value: resolvedValue } : {}
510
+ value: resolvedValue
485
511
  });
486
512
  return;
487
513
  }
488
- settle(status === "error" ? {
514
+ if (status === "error") {
515
+ settle({
516
+ status,
517
+ error
518
+ });
519
+ return;
520
+ }
521
+ settle({
489
522
  status,
490
- error
491
- } : { status });
523
+ ...cancelReason !== void 0 ? { reason: cancelReason } : {}
524
+ });
492
525
  }
493
526
  /**
494
527
  * Settling as cancelled, for both places that can do it. The CANCEL
@@ -506,13 +539,16 @@ function createDeferredTask(options) {
506
539
  finishCancelled();
507
540
  }
508
541
  function handleAbort() {
542
+ cancelReason = signal?.reason;
509
543
  cancel();
510
544
  }
511
545
  function detachSignal() {
512
546
  signal?.removeEventListener("abort", handleAbort);
513
547
  }
514
- if (signal?.aborted) dispatch({ type: "CANCEL" });
515
- else signal?.addEventListener("abort", handleAbort);
548
+ if (signal?.aborted) {
549
+ cancelReason = signal.reason;
550
+ dispatch({ type: "CANCEL" });
551
+ } else signal?.addEventListener("abort", handleAbort);
516
552
  async function runLoop() {
517
553
  let sliceStartedAt = now();
518
554
  let segment = 0;
@@ -543,10 +579,13 @@ function createDeferredTask(options) {
543
579
  }
544
580
  }
545
581
  if (done) break;
546
- if (now() - sliceStartedAt >= timeSliceMs || inputPending()) {
582
+ const checkedAt = now();
583
+ if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
584
+ onSlice?.(checkedAt - sliceStartedAt, segment);
547
585
  await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
548
586
  notify();
549
587
  sliceStartedAt = now();
588
+ onYield?.();
550
589
  }
551
590
  }
552
591
  } catch (error) {
@@ -605,4 +644,4 @@ function createDeferredTask(options) {
605
644
  }
606
645
  export { now as a, supportsInputPending as c, yieldToMain as d, initialChunkState as f, isInputPending as i, supportsMessageChannel as l, transition as m, createChunkScheduler as n, resolveYieldStrategy as o, isTerminal as p, DEFAULT_TIME_SLICE_MS as r, shouldYieldNow as s, createDeferredTask as t, supportsSchedulerYield as u };
607
646
 
608
- //# sourceMappingURL=deferred-task-BGsfee-L.js.map
647
+ //# sourceMappingURL=deferred-task-MsTNINAd.js.map