react-inp-chunker 0.1.2 → 0.3.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 (37) hide show
  1. package/README.md +165 -53
  2. package/core/package.json +10 -0
  3. package/dist/core/index.cjs +1 -1
  4. package/dist/core/index.d.cts +2 -2
  5. package/dist/core/index.d.ts +2 -2
  6. package/dist/core/index.js +1 -1
  7. package/dist/deferred-task-6oTEnHWa.js +778 -0
  8. package/dist/deferred-task-6oTEnHWa.js.map +1 -0
  9. package/dist/deferred-task-DcnC99_Y.cjs +861 -0
  10. package/dist/deferred-task-DcnC99_Y.cjs.map +1 -0
  11. package/dist/{index-C6B_Nm3i.d.ts → index-B4gKjLYR.d.cts} +117 -6
  12. package/dist/index-B4gKjLYR.d.cts.map +1 -0
  13. package/dist/{index-C6B_Nm3i.d.cts → index-B4gKjLYR.d.ts} +117 -6
  14. package/dist/index-B4gKjLYR.d.ts.map +1 -0
  15. package/dist/index.cjs +68 -12
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +49 -2
  18. package/dist/index.d.cts.map +1 -1
  19. package/dist/index.d.ts +49 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +45 -10
  22. package/dist/index.js.map +1 -1
  23. package/dist/production/core/index.cjs +16 -0
  24. package/dist/production/core/index.js +1 -0
  25. package/dist/{deferred-task-5kfztF2G.js → production/deferred-task-DkHdsliy.js} +227 -99
  26. package/dist/production/deferred-task-DkHdsliy.js.map +1 -0
  27. package/dist/{deferred-task-BXVzapQg.cjs → production/deferred-task-yh5pWlSj.cjs} +227 -99
  28. package/dist/production/deferred-task-yh5pWlSj.cjs.map +1 -0
  29. package/dist/production/index.cjs +216 -0
  30. package/dist/production/index.cjs.map +1 -0
  31. package/dist/production/index.js +193 -0
  32. package/dist/production/index.js.map +1 -0
  33. package/package.json +69 -23
  34. package/dist/deferred-task-5kfztF2G.js.map +0 -1
  35. package/dist/deferred-task-BXVzapQg.cjs.map +0 -1
  36. package/dist/index-C6B_Nm3i.d.cts.map +0 -1
  37. package/dist/index-C6B_Nm3i.d.ts.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,6 +1,28 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_deferred_task = require("./deferred-task-BXVzapQg.cjs");
2
+ // \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ const require_deferred_task = require("./deferred-task-DcnC99_Y.cjs");
3
24
  let react = require("react");
25
+ react = __toESM(react, 1);
4
26
  // src/react/task-controller.ts
5
27
  function createTaskController() {
6
28
  let current = null;
@@ -26,7 +48,18 @@ function createTaskController() {
26
48
  }
27
49
  };
28
50
  }
51
+ // src/react/use-effect-event.ts
52
+ function useEffectEventFallback(callback) {
53
+ const latest = (0, react.useRef)(callback);
54
+ (0, react.useInsertionEffect)(() => {
55
+ latest.current = callback;
56
+ });
57
+ return (0, react.useCallback)((...args) => latest.current(...args), []);
58
+ }
59
+ const useEffectEvent = react.useEffectEvent ?? useEffectEventFallback;
29
60
  // src/react/useChunkedTask.ts
61
+ /** Stable empty array so a never-skipped task doesn't hand out a fresh `[]` every render. */
62
+ const NO_SKIPPED = [];
30
63
  const IDLE_STATUS = "idle";
31
64
  /**
32
65
  * Chunks a heavy, synchronous per-item computation over `items` so it never
@@ -44,11 +77,18 @@ const IDLE_STATUS = "idle";
44
77
  * same as any other React dependency.
45
78
  */
