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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"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.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\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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"}
@@ -0,0 +1,16 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_deferred_task = require("../deferred-task-yh5pWlSj.cjs");
3
+ exports.DEFAULT_TIME_SLICE_MS = require_deferred_task.DEFAULT_TIME_SLICE_MS;
4
+ exports.createChunkScheduler = require_deferred_task.createChunkScheduler;
5
+ exports.createDeferredTask = require_deferred_task.createDeferredTask;
6
+ exports.initialChunkState = require_deferred_task.initialChunkState;
7
+ exports.isInputPending = require_deferred_task.isInputPending;
8
+ exports.isTerminal = require_deferred_task.isTerminal;
9
+ exports.now = require_deferred_task.now;
10
+ exports.resolveYieldStrategy = require_deferred_task.resolveYieldStrategy;
11
+ exports.shouldYieldNow = require_deferred_task.shouldYieldNow;
12
+ exports.supportsInputPending = require_deferred_task.supportsInputPending;
13
+ exports.supportsMessageChannel = require_deferred_task.supportsMessageChannel;
14
+ exports.supportsSchedulerYield = require_deferred_task.supportsSchedulerYield;
15
+ exports.transition = require_deferred_task.transition;
16
+ exports.yieldToMain = require_deferred_task.yieldToMain;
@@ -0,0 +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-DkHdsliy.js";
@@ -71,6 +71,19 @@ function supportsInputPending() {
71
71
  return typeof globalThis.navigator?.scheduling?.isInputPending === "function";
72
72
  }
73
73
  /**
74
+ * Resolves the input-pending probe once, for callers that would otherwise
75
+ * run the feature detection on every item of a long task: walking
76
+ * `navigator.scheduling.isInputPending` through optional calls is wasted
77
+ * work a million times over. Returns a function that always answers
78
+ * `false` where the API is missing, so callers need no second branch.
79
+ */
80
+ function resolveInputPending() {
81
+ return supportsInputPending() ? isInputPending : returnsFalse;
82
+ }
83
+ function returnsFalse() {
84
+ return false;
85
+ }
86
+ /**
74
87
  * Best-effort read of whether the browser has pending input right now.
75
88
  * Returns `false` — never throws — when the API is unavailable or errors,
76
89
  * since the caller's fallback (a plain time budget) is always safe.
@@ -92,6 +105,7 @@ function yieldViaMessageChannel() {
92
105
  return new Promise((resolve) => {
93
106
  const channel = new MessageChannel();
94
107
  channel.port1.onmessage = () => {
108
+ channel.port1.close();
95
109
  resolve();
96
110
  };
97
111
  channel.port2.postMessage(null);
@@ -139,6 +153,23 @@ function now() {
139
153
  return Date.now();
140
154
  }
141
155
  /**
156
+ * A time slice of zero, a negative number or NaN is meaningless, so those
157
+ * fall back to the default rather than producing a task that yields after
158
+ * every item (or never).
159
+ */
160
+ function normalizeTimeSliceMs(value) {
161
+ if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 5;
162
+ return value;
163
+ }
164
+ /**
165
+ * Batch sizes are whole items, at least one. Anything else — a fraction,
166
+ * zero, a negative, NaN — falls back to 1, the safe default.
167
+ */
168
+ function normalizeBatchSize(value) {
169
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return 1;
170
+ return Math.floor(value);
171
+ }
172
+ /**
142
173
  * Whether the caller should yield right now: either the time slice has run
143
174
  * out, or the browser is reporting pending input (a stronger, earlier
144
175
  * signal than the clock — used only when the runtime exposes it).
@@ -149,9 +180,17 @@ function shouldYieldNow(options) {
149
180
  return isInputPending();
150
181
  }
151
182
  // src/core/chunk-scheduler.ts
152
- function normalizeTimeSliceMs$1(value) {
153
- if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 5;
154
- return value;
183
+ /**
184
+ * `items[index]` for an index the caller has already bounds-checked.
185
+ *
186
+ * The loop below only ever indexes below `items.length`, but
187
+ * `noUncheckedIndexedAccess` widens every index read to `T | undefined`,
188
+ * and `T` may itself include `undefined` — so no runtime check can narrow
189
+ * it. This states the invariant in one place instead of asserting at each
190
+ * use, and compiles to a plain index read.
191
+ */
192
+ function at(items, index) {
193
+ return items[index];
155
194
  }
156
195
  /**
157
196
  * Builds a one-shot, controllable scheduler that processes `items` through
@@ -166,20 +205,26 @@ function normalizeTimeSliceMs$1(value) {
166
205
  * scheduler, which is the idiomatic React data-flow anyway.
167
206
  */
168
207
  function createChunkScheduler(options) {
169
- const { items, processItem, onItem, signal, yieldStrategy } = options;
170
- const timeSliceMs = normalizeTimeSliceMs$1(options.timeSliceMs);
208
+ const { items, processItem, onItem, onError, signal, yieldStrategy, onSlice, onYield } = options;
209
+ const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
210
+ const batchSize = normalizeBatchSize(options.batchSize);
211
+ const collectResults = options.collectResults ?? true;
171
212
  const total = items.length;
213
+ const inputPending = resolveInputPending();
172
214
  let machineState = initialChunkState;
173
215
  let processed = 0;
174
216
  let loopStarted = false;
175
217
  let settledFlag = false;
176
218
  const results = [];
177
219
  let finalResults;
220
+ const skipped = [];
221
+ let cancelReason;
178
222
  let snapshot = {
179
223
  status: machineState.status,
180
224
  processed,
181
225
  total
182
226
  };
227
+ let snapshotStale = false;
183
228
  const listeners = /* @__PURE__ */ new Set();
184
229
  let settleFn;
185
230
  const settledPromise = new Promise((resolve) => {
@@ -201,20 +246,27 @@ function createChunkScheduler(options) {
201
246
  function notify() {
202
247
  for (const listener of listeners) listener();
203
248
  }
204
- function refreshSnapshot() {
205
- snapshot = {
206
- status: machineState.status,
207
- processed,
208
- total,
209
- ...machineState.error !== void 0 ? { error: machineState.error } : {}
210
- };
249
+ function getSnapshot() {
250
+ if (snapshotStale) {
251
+ snapshotStale = false;
252
+ snapshot = {
253
+ status: machineState.status,
254
+ processed,
255
+ total,
256
+ ...machineState.error !== void 0 ? { error: machineState.error } : {},
257
+ ...finalResults !== void 0 ? { results: finalResults } : {},
258
+ ...skipped.length > 0 ? { skipped: skipped.slice() } : {},
259
+ ...cancelReason !== void 0 ? { reason: cancelReason } : {}
260
+ };
261
+ }
262
+ return snapshot;
211
263
  }
212
264
  function dispatch(event) {
213
265
  const next = transition(machineState, event);
214
266
  const changed = next !== machineState;
215
267
  if (changed) {
216
268
  machineState = next;
217
- refreshSnapshot();
269
+ snapshotStale = true;
218
270
  notify();
219
271
  }
220
272
  return { changed };
@@ -227,78 +279,113 @@ function createChunkScheduler(options) {
227
279
  settleFn(result);
228
280
  }
229
281
  /**
230
- * The single path every terminal transition goes through: snapshots the
231
- * results array exactly once (not per item), publishes it via
232
- * getResults(), and settles the public promise with the same reference —
233
- * no redundant second copy.
282
+ * Publishes the accumulated results exactly once, at the moment the task
283
+ * settles — not per item, and never as a second copy. Always called
284
+ * *before* the terminal status is announced, so the notification that
285
+ * reports `done` already carries the results: a subscriber can never
286
+ * observe one without the other.
234
287
  */
235
- function finish(status, error) {
236
- const resultsCopy = [...results];
237
- finalResults = resultsCopy;
238
- settle(status === "error" ? {
239
- status,
240
- error,
241
- results: resultsCopy
242
- } : {
288
+ function publishResults() {
289
+ finalResults = results;
290
+ snapshotStale = true;
291
+ return finalResults;
292
+ }
293
+ function settleWith(status, published, error) {
294
+ settle({
243
295
  status,
244
- results: resultsCopy
296
+ results: published,
297
+ ...error !== void 0 ? { error } : {},
298
+ ...skipped.length > 0 ? { skipped: skipped.slice() } : {},
299
+ ...status === "cancelled" && cancelReason !== void 0 ? { reason: cancelReason } : {}
245
300
  });
246
301
  }
302
+ /**
303
+ * Settling as cancelled, for both places that can do it: the loop
304
+ * noticing, and cancel() on a task whose loop never ran. The CANCEL
305
+ * transition was announced earlier, before there were any results to
306
+ * carry, so this notifies again now that there are.
307
+ */
308
+ function finishCancelled() {
309
+ settleWith("cancelled", publishResults());
310
+ notify();
311
+ }
247
312
  function settleIfNeverStarted() {
248
- const { status, error } = machineState;
313
+ const { status } = machineState;
249
314
  if (loopStarted || !isTerminal(status)) return;
250
315
  loopStarted = true;
251
- /* v8 ignore next */
252
- if (status === "error") finish("error", error);
253
- else finish(status);
316
+ finishCancelled();
254
317
  }
255
318
  function handleAbort() {
319
+ cancelReason = signal?.reason;
256
320
  cancel();
257
321
  }
258
322
  function detachSignal() {
259
323
  signal?.removeEventListener("abort", handleAbort);
260
324
  }
261
- if (signal?.aborted) dispatch({ type: "CANCEL" });
262
- else signal?.addEventListener("abort", handleAbort);
325
+ if (signal?.aborted) {
326
+ cancelReason = signal.reason;
327
+ dispatch({ type: "CANCEL" });
328
+ } else signal?.addEventListener("abort", handleAbort);
263
329
  async function runLoop() {
264
330
  let sliceStartedAt = now();
265
- itemLoop: for (const [index, item] of items.entries()) {
266
- for (;;) {
331
+ let index = 0;
332
+ try {
333
+ while (index < total) {
267
334
  const status = machineState.status;
268
- if (status === "cancelled") break itemLoop;
269
- if (status !== "paused") break;
270
- await waitForWake();
271
- sliceStartedAt = now();
272
- }
273
- try {
274
- const result = processItem(item, index);
275
- results.push(result);
276
- processed = index + 1;
277
- refreshSnapshot();
278
- onItem?.(result, item, index);
279
- } catch (error) {
280
- dispatch({
281
- type: "ERROR",
282
- error
283
- });
284
- finish("error", error);
285
- return;
286
- }
287
- if (shouldYieldNow({
288
- sliceStartedAt,
289
- timeSliceMs
290
- })) {
291
- await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
292
- notify();
293
- sliceStartedAt = now();
335
+ if (status === "cancelled") break;
336
+ if (status === "paused") {
337
+ await waitForWake();
338
+ sliceStartedAt = now();
339
+ continue;
340
+ }
341
+ const batchEnd = index + batchSize <= total ? index + batchSize : total;
342
+ while (index < batchEnd) {
343
+ const item = at(items, 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
+ }
358
+ if (collectResults) results.push(result);
359
+ processed = index + 1;
360
+ onItem?.(result, item, index);
361
+ index += 1;
362
+ }
363
+ snapshotStale = true;
364
+ const checkedAt = now();
365
+ if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
366
+ onSlice?.(checkedAt - sliceStartedAt, index);
367
+ await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
368
+ notify();
369
+ sliceStartedAt = now();
370
+ onYield?.();
371
+ }
294
372
  }
373
+ } catch (error) {
374
+ const published = publishResults();
375
+ dispatch({
376
+ type: "ERROR",
377
+ error
378
+ });
379
+ settleWith("error", published, error);
380
+ return;
295
381
  }
296
382
  if (machineState.status === "cancelled") {
297
- finish("cancelled");
383
+ finishCancelled();
298
384
  return;
299
385
  }
386
+ const published = publishResults();
300
387
  dispatch({ type: "COMPLETE" });
301
- finish("done");
388
+ settleWith("done", published);
302
389
  }
303
390
  function start() {
304
391
  dispatch({ type: "START" });
@@ -325,7 +412,7 @@ function createChunkScheduler(options) {
325
412
  pause,
326
413
  resume,
327
414
  cancel,
328
- getSnapshot: () => snapshot,
415
+ getSnapshot,
329
416
  getResults: () => finalResults,
330
417
  subscribe(listener) {
331
418
  listeners.add(listener);
@@ -337,10 +424,6 @@ function createChunkScheduler(options) {
337
424
  };
338
425
  }
339
426
  // src/core/deferred-task.ts
340
- function normalizeTimeSliceMs(value) {
341
- if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 5;
342
- return value;
343
- }
344
427
  /**
345
428
  * The generator-driven sibling of `createChunkScheduler`, for heavy work
346
429
  * that isn't shaped like "one function applied to each item of an array".
@@ -349,13 +432,17 @@ function normalizeTimeSliceMs(value) {
349
432
  * here unchanged.
350
433
  */
351
434
  function createDeferredTask(options) {
352
- const { task, signal, yieldStrategy } = options;
435
+ const { task, signal, yieldStrategy, onSlice, onYield } = options;
353
436
  const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
437
+ const batchSize = normalizeBatchSize(options.batchSize);
438
+ const inputPending = resolveInputPending();
354
439
  let machineState = initialChunkState;
355
440
  let loopStarted = false;
356
441
  let settledFlag = false;
357
- let value;
442
+ let settledValue;
443
+ let cancelReason;
358
444
  let snapshot = { status: machineState.status };
445
+ let snapshotStale = false;
359
446
  const listeners = /* @__PURE__ */ new Set();
360
447
  let settleFn;
361
448
  const settledPromise = new Promise((resolve) => {
@@ -377,18 +464,24 @@ function createDeferredTask(options) {
377
464
  function notify() {
378
465
  for (const listener of listeners) listener();
379
466
  }
380
- function refreshSnapshot() {
381
- snapshot = {
382
- status: machineState.status,
383
- ...machineState.error !== void 0 ? { error: machineState.error } : {}
384
- };
467
+ function getSnapshot() {
468
+ if (snapshotStale) {
469
+ snapshotStale = false;
470
+ snapshot = {
471
+ status: machineState.status,
472
+ ...machineState.error !== void 0 ? { error: machineState.error } : {},
473
+ ...settledValue !== void 0 ? { value: settledValue } : {},
474
+ ...cancelReason !== void 0 ? { reason: cancelReason } : {}
475
+ };
476
+ }
477
+ return snapshot;
385
478
  }
386
479
  function dispatch(event) {
387
480
  const next = transition(machineState, event);
388
481
  const changed = next !== machineState;
389
482
  if (changed) {
390
483
  machineState = next;
391
- refreshSnapshot();
484
+ snapshotStale = true;
392
485
  notify();
393
486
  }
394
487
  return { changed };
@@ -400,38 +493,63 @@ function createDeferredTask(options) {
400
493
  detachSignal();
401
494
  settleFn(result);
402
495
  }
403
- function finish(status, resolvedValue, error) {
496
+ /**
497
+ * Publishes the returned value before the terminal status is announced,
498
+ * so the notification that reports `done` already carries it.
499
+ */
500
+ function publishValue(resolvedValue) {
501
+ settledValue = resolvedValue;
502
+ snapshotStale = true;
503
+ }
504
+ function settleWith(status, resolvedValue, error) {
404
505
  if (status === "done") {
405
- value = resolvedValue;
406
506
  settle({
407
507
  status,
408
508
  ...resolvedValue !== void 0 ? { value: resolvedValue } : {}
409
509
  });
410
510
  return;
411
511
  }
412
- settle(status === "error" ? {
512
+ if (status === "error") {
513
+ settle({
514
+ status,
515
+ error
516
+ });
517
+ return;
518
+ }
519
+ settle({
413
520
  status,
414
- error
415
- } : { status });
521
+ ...cancelReason !== void 0 ? { reason: cancelReason } : {}
522
+ });
523
+ }
524
+ /**
525
+ * Settling as cancelled, for both places that can do it. The CANCEL
526
+ * transition was announced earlier, so this notifies again now that the
527
+ * task has actually settled.
528
+ */
529
+ function finishCancelled() {
530
+ settleWith("cancelled");
531
+ notify();
416
532
  }
417
533
  function settleIfNeverStarted() {
418
- const { status, error } = machineState;
534
+ const { status } = machineState;
419
535
  if (loopStarted || !isTerminal(status)) return;
420
536
  loopStarted = true;
421
- /* v8 ignore next */
422
- if (status === "error") finish("error", void 0, error);
423
- else finish(status);
537
+ finishCancelled();
424
538
  }
425
539
  function handleAbort() {
540
+ cancelReason = signal?.reason;
426
541
  cancel();
427
542
  }
428
543
  function detachSignal() {
429
544
  signal?.removeEventListener("abort", handleAbort);
430
545
  }
431
- if (signal?.aborted) dispatch({ type: "CANCEL" });
432
- else signal?.addEventListener("abort", handleAbort);
546
+ if (signal?.aborted) {
547
+ cancelReason = signal.reason;
548
+ dispatch({ type: "CANCEL" });
549
+ } else signal?.addEventListener("abort", handleAbort);
433
550
  async function runLoop() {
434
551
  let sliceStartedAt = now();
552
+ let segment = 0;
435
553
  const generator = task();
436
554
  let step = {
437
555
  done: false,
@@ -449,15 +567,23 @@ function createDeferredTask(options) {
449
567
  sliceStartedAt = now();
450
568
  continue;
451
569
  }
452
- step = generator.next();
453
- if (step.done) break;
454
- if (shouldYieldNow({
455
- sliceStartedAt,
456
- timeSliceMs
457
- })) {
570
+ let done = false;
571
+ for (let inBatch = 0; inBatch < batchSize; inBatch++) {
572
+ step = generator.next();
573
+ segment += 1;
574
+ if (step.done) {
575
+ done = true;
576
+ break;
577
+ }
578
+ }
579
+ if (done) break;
580
+ const checkedAt = now();
581
+ if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
582
+ onSlice?.(checkedAt - sliceStartedAt, segment);
458
583
  await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
459
584
  notify();
460
585
  sliceStartedAt = now();
586
+ onYield?.();
461
587
  }
462
588
  }
463
589
  } catch (error) {
@@ -465,16 +591,18 @@ function createDeferredTask(options) {
465
591
  type: "ERROR",
466
592
  error
467
593
  });
468
- finish("error", void 0, error);
594
+ settleWith("error", void 0, error);
469
595
  return;
470
596
  }
471
597
  if (machineState.status === "cancelled") {
472
- finish("cancelled");
598
+ finishCancelled();
473
599
  return;
474
600
  }
475
- dispatch({ type: "COMPLETE" });
476
601
  /* v8 ignore next */
477
- finish("done", step.done ? step.value : void 0);
602
+ const returned = step.done ? step.value : void 0;
603
+ publishValue(returned);
604
+ dispatch({ type: "COMPLETE" });
605
+ settleWith("done", returned);
478
606
  }
479
607
  function start() {
480
608
  dispatch({ type: "START" });
@@ -501,8 +629,8 @@ function createDeferredTask(options) {
501
629
  pause,
502
630
  resume,
503
631
  cancel,
504
- getSnapshot: () => snapshot,
505
- getValue: () => value,
632
+ getSnapshot,
633
+ getValue: () => settledValue,
506
634
  subscribe(listener) {
507
635
  listeners.add(listener);
508
636
  return () => {
@@ -514,4 +642,4 @@ function createDeferredTask(options) {
514
642
  }
515
643
  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 };
516
644
 
517
- //# sourceMappingURL=deferred-task-5kfztF2G.js.map
645
+ //# sourceMappingURL=deferred-task-DkHdsliy.js.map