46
79
  function useChunkedTask(items, processItem, options) {
47
- const { timeSliceMs, autoStart = true, onItem } = options ?? {};
48
- const stableProcessItem = (0, react.useEffectEvent)((item, index) => processItem(item, index));
49
- const stableOnItem = (0, react.useEffectEvent)((result, item, index) => {
80
+ const { timeSliceMs, batchSize, collectResults, autoStart = true, onItem, onError, onSlice, onYield } = options ?? {};
81
+ const stableProcessItem = useEffectEvent((item, index) => processItem(item, index));
82
+ const stableOnItem = useEffectEvent((result, item, index) => {
50
83
  onItem?.(result, item, index);
51
84
  });
85
+ const stableOnError = useEffectEvent((error, item, index) => onError?.(error, item, index) ?? "abort");
86
+ const stableOnSlice = useEffectEvent((durationMs, processed) => {
87
+ onSlice?.(durationMs, processed);
88
+ });
89
+ const stableOnYield = useEffectEvent(() => {
90
+ onYield?.();
91
+ });
52
92
  const [controller] = (0, react.useState)(() => createTaskController());
53
93
  const idleSnapshot = (0, react.useMemo)(() => ({
54
94
  status: IDLE_STATUS,
@@ -61,7 +101,12 @@ function useChunkedTask(items, processItem, options) {
61
101
  items,
62
102
  processItem: stableProcessItem,
63
103
  onItem: stableOnItem,
64
- ...timeSliceMs !== void 0 ? { timeSliceMs } : {}
104
+ onError: stableOnError,
105
+ onSlice: stableOnSlice,
106
+ onYield: stableOnYield,
107
+ ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
108
+ ...batchSize !== void 0 ? { batchSize } : {},
109
+ ...collectResults !== void 0 ? { collectResults } : {}
65
110
  });
66
111
  controller.replace(scheduler);
67
112
  if (autoStart) scheduler.start();
@@ -72,6 +117,8 @@ function useChunkedTask(items, processItem, options) {
72
117
  }, [
73
118
  items,
74
119
  timeSliceMs,
120
+ batchSize,
121
+ collectResults,
75
122
  autoStart,
76
123
  controller
77
124
  ]);
@@ -79,13 +126,13 @@ function useChunkedTask(items, processItem, options) {
79
126
  const pause = (0, react.useCallback)(() => controller.getCurrent()?.pause(), [controller]);
80
127
  const resume = (0, react.useCallback)(() => controller.getCurrent()?.resume(), [controller]);
81
128
  const cancel = (0, react.useCallback)(() => controller.getCurrent()?.cancel(), [controller]);
82
- const results = controller.getCurrent()?.getResults();
83
129
  return {
84
130
  status: snapshot.status,
85
131
  processed: snapshot.processed,
86
132
  total: snapshot.total,
87
133
  error: snapshot.error,
88
- results,
134
+ results: snapshot.results,
135
+ skipped: snapshot.skipped ?? NO_SKIPPED,
89
136
  isIdle: snapshot.status === "idle",
90
137
  isProcessing: snapshot.status === "processing",
91
138
  isPaused: snapshot.status === "paused",
@@ -112,14 +159,23 @@ const IDLE_SNAPSHOT = { status: "idle" };
112
159
  * you would for `useMemo` or `useEffect`.
113
160
  */
114
161
  function useDeferredTask(task, deps, options) {
115
- const { timeSliceMs, autoStart = true } = options ?? {};
116
- const stableTask = (0, react.useEffectEvent)(() => task());
162
+ const { timeSliceMs, batchSize, autoStart = true, onSlice, onYield } = options ?? {};
163
+ const stableTask = useEffectEvent(() => task());
164
+ const stableOnSlice = useEffectEvent((durationMs, segmentsDone) => {
165
+ onSlice?.(durationMs, segmentsDone);
166
+ });
167
+ const stableOnYield = useEffectEvent(() => {
168
+ onYield?.();
169
+ });
117
170
  const [controller] = (0, react.useState)(() => createTaskController());
118
171
  const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, () => controller.getSnapshot() ?? IDLE_SNAPSHOT, () => IDLE_SNAPSHOT);
119
172
  (0, react.useEffect)(() => {
120
173
  const deferredTask = require_deferred_task.createDeferredTask({
121
174
  task: stableTask,
122
- ...timeSliceMs !== void 0 ? { timeSliceMs } : {}
175
+ onSlice: stableOnSlice,
176
+ onYield: stableOnYield,
177
+ ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
178
+ ...batchSize !== void 0 ? { batchSize } : {}
123
179
  });
124
180
  controller.replace(deferredTask);
125
181
  if (autoStart) deferredTask.start();
@@ -130,6 +186,7 @@ function useDeferredTask(task, deps, options) {
130
186
  }, [
131
187
  ...deps,
132
188
  timeSliceMs,
189
+ batchSize,
133
190
  autoStart,
134
191
  controller
135
192
  ]);
@@ -137,11 +194,10 @@ function useDeferredTask(task, deps, options) {
137
194
  const pause = (0, react.useCallback)(() => controller.getCurrent()?.pause(), [controller]);
138
195
  const resume = (0, react.useCallback)(() => controller.getCurrent()?.resume(), [controller]);
139
196
  const cancel = (0, react.useCallback)(() => controller.getCurrent()?.cancel(), [controller]);
140
- const value = controller.getCurrent()?.getValue();
141
197
  return {
142
198
  status: snapshot.status,
143
199
  error: snapshot.error,
144
- value,
200
+ value: snapshot.value,
145
201
  isIdle: snapshot.status === "idle",
146
202
  isProcessing: snapshot.status === "processing",
147
203
  isPaused: snapshot.status === "paused",
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["useEffectEvent","useState","useMemo","useSyncExternalStore","createChunkScheduler","useCallback","useEffectEvent","useState","useSyncExternalStore","createDeferredTask","useCallback"],"sources":["../src/react/task-controller.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 {\n useCallback,\n useEffect,\n useEffectEvent,\n useMemo,\n useState,\n useSyncExternalStore,\n} 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';\n\nexport interface UseChunkedTaskOptions<T, R> {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\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 { timeSliceMs, autoStart = true, onItem } = 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>(\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 });\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, 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 // Safe to read outside useSyncExternalStore's own getSnapshot: a\n // scheduler only ever finishes synchronously (dispatch-then-finish, no\n // await between them — see chunk-scheduler.ts), so by the time React\n // re-renders in response to the status-change notification, getResults()\n // already agrees with that status. They can never be observed out of sync.\n const results = controller.getCurrent()?.getResults();\n\n return {\n status: snapshot.status,\n processed: snapshot.processed,\n total: snapshot.total,\n error: snapshot.error,\n 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 {\n useCallback,\n useEffect,\n useEffectEvent,\n useState,\n useSyncExternalStore,\n} 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';\n\nexport interface UseDeferredTaskOptions {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: 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 = { 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, 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 });\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, 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 // See useChunkedTask.ts for why reading this outside useSyncExternalStore\n // is safe: status and value are always settled in the same synchronous\n // turn, with no await between them.\n const value = controller.getCurrent()?.getValue();\n\n return {\n status: snapshot.status,\n error: snapshot.error,\n 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.cjs","names":["useRef","useCallback","React","useState","useMemo","useSyncExternalStore","createChunkScheduler","useCallback","useState","useSyncExternalStore","createDeferredTask","useCallback"],"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\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 /** Items `onError` has skipped so far, growing live as skips happen. Empty when unused. */\n readonly skipped: readonly SkippedChunkItem[];\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 onError,\n onSlice,\n onYield,\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 });\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, 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 skipped: snapshot.skipped ?? NO_SKIPPED,\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 * 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\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, onSlice, onYield } = 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 });\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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"}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,20 @@
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-C6B_Nm3i.cjs";
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-B4gKjLYR.cjs";
2
2
  interface UseChunkedTaskOptions<T, R> {
3
3
  /** Max ms of uninterrupted work before yielding. Default 5. */
4
4
  readonly timeSliceMs?: number;
5
+ /**
6
+ * Items to process between clock checks. Default 1. Raise it only for
7
+ * items measured in nanoseconds, where reading the clock costs more than
8
+ * the work; a slice can then run up to `batchSize` items past its budget.
9
+ * See the `batchSize` note on `createChunkScheduler`.
10
+ */
11
+ readonly batchSize?: number;
12
+ /**
13
+ * Keep every result in `results`. Default true. Set false when results
14
+ * are consumed through `onItem`, to avoid retaining one result per item;
15
+ * `results` is then an empty array once the task settles.
16
+ */
17
+ readonly collectResults?: boolean;
5
18
  /** Start processing automatically when the task is (re)created. Default `true`. */
6
19
  readonly autoStart?: boolean;
7
20
  /**
@@ -11,6 +24,23 @@ interface UseChunkedTaskOptions<T, R> {
11
24
  * without paying for an array copy on every yield checkpoint.
12
25
  */
13
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;
14
44
  }
15
45
  interface UseChunkedTaskState<R> {
16
46
  readonly status: ChunkStatus;
@@ -19,6 +49,8 @@ interface UseChunkedTaskState<R> {
19
49
  readonly error: unknown;
20
50
  /** The full results array once the task has settled — `undefined` until then. */
21
51
  readonly results: readonly R[] | undefined;
52
+ /** Items `onError` has skipped so far, growing live as skips happen. Empty when unused. */
53
+ readonly skipped: readonly SkippedChunkItem[];
22
54
  readonly isIdle: boolean;
23
55
  readonly isProcessing: boolean;
24
56
  readonly isPaused: boolean;
@@ -49,8 +81,23 @@ export declare function useChunkedTask<T, R>(items: readonly T[], processItem: (
49
81
  interface UseDeferredTaskOptions {
50
82
  /** Max ms of uninterrupted work before yielding. Default 5. */
51
83
  readonly timeSliceMs?: number;
84
+ /**
85
+ * Generator segments to run between clock checks. Default 1. Raise it
86
+ * only for segments measured in nanoseconds; a slice can then run up to
87
+ * `batchSize` segments past its budget.
88
+ */
89
+ readonly batchSize?: number;
52
90
  /** Start processing automatically when the task is (re)created. Default `true`. */
53
91
  readonly autoStart?: boolean;
92
+ /**
93
+ * Called every time a segment-batch ends and the task is about to yield,
94
+ * with how long that slice ran and how many segments are done so far.
95
+ * Ships in the production build — see the identical option on
96
+ * `useChunkedTask`/`createChunkScheduler` for why.
97
+ */
98
+ readonly onSlice?: (durationMs: number, segmentsDone: number) => void;
99
+ /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */
100
+ readonly onYield?: () => void;
54
101
  }
55
102
  interface UseDeferredTaskState<T> {
56
103
  readonly status: ChunkStatus;
@@ -80,5 +127,5 @@ interface UseDeferredTaskState<T> {
80
127
  * you would for `useMemo` or `useEffect`.
81
128
  */
82
129
  export declare function useDeferredTask<T>(task: () => DeferredGenerator<T>, deps: readonly unknown[], options?: UseDeferredTaskOptions): UseDeferredTaskState<T>;
83
- export type { ChunkEvent, ChunkMachineState, ChunkScheduler, ChunkSchedulerOptions, ChunkSchedulerResult, ChunkSnapshot, ChunkStatus, DeferredGenerator, DeferredTask, DeferredTaskOptions, DeferredTaskResult, DeferredTaskSnapshot, UseChunkedTaskOptions, UseChunkedTaskState, UseDeferredTaskOptions, UseDeferredTaskState, YieldStrategy };
130
+ export type { ChunkEvent, ChunkMachineState, ChunkScheduler, ChunkSchedulerOptions, ChunkSchedulerResult, ChunkSnapshot, ChunkStatus, DeferredGenerator, DeferredTask, DeferredTaskOptions, DeferredTaskResult, DeferredTaskSnapshot, SkippedChunkItem, UseChunkedTaskOptions, UseChunkedTaskState, UseDeferredTaskOptions, UseDeferredTaskState, YieldStrategy };
84
131
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/react/useChunkedTask.ts","../src/react/useDeferredTask.ts"],"mappings":";UAciB,sBAAsB,GAAG;;WAE/B;;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;UCnDN;;WAEN;;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.cts","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;;UAGM,oBAAoB;WAC1B,QAAQ;WACR;WACA;WACA;;WAEA,kBAAkB;;WAElB,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;UCzFN;;WAEN;;;;;;WAMA;;WAEA;;;;;;;WAOA,WAAW,oBAAoB;;WAE/B;;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"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,20 @@
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-C6B_Nm3i.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-B4gKjLYR.js";
2
2
  interface UseChunkedTaskOptions<T, R> {
3
3
  /** Max ms of uninterrupted work before yielding. Default 5. */
4
4
  readonly timeSliceMs?: number;
5
+ /**
6
+ * Items to process between clock checks. Default 1. Raise it only for
7
+ * items measured in nanoseconds, where reading the clock costs more than
8
+ * the work; a slice can then run up to `batchSize` items past its budget.
9
+ * See the `batchSize` note on `createChunkScheduler`.
10
+ */
11
+ readonly batchSize?: number;
12
+ /**
13
+ * Keep every result in `results`. Default true. Set false when results
14
+ * are consumed through `onItem`, to avoid retaining one result per item;
15
+ * `results` is then an empty array once the task settles.
16
+ */
17
+ readonly collectResults?: boolean;
5
18
  /** Start processing automatically when the task is (re)created. Default `true`. */
6
19
  readonly autoStart?: boolean;
7
20
  /**
@@ -11,6 +24,23 @@ interface UseChunkedTaskOptions<T, R> {
11
24
  * without paying for an array copy on every yield checkpoint.
12
25
  */
13
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;
14
44
  }
15
45
  interface UseChunkedTaskState<R> {
16
46
  readonly status: ChunkStatus;
@@ -19,6 +49,8 @@ interface UseChunkedTaskState<R> {
19
49
  readonly error: unknown;
20
50
  /** The full results array once the task has settled — `undefined` until then. */
21
51
  readonly results: readonly R[] | undefined;
52
+ /** Items `onError` has skipped so far, growing live as skips happen. Empty when unused. */
53
+ readonly skipped: readonly SkippedChunkItem[];
22
54
  readonly isIdle: boolean;
23
55
  readonly isProcessing: boolean;
24
56
  readonly isPaused: boolean;
@@ -49,8 +81,23 @@ export declare function useChunkedTask<T, R>(items: readonly T[], processItem: (
49
81
  interface UseDeferredTaskOptions {
50
82
  /** Max ms of uninterrupted work before yielding. Default 5. */
51
83
  readonly timeSliceMs?: number;
84
+ /**
85
+ * Generator segments to run between clock checks. Default 1. Raise it
86
+ * only for segments measured in nanoseconds; a slice can then run up to
87
+ * `batchSize` segments past its budget.
88
+ */
89
+ readonly batchSize?: number;
52
90
  /** Start processing automatically when the task is (re)created. Default `true`. */
53
91
  readonly autoStart?: boolean;
92
+ /**
93
+ * Called every time a segment-batch ends and the task is about to yield,
94
+ * with how long that slice ran and how many segments are done so far.
95
+ * Ships in the production build — see the identical option on
96
+ * `useChunkedTask`/`createChunkScheduler` for why.
97
+ */
98
+ readonly onSlice?: (durationMs: number, segmentsDone: number) => void;
99
+ /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */
100
+ readonly onYield?: () => void;
54
101
  }
55
102
  interface UseDeferredTaskState<T> {
56
103
  readonly status: ChunkStatus;
@@ -80,5 +127,5 @@ interface UseDeferredTaskState<T> {
80
127
  * you would for `useMemo` or `useEffect`.
81
128
  */
82
129
  export declare function useDeferredTask<T>(task: () => DeferredGenerator<T>, deps: readonly unknown[], options?: UseDeferredTaskOptions): UseDeferredTaskState<T>;
83
- export type { ChunkEvent, ChunkMachineState, ChunkScheduler, ChunkSchedulerOptions, ChunkSchedulerResult, ChunkSnapshot, ChunkStatus, DeferredGenerator, DeferredTask, DeferredTaskOptions, DeferredTaskResult, DeferredTaskSnapshot, UseChunkedTaskOptions, UseChunkedTaskState, UseDeferredTaskOptions, UseDeferredTaskState, YieldStrategy };
130
+ export type { ChunkEvent, ChunkMachineState, ChunkScheduler, ChunkSchedulerOptions, ChunkSchedulerResult, ChunkSnapshot, ChunkStatus, DeferredGenerator, DeferredTask, DeferredTaskOptions, DeferredTaskResult, DeferredTaskSnapshot, SkippedChunkItem, UseChunkedTaskOptions, UseChunkedTaskState, UseDeferredTaskOptions, UseDeferredTaskState, YieldStrategy };
84
131
  //# 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":";UAciB,sBAAsB,GAAG;;WAE/B;;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;UCnDN;;WAEN;;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;;UAGM,oBAAoB;WAC1B,QAAQ;WACR;WACA;WACA;;WAEA,kBAAkB;;WAElB,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;UCzFN;;WAEN;;;;;;WAMA;;WAEA;;;;;;;WAOA,WAAW,oBAAoB;;WAE/B;;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"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { n as createChunkScheduler, t as createDeferredTask } from "./deferred-task-5kfztF2G.js";
2
- import { useCallback, useEffect, useEffectEvent, useMemo, useState, useSyncExternalStore } from "react";
1
+ import { n as createChunkScheduler, t as createDeferredTask } from "./deferred-task-6oTEnHWa.js";
2
+ import * as React from "react";
3
+ import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
4
  // src/react/task-controller.ts
4
5
  function createTaskController() {
5
6
  let current = null;
@@ -25,7 +26,18 @@ function createTaskController() {
25
26
  }
26
27
  };
27
28
  }
29
+ // src/react/use-effect-event.ts
30
+ function useEffectEventFallback(callback) {
31
+ const latest = useRef(callback);
32
+ useInsertionEffect(() => {
33
+ latest.current = callback;
34
+ });
35
+ return useCallback((...args) => latest.current(...args), []);
36
+ }
37
+ const useEffectEvent = React.useEffectEvent ?? useEffectEventFallback;
28
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 = [];
29
41
  const IDLE_STATUS = "idle";
30
42
  /**
31
43
  * Chunks a heavy, synchronous per-item computation over `items` so it never
@@ -43,11 +55,18 @@ const IDLE_STATUS = "idle";
43
55
  * same as any other React dependency.
44
56
  */
45
57
  function useChunkedTask(items, processItem, options) {
46
- const { timeSliceMs, autoStart = true, onItem } = options ?? {};
58
+ const { timeSliceMs, batchSize, collectResults, autoStart = true, onItem, onError, onSlice, onYield } = options ?? {};
47
59
  const stableProcessItem = useEffectEvent((item, index) => processItem(item, index));
48
60
  const stableOnItem = useEffectEvent((result, item, index) => {
49
61
  onItem?.(result, item, index);
50
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
+ });
51
70
  const [controller] = useState(() => createTaskController());
52
71
  const idleSnapshot = useMemo(() => ({
53
72
  status: IDLE_STATUS,
@@ -60,7 +79,12 @@ function useChunkedTask(items, processItem, options) {
60
79
  items,
61
80
  processItem: stableProcessItem,
62
81
  onItem: stableOnItem,
63
- ...timeSliceMs !== void 0 ? { timeSliceMs } : {}
82
+ onError: stableOnError,
83
+ onSlice: stableOnSlice,
84
+ onYield: stableOnYield,
85
+ ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
86
+ ...batchSize !== void 0 ? { batchSize } : {},
87
+ ...collectResults !== void 0 ? { collectResults } : {}
64
88
  });
65
89
  controller.replace(scheduler);
66
90
  if (autoStart) scheduler.start();
@@ -71,6 +95,8 @@ function useChunkedTask(items, processItem, options) {
71
95
  }, [
72
96
  items,
73
97
  timeSliceMs,
98
+ batchSize,
99
+ collectResults,
74
100
  autoStart,
75
101
  controller
76
102
  ]);
@@ -78,13 +104,13 @@ function useChunkedTask(items, processItem, options) {
78
104
  const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);
79
105
  const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);
80
106
  const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);
81
- const results = controller.getCurrent()?.getResults();
82
107
  return {
83
108
  status: snapshot.status,
84
109
  processed: snapshot.processed,
85
110
  total: snapshot.total,
86
111
  error: snapshot.error,
87
- results,
112
+ results: snapshot.results,
113
+ skipped: snapshot.skipped ?? NO_SKIPPED,
88
114
  isIdle: snapshot.status === "idle",
89
115
  isProcessing: snapshot.status === "processing",
90
116
  isPaused: snapshot.status === "paused",
@@ -111,14 +137,23 @@ const IDLE_SNAPSHOT = { status: "idle" };
111
137
  * you would for `useMemo` or `useEffect`.
112
138
  */
113
139
  function useDeferredTask(task, deps, options) {
114
- const { timeSliceMs, autoStart = true } = options ?? {};
140
+ const { timeSliceMs, batchSize, autoStart = true, onSlice, onYield } = options ?? {};
115
141
  const stableTask = useEffectEvent(() => task());
142
+ const stableOnSlice = useEffectEvent((durationMs, segmentsDone) => {
143
+ onSlice?.(durationMs, segmentsDone);
144
+ });
145
+ const stableOnYield = useEffectEvent(() => {
146
+ onYield?.();
147
+ });
116
148
  const [controller] = useState(() => createTaskController());
117
149
  const snapshot = useSyncExternalStore(controller.subscribe, () => controller.getSnapshot() ?? IDLE_SNAPSHOT, () => IDLE_SNAPSHOT);
118
150
  useEffect(() => {
119
151
  const deferredTask = createDeferredTask({
120
152
  task: stableTask,
121
- ...timeSliceMs !== void 0 ? { timeSliceMs } : {}
153
+ onSlice: stableOnSlice,
154
+ onYield: stableOnYield,
155
+ ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
156
+ ...batchSize !== void 0 ? { batchSize } : {}
122
157
  });
123
158
  controller.replace(deferredTask);
124
159
  if (autoStart) deferredTask.start();
@@ -129,6 +164,7 @@ function useDeferredTask(task, deps, options) {
129
164
  }, [
130
165
  ...deps,
131
166
  timeSliceMs,
167
+ batchSize,
132
168
  autoStart,
133
169
  controller
134
170
  ]);
@@ -136,11 +172,10 @@ function useDeferredTask(task, deps, options) {
136
172
  const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);
137
173
  const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);
138
174
  const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);
139
- const value = controller.getCurrent()?.getValue();
140
175
  return {
141
176
  status: snapshot.status,
142
177
  error: snapshot.error,
143
- value,
178
+ value: snapshot.value,
144
179
  isIdle: snapshot.status === "idle",
145
180
  isProcessing: snapshot.status === "processing",
146
181
  isPaused: snapshot.status === "paused",