react-inp-chunker 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/core/index.cjs +16 -0
- package/dist/core/index.d.cts +2 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/deferred-task-5kfztF2G.js +517 -0
- package/dist/deferred-task-5kfztF2G.js.map +1 -0
- package/dist/deferred-task-BXVzapQg.cjs +600 -0
- package/dist/deferred-task-BXVzapQg.cjs.map +1 -0
- package/dist/index-C6B_Nm3i.d.cts +237 -0
- package/dist/index-C6B_Nm3i.d.cts.map +1 -0
- package/dist/index-C6B_Nm3i.d.ts +237 -0
- package/dist/index-C6B_Nm3i.d.ts.map +1 -0
- package/dist/index.cjs +160 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +84 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/package.json +113 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_deferred_task = require("./deferred-task-BXVzapQg.cjs");
|
|
3
|
+
let react = require("react");
|
|
4
|
+
// src/react/task-controller.ts
|
|
5
|
+
function createTaskController() {
|
|
6
|
+
let current = null;
|
|
7
|
+
let detachFromCurrent;
|
|
8
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
9
|
+
const notify = () => {
|
|
10
|
+
for (const listener of listeners) listener();
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
subscribe: (listener) => {
|
|
14
|
+
listeners.add(listener);
|
|
15
|
+
return () => {
|
|
16
|
+
listeners.delete(listener);
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
getSnapshot: () => current?.getSnapshot(),
|
|
20
|
+
getCurrent: () => current,
|
|
21
|
+
replace: (task) => {
|
|
22
|
+
detachFromCurrent?.();
|
|
23
|
+
current = task;
|
|
24
|
+
detachFromCurrent = task?.subscribe(notify);
|
|
25
|
+
notify();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// src/react/useChunkedTask.ts
|
|
30
|
+
const IDLE_STATUS = "idle";
|
|
31
|
+
/**
|
|
32
|
+
* Chunks a heavy, synchronous per-item computation over `items` so it never
|
|
33
|
+
* blocks the main thread for longer than a time slice — the thing that
|
|
34
|
+
* costs you Interaction to Next Paint. Starts automatically by default;
|
|
35
|
+
* pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.
|
|
36
|
+
* behind a button).
|
|
37
|
+
*
|
|
38
|
+
* `processItem` and `onItem` can be fresh closures every render — the hook
|
|
39
|
+
* always calls the latest version (via `useEffectEvent`) without that
|
|
40
|
+
* causing the task to restart. What *does* restart the task is `items`
|
|
41
|
+
* changing identity (a new array reference), or `timeSliceMs`/`autoStart`
|
|
42
|
+
* changing. That's deliberate: a new `items` array means new work, and
|
|
43
|
+
* memoizing it when you don't want that is the caller's call to make, the
|
|
44
|
+
* same as any other React dependency.
|
|
45
|
+
*/
|
|
46
|
+
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) => {
|
|
50
|
+
onItem?.(result, item, index);
|
|
51
|
+
});
|
|
52
|
+
const [controller] = (0, react.useState)(() => createTaskController());
|
|
53
|
+
const idleSnapshot = (0, react.useMemo)(() => ({
|
|
54
|
+
status: IDLE_STATUS,
|
|
55
|
+
processed: 0,
|
|
56
|
+
total: items.length
|
|
57
|
+
}), [items.length]);
|
|
58
|
+
const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, () => controller.getSnapshot() ?? idleSnapshot, () => idleSnapshot);
|
|
59
|
+
(0, react.useEffect)(() => {
|
|
60
|
+
const scheduler = require_deferred_task.createChunkScheduler({
|
|
61
|
+
items,
|
|
62
|
+
processItem: stableProcessItem,
|
|
63
|
+
onItem: stableOnItem,
|
|
64
|
+
...timeSliceMs !== void 0 ? { timeSliceMs } : {}
|
|
65
|
+
});
|
|
66
|
+
controller.replace(scheduler);
|
|
67
|
+
if (autoStart) scheduler.start();
|
|
68
|
+
return () => {
|
|
69
|
+
scheduler.cancel();
|
|
70
|
+
controller.replace(null);
|
|
71
|
+
};
|
|
72
|
+
}, [
|
|
73
|
+
items,
|
|
74
|
+
timeSliceMs,
|
|
75
|
+
autoStart,
|
|
76
|
+
controller
|
|
77
|
+
]);
|
|
78
|
+
const start = (0, react.useCallback)(() => controller.getCurrent()?.start(), [controller]);
|
|
79
|
+
const pause = (0, react.useCallback)(() => controller.getCurrent()?.pause(), [controller]);
|
|
80
|
+
const resume = (0, react.useCallback)(() => controller.getCurrent()?.resume(), [controller]);
|
|
81
|
+
const cancel = (0, react.useCallback)(() => controller.getCurrent()?.cancel(), [controller]);
|
|
82
|
+
const results = controller.getCurrent()?.getResults();
|
|
83
|
+
return {
|
|
84
|
+
status: snapshot.status,
|
|
85
|
+
processed: snapshot.processed,
|
|
86
|
+
total: snapshot.total,
|
|
87
|
+
error: snapshot.error,
|
|
88
|
+
results,
|
|
89
|
+
isIdle: snapshot.status === "idle",
|
|
90
|
+
isProcessing: snapshot.status === "processing",
|
|
91
|
+
isPaused: snapshot.status === "paused",
|
|
92
|
+
isDone: snapshot.status === "done",
|
|
93
|
+
isCancelled: snapshot.status === "cancelled",
|
|
94
|
+
isError: snapshot.status === "error",
|
|
95
|
+
start,
|
|
96
|
+
pause,
|
|
97
|
+
resume,
|
|
98
|
+
cancel
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// src/react/useDeferredTask.ts
|
|
102
|
+
const IDLE_SNAPSHOT = { status: "idle" };
|
|
103
|
+
/**
|
|
104
|
+
* Runs an arbitrary heavy, synchronous generator function in chunks, so it
|
|
105
|
+
* never produces a long task on its own. Where `useChunkedTask` chunks "one
|
|
106
|
+
* function applied to every item of an array" automatically, this hook
|
|
107
|
+
* leaves the chunk boundaries to you — put `yield` wherever your function
|
|
108
|
+
* can safely pause (see `DeferredGenerator`'s doc comment for an example).
|
|
109
|
+
*
|
|
110
|
+
* `task` is only called when the effect (re)creates the underlying task, not
|
|
111
|
+
* on every render — list whatever it closes over in `deps`, the same way
|
|
112
|
+
* you would for `useMemo` or `useEffect`.
|
|
113
|
+
*/
|
|
114
|
+
function useDeferredTask(task, deps, options) {
|
|
115
|
+
const { timeSliceMs, autoStart = true } = options ?? {};
|
|
116
|
+
const stableTask = (0, react.useEffectEvent)(() => task());
|
|
117
|
+
const [controller] = (0, react.useState)(() => createTaskController());
|
|
118
|
+
const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, () => controller.getSnapshot() ?? IDLE_SNAPSHOT, () => IDLE_SNAPSHOT);
|
|
119
|
+
(0, react.useEffect)(() => {
|
|
120
|
+
const deferredTask = require_deferred_task.createDeferredTask({
|
|
121
|
+
task: stableTask,
|
|
122
|
+
...timeSliceMs !== void 0 ? { timeSliceMs } : {}
|
|
123
|
+
});
|
|
124
|
+
controller.replace(deferredTask);
|
|
125
|
+
if (autoStart) deferredTask.start();
|
|
126
|
+
return () => {
|
|
127
|
+
deferredTask.cancel();
|
|
128
|
+
controller.replace(null);
|
|
129
|
+
};
|
|
130
|
+
}, [
|
|
131
|
+
...deps,
|
|
132
|
+
timeSliceMs,
|
|
133
|
+
autoStart,
|
|
134
|
+
controller
|
|
135
|
+
]);
|
|
136
|
+
const start = (0, react.useCallback)(() => controller.getCurrent()?.start(), [controller]);
|
|
137
|
+
const pause = (0, react.useCallback)(() => controller.getCurrent()?.pause(), [controller]);
|
|
138
|
+
const resume = (0, react.useCallback)(() => controller.getCurrent()?.resume(), [controller]);
|
|
139
|
+
const cancel = (0, react.useCallback)(() => controller.getCurrent()?.cancel(), [controller]);
|
|
140
|
+
const value = controller.getCurrent()?.getValue();
|
|
141
|
+
return {
|
|
142
|
+
status: snapshot.status,
|
|
143
|
+
error: snapshot.error,
|
|
144
|
+
value,
|
|
145
|
+
isIdle: snapshot.status === "idle",
|
|
146
|
+
isProcessing: snapshot.status === "processing",
|
|
147
|
+
isPaused: snapshot.status === "paused",
|
|
148
|
+
isDone: snapshot.status === "done",
|
|
149
|
+
isCancelled: snapshot.status === "cancelled",
|
|
150
|
+
isError: snapshot.status === "error",
|
|
151
|
+
start,
|
|
152
|
+
pause,
|
|
153
|
+
resume,
|
|
154
|
+
cancel
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
exports.useChunkedTask = useChunkedTask;
|
|
158
|
+
exports.useDeferredTask = useDeferredTask;
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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":";;;;AAuCA,SAAgB,uBAEU;CACxB,IAAI,UAAuB;CAC3B,IAAI;CACJ,MAAM,4BAAY,IAAI,IAAgB;CAEtC,MAAM,eAAqB;EACzB,KAAK,MAAM,YAAY,WAAW,SAAS;CAC7C;CAEA,OAAO;EACL,YAAY,aAAa;GACvB,UAAU,IAAI,QAAQ;GACtB,aAAa;IACX,UAAU,OAAO,QAAQ;GAC3B;EACF;EACA,mBACE,SAAS,YAAY;EACvB,kBAAkB;EAClB,UAAU,SAAS;GACjB,oBAAoB;GACpB,UAAU;GACV,oBAAoB,MAAM,UAAU,MAAM;GAC1C,OAAO;EACT;CACF;AACF;;ACpBA,MAAM,cAA2B;;;;;;;;;;;;;;;;AAiBjC,SAAgB,eACd,OACA,aACA,SACwB;CACxB,MAAM,EAAE,aAAa,YAAY,MAAM,WAAW,WAAW,CAAC;CAO9D,MAAM,qBAAA,GAAoBA,MAAAA,eAAAA,EAAgB,MAAS,UACjD,YAAY,MAAM,KAAK,CACzB;CACA,MAAM,gBAAA,GAAeA,MAAAA,eAAAA,EAAgB,QAAW,MAAS,UAAkB;EACzE,SAAS,QAAQ,MAAM,KAAK;CAC9B,CAAC;CAMD,MAAM,CAAC,eAAA,GAAcC,MAAAA,SAAAA,OAAe,qBAAwC,CAAC;CAI7E,MAAM,gBAAA,GAAeC,MAAAA,QAAAA,QACZ;EAAE,QAAQ;EAAa,WAAW;EAAG,OAAO,MAAM;CAAO,IAChE,CAAC,MAAM,MAAM,CACf;CAEA,MAAM,YAAA,GAAWC,MAAAA,qBAAAA,CACf,WAAW,iBACL,WAAW,YAAY,KAAK,oBAC5B,YACR;CAEA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,YAAYC,sBAAAA,qBAA2B;GAC3C;GACA,aAAa;GACb,QAAQ;GACR,GAAI,gBAAgB,KAAA,IAAY,EAAE,YAAY,IAAI,CAAC;EACrD,CAAC;EACD,WAAW,QAAQ,SAAS;EAC5B,IAAI,WAAW,UAAU,MAAM;EAE/B,aAAa;GAGX,UAAU,OAAO;GACjB,WAAW,QAAQ,IAAI;EACzB;CAKF,GAAG;EAAC;EAAO;EAAa;EAAW;CAAU,CAAC;CAE9C,MAAM,SAAA,GAAQC,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,MAAM,GAAG,CAAC,UAAU,CAAC;CAC9E,MAAM,SAAA,GAAQA,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,MAAM,GAAG,CAAC,UAAU,CAAC;CAC9E,MAAM,UAAA,GAASA,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,OAAO,GAAG,CAAC,UAAU,CAAC;CAChF,MAAM,UAAA,GAASA,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,OAAO,GAAG,CAAC,UAAU,CAAC;CAOhF,MAAM,UAAU,WAAW,WAAW,CAAC,EAAE,WAAW;CAEpD,OAAO;EACL,QAAQ,SAAS;EACjB,WAAW,SAAS;EACpB,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB;EACA,QAAQ,SAAS,WAAW;EAC5B,cAAc,SAAS,WAAW;EAClC,UAAU,SAAS,WAAW;EAC9B,QAAQ,SAAS,WAAW;EAC5B,aAAa,SAAS,WAAW;EACjC,SAAS,SAAS,WAAW;EAC7B;EACA;EACA;EACA;CACF;AACF;;AChHA,MAAM,gBAAsC,EAAE,QAAQ,OAAO;;;;;;;;;;;;AAa7D,SAAgB,gBACd,MACA,MACA,SACyB;CACzB,MAAM,EAAE,aAAa,YAAY,SAAS,WAAW,CAAC;CAKtD,MAAM,cAAA,GAAaC,MAAAA,eAAAA,OAAqB,KAAK,CAAC;CAE9C,MAAM,CAAC,eAAA,GAAcC,MAAAA,SAAAA,OAAe,qBAAsC,CAAC;CAE3E,MAAM,YAAA,GAAWC,MAAAA,qBAAAA,CACf,WAAW,iBACL,WAAW,YAAY,KAAK,qBAC5B,aACR;CAEA,CAAA,GAAA,MAAA,UAAA,OACQ;EACJ,MAAM,eAAeC,sBAAAA,mBAAsB;GACzC,MAAM;GACN,GAAI,gBAAgB,KAAA,IAAY,EAAE,YAAY,IAAI,CAAC;EACrD,CAAC;EACD,WAAW,QAAQ,YAAY;EAC/B,IAAI,WAAW,aAAa,MAAM;EAElC,aAAa;GACX,aAAa,OAAO;GACpB,WAAW,QAAQ,IAAI;EACzB;CACF,GAQA;EAAC,GAAG;EAAM;EAAa;EAAW;CAAU,CAC9C;CAEA,MAAM,SAAA,GAAQC,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,MAAM,GAAG,CAAC,UAAU,CAAC;CAC9E,MAAM,SAAA,GAAQA,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,MAAM,GAAG,CAAC,UAAU,CAAC;CAC9E,MAAM,UAAA,GAASA,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,OAAO,GAAG,CAAC,UAAU,CAAC;CAChF,MAAM,UAAA,GAASA,MAAAA,YAAAA,OAAkB,WAAW,WAAW,CAAC,EAAE,OAAO,GAAG,CAAC,UAAU,CAAC;CAKhF,MAAM,QAAQ,WAAW,WAAW,CAAC,EAAE,SAAS;CAEhD,OAAO;EACL,QAAQ,SAAS;EACjB,OAAO,SAAS;EAChB;EACA,QAAQ,SAAS,WAAW;EAC5B,cAAc,SAAS,WAAW;EAClC,UAAU,SAAS,WAAW;EAC9B,QAAQ,SAAS,WAAW;EAC5B,aAAa,SAAS,WAAW;EACjC,SAAS,SAAS,WAAW;EAC7B;EACA;EACA;EACA;CACF;AACF"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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";
|
|
2
|
+
interface UseChunkedTaskOptions<T, R> {
|
|
3
|
+
/** Max ms of uninterrupted work before yielding. Default 5. */
|
|
4
|
+
readonly timeSliceMs?: number;
|
|
5
|
+
/** Start processing automatically when the task is (re)created. Default `true`. */
|
|
6
|
+
readonly autoStart?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Called synchronously after each item is processed. This is the cheap
|
|
9
|
+
* way to consume results incrementally while the task is still running —
|
|
10
|
+
* write into a ref-backed buffer, a canvas, a chart, whatever you need —
|
|
11
|
+
* without paying for an array copy on every yield checkpoint.
|
|
12
|
+
*/
|
|
13
|
+
readonly onItem?: (result: R, item: T, index: number) => void;
|
|
14
|
+
}
|
|
15
|
+
interface UseChunkedTaskState<R> {
|
|
16
|
+
readonly status: ChunkStatus;
|
|
17
|
+
readonly processed: number;
|
|
18
|
+
readonly total: number;
|
|
19
|
+
readonly error: unknown;
|
|
20
|
+
/** The full results array once the task has settled — `undefined` until then. */
|
|
21
|
+
readonly results: readonly R[] | undefined;
|
|
22
|
+
readonly isIdle: boolean;
|
|
23
|
+
readonly isProcessing: boolean;
|
|
24
|
+
readonly isPaused: boolean;
|
|
25
|
+
readonly isDone: boolean;
|
|
26
|
+
readonly isCancelled: boolean;
|
|
27
|
+
readonly isError: boolean;
|
|
28
|
+
readonly start: () => void;
|
|
29
|
+
readonly pause: () => void;
|
|
30
|
+
readonly resume: () => void;
|
|
31
|
+
readonly cancel: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Chunks a heavy, synchronous per-item computation over `items` so it never
|
|
35
|
+
* blocks the main thread for longer than a time slice — the thing that
|
|
36
|
+
* costs you Interaction to Next Paint. Starts automatically by default;
|
|
37
|
+
* pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.
|
|
38
|
+
* behind a button).
|
|
39
|
+
*
|
|
40
|
+
* `processItem` and `onItem` can be fresh closures every render — the hook
|
|
41
|
+
* always calls the latest version (via `useEffectEvent`) without that
|
|
42
|
+
* causing the task to restart. What *does* restart the task is `items`
|
|
43
|
+
* changing identity (a new array reference), or `timeSliceMs`/`autoStart`
|
|
44
|
+
* changing. That's deliberate: a new `items` array means new work, and
|
|
45
|
+
* memoizing it when you don't want that is the caller's call to make, the
|
|
46
|
+
* same as any other React dependency.
|
|
47
|
+
*/
|
|
48
|
+
export declare function useChunkedTask<T, R>(items: readonly T[], processItem: (item: T, index: number) => R, options?: UseChunkedTaskOptions<T, R>): UseChunkedTaskState<R>;
|
|
49
|
+
interface UseDeferredTaskOptions {
|
|
50
|
+
/** Max ms of uninterrupted work before yielding. Default 5. */
|
|
51
|
+
readonly timeSliceMs?: number;
|
|
52
|
+
/** Start processing automatically when the task is (re)created. Default `true`. */
|
|
53
|
+
readonly autoStart?: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface UseDeferredTaskState<T> {
|
|
56
|
+
readonly status: ChunkStatus;
|
|
57
|
+
readonly error: unknown;
|
|
58
|
+
/** The returned value once the task settles with `done` — `undefined` until then. */
|
|
59
|
+
readonly value: T | undefined;
|
|
60
|
+
readonly isIdle: boolean;
|
|
61
|
+
readonly isProcessing: boolean;
|
|
62
|
+
readonly isPaused: boolean;
|
|
63
|
+
readonly isDone: boolean;
|
|
64
|
+
readonly isCancelled: boolean;
|
|
65
|
+
readonly isError: boolean;
|
|
66
|
+
readonly start: () => void;
|
|
67
|
+
readonly pause: () => void;
|
|
68
|
+
readonly resume: () => void;
|
|
69
|
+
readonly cancel: () => void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Runs an arbitrary heavy, synchronous generator function in chunks, so it
|
|
73
|
+
* never produces a long task on its own. Where `useChunkedTask` chunks "one
|
|
74
|
+
* function applied to every item of an array" automatically, this hook
|
|
75
|
+
* leaves the chunk boundaries to you — put `yield` wherever your function
|
|
76
|
+
* can safely pause (see `DeferredGenerator`'s doc comment for an example).
|
|
77
|
+
*
|
|
78
|
+
* `task` is only called when the effect (re)creates the underlying task, not
|
|
79
|
+
* on every render — list whatever it closes over in `deps`, the same way
|
|
80
|
+
* you would for `useMemo` or `useEffect`.
|
|
81
|
+
*/
|
|
82
|
+
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 };
|
|
84
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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";
|
|
2
|
+
interface UseChunkedTaskOptions<T, R> {
|
|
3
|
+
/** Max ms of uninterrupted work before yielding. Default 5. */
|
|
4
|
+
readonly timeSliceMs?: number;
|
|
5
|
+
/** Start processing automatically when the task is (re)created. Default `true`. */
|
|
6
|
+
readonly autoStart?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Called synchronously after each item is processed. This is the cheap
|
|
9
|
+
* way to consume results incrementally while the task is still running —
|
|
10
|
+
* write into a ref-backed buffer, a canvas, a chart, whatever you need —
|
|
11
|
+
* without paying for an array copy on every yield checkpoint.
|
|
12
|
+
*/
|
|
13
|
+
readonly onItem?: (result: R, item: T, index: number) => void;
|
|
14
|
+
}
|
|
15
|
+
interface UseChunkedTaskState<R> {
|
|
16
|
+
readonly status: ChunkStatus;
|
|
17
|
+
readonly processed: number;
|
|
18
|
+
readonly total: number;
|
|
19
|
+
readonly error: unknown;
|
|
20
|
+
/** The full results array once the task has settled — `undefined` until then. */
|
|
21
|
+
readonly results: readonly R[] | undefined;
|
|
22
|
+
readonly isIdle: boolean;
|
|
23
|
+
readonly isProcessing: boolean;
|
|
24
|
+
readonly isPaused: boolean;
|
|
25
|
+
readonly isDone: boolean;
|
|
26
|
+
readonly isCancelled: boolean;
|
|
27
|
+
readonly isError: boolean;
|
|
28
|
+
readonly start: () => void;
|
|
29
|
+
readonly pause: () => void;
|
|
30
|
+
readonly resume: () => void;
|
|
31
|
+
readonly cancel: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Chunks a heavy, synchronous per-item computation over `items` so it never
|
|
35
|
+
* blocks the main thread for longer than a time slice — the thing that
|
|
36
|
+
* costs you Interaction to Next Paint. Starts automatically by default;
|
|
37
|
+
* pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.
|
|
38
|
+
* behind a button).
|
|
39
|
+
*
|
|
40
|
+
* `processItem` and `onItem` can be fresh closures every render — the hook
|
|
41
|
+
* always calls the latest version (via `useEffectEvent`) without that
|
|
42
|
+
* causing the task to restart. What *does* restart the task is `items`
|
|
43
|
+
* changing identity (a new array reference), or `timeSliceMs`/`autoStart`
|
|
44
|
+
* changing. That's deliberate: a new `items` array means new work, and
|
|
45
|
+
* memoizing it when you don't want that is the caller's call to make, the
|
|
46
|
+
* same as any other React dependency.
|
|
47
|
+
*/
|
|
48
|
+
export declare function useChunkedTask<T, R>(items: readonly T[], processItem: (item: T, index: number) => R, options?: UseChunkedTaskOptions<T, R>): UseChunkedTaskState<R>;
|
|
49
|
+
interface UseDeferredTaskOptions {
|
|
50
|
+
/** Max ms of uninterrupted work before yielding. Default 5. */
|
|
51
|
+
readonly timeSliceMs?: number;
|
|
52
|
+
/** Start processing automatically when the task is (re)created. Default `true`. */
|
|
53
|
+
readonly autoStart?: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface UseDeferredTaskState<T> {
|
|
56
|
+
readonly status: ChunkStatus;
|
|
57
|
+
readonly error: unknown;
|
|
58
|
+
/** The returned value once the task settles with `done` — `undefined` until then. */
|
|
59
|
+
readonly value: T | undefined;
|
|
60
|
+
readonly isIdle: boolean;
|
|
61
|
+
readonly isProcessing: boolean;
|
|
62
|
+
readonly isPaused: boolean;
|
|
63
|
+
readonly isDone: boolean;
|
|
64
|
+
readonly isCancelled: boolean;
|
|
65
|
+
readonly isError: boolean;
|
|
66
|
+
readonly start: () => void;
|
|
67
|
+
readonly pause: () => void;
|
|
68
|
+
readonly resume: () => void;
|
|
69
|
+
readonly cancel: () => void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Runs an arbitrary heavy, synchronous generator function in chunks, so it
|
|
73
|
+
* never produces a long task on its own. Where `useChunkedTask` chunks "one
|
|
74
|
+
* function applied to every item of an array" automatically, this hook
|
|
75
|
+
* leaves the chunk boundaries to you — put `yield` wherever your function
|
|
76
|
+
* can safely pause (see `DeferredGenerator`'s doc comment for an example).
|
|
77
|
+
*
|
|
78
|
+
* `task` is only called when the effect (re)creates the underlying task, not
|
|
79
|
+
* on every render — list whatever it closes over in `deps`, the same way
|
|
80
|
+
* you would for `useMemo` or `useEffect`.
|
|
81
|
+
*/
|
|
82
|
+
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 };
|
|
84
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { n as createChunkScheduler, t as createDeferredTask } from "./deferred-task-5kfztF2G.js";
|
|
2
|
+
import { useCallback, useEffect, useEffectEvent, useMemo, useState, useSyncExternalStore } from "react";
|
|
3
|
+
// src/react/task-controller.ts
|
|
4
|
+
function createTaskController() {
|
|
5
|
+
let current = null;
|
|
6
|
+
let detachFromCurrent;
|
|
7
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
8
|
+
const notify = () => {
|
|
9
|
+
for (const listener of listeners) listener();
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
subscribe: (listener) => {
|
|
13
|
+
listeners.add(listener);
|
|
14
|
+
return () => {
|
|
15
|
+
listeners.delete(listener);
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
getSnapshot: () => current?.getSnapshot(),
|
|
19
|
+
getCurrent: () => current,
|
|
20
|
+
replace: (task) => {
|
|
21
|
+
detachFromCurrent?.();
|
|
22
|
+
current = task;
|
|
23
|
+
detachFromCurrent = task?.subscribe(notify);
|
|
24
|
+
notify();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// src/react/useChunkedTask.ts
|
|
29
|
+
const IDLE_STATUS = "idle";
|
|
30
|
+
/**
|
|
31
|
+
* Chunks a heavy, synchronous per-item computation over `items` so it never
|
|
32
|
+
* blocks the main thread for longer than a time slice — the thing that
|
|
33
|
+
* costs you Interaction to Next Paint. Starts automatically by default;
|
|
34
|
+
* pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.
|
|
35
|
+
* behind a button).
|
|
36
|
+
*
|
|
37
|
+
* `processItem` and `onItem` can be fresh closures every render — the hook
|
|
38
|
+
* always calls the latest version (via `useEffectEvent`) without that
|
|
39
|
+
* causing the task to restart. What *does* restart the task is `items`
|
|
40
|
+
* changing identity (a new array reference), or `timeSliceMs`/`autoStart`
|
|
41
|
+
* changing. That's deliberate: a new `items` array means new work, and
|
|
42
|
+
* memoizing it when you don't want that is the caller's call to make, the
|
|
43
|
+
* same as any other React dependency.
|
|
44
|
+
*/
|
|
45
|
+
function useChunkedTask(items, processItem, options) {
|
|
46
|
+
const { timeSliceMs, autoStart = true, onItem } = options ?? {};
|
|
47
|
+
const stableProcessItem = useEffectEvent((item, index) => processItem(item, index));
|
|
48
|
+
const stableOnItem = useEffectEvent((result, item, index) => {
|
|
49
|
+
onItem?.(result, item, index);
|
|
50
|
+
});
|
|
51
|
+
const [controller] = useState(() => createTaskController());
|
|
52
|
+
const idleSnapshot = useMemo(() => ({
|
|
53
|
+
status: IDLE_STATUS,
|
|
54
|
+
processed: 0,
|
|
55
|
+
total: items.length
|
|
56
|
+
}), [items.length]);
|
|
57
|
+
const snapshot = useSyncExternalStore(controller.subscribe, () => controller.getSnapshot() ?? idleSnapshot, () => idleSnapshot);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const scheduler = createChunkScheduler({
|
|
60
|
+
items,
|
|
61
|
+
processItem: stableProcessItem,
|
|
62
|
+
onItem: stableOnItem,
|
|
63
|
+
...timeSliceMs !== void 0 ? { timeSliceMs } : {}
|
|
64
|
+
});
|
|
65
|
+
controller.replace(scheduler);
|
|
66
|
+
if (autoStart) scheduler.start();
|
|
67
|
+
return () => {
|
|
68
|
+
scheduler.cancel();
|
|
69
|
+
controller.replace(null);
|
|
70
|
+
};
|
|
71
|
+
}, [
|
|
72
|
+
items,
|
|
73
|
+
timeSliceMs,
|
|
74
|
+
autoStart,
|
|
75
|
+
controller
|
|
76
|
+
]);
|
|
77
|
+
const start = useCallback(() => controller.getCurrent()?.start(), [controller]);
|
|
78
|
+
const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);
|
|
79
|
+
const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);
|
|
80
|
+
const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);
|
|
81
|
+
const results = controller.getCurrent()?.getResults();
|
|
82
|
+
return {
|
|
83
|
+
status: snapshot.status,
|
|
84
|
+
processed: snapshot.processed,
|
|
85
|
+
total: snapshot.total,
|
|
86
|
+
error: snapshot.error,
|
|
87
|
+
results,
|
|
88
|
+
isIdle: snapshot.status === "idle",
|
|
89
|
+
isProcessing: snapshot.status === "processing",
|
|
90
|
+
isPaused: snapshot.status === "paused",
|
|
91
|
+
isDone: snapshot.status === "done",
|
|
92
|
+
isCancelled: snapshot.status === "cancelled",
|
|
93
|
+
isError: snapshot.status === "error",
|
|
94
|
+
start,
|
|
95
|
+
pause,
|
|
96
|
+
resume,
|
|
97
|
+
cancel
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// src/react/useDeferredTask.ts
|
|
101
|
+
const IDLE_SNAPSHOT = { status: "idle" };
|
|
102
|
+
/**
|
|
103
|
+
* Runs an arbitrary heavy, synchronous generator function in chunks, so it
|
|
104
|
+
* never produces a long task on its own. Where `useChunkedTask` chunks "one
|
|
105
|
+
* function applied to every item of an array" automatically, this hook
|
|
106
|
+
* leaves the chunk boundaries to you — put `yield` wherever your function
|
|
107
|
+
* can safely pause (see `DeferredGenerator`'s doc comment for an example).
|
|
108
|
+
*
|
|
109
|
+
* `task` is only called when the effect (re)creates the underlying task, not
|
|
110
|
+
* on every render — list whatever it closes over in `deps`, the same way
|
|
111
|
+
* you would for `useMemo` or `useEffect`.
|
|
112
|
+
*/
|
|
113
|
+
function useDeferredTask(task, deps, options) {
|
|
114
|
+
const { timeSliceMs, autoStart = true } = options ?? {};
|
|
115
|
+
const stableTask = useEffectEvent(() => task());
|
|
116
|
+
const [controller] = useState(() => createTaskController());
|
|
117
|
+
const snapshot = useSyncExternalStore(controller.subscribe, () => controller.getSnapshot() ?? IDLE_SNAPSHOT, () => IDLE_SNAPSHOT);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const deferredTask = createDeferredTask({
|
|
120
|
+
task: stableTask,
|
|
121
|
+
...timeSliceMs !== void 0 ? { timeSliceMs } : {}
|
|
122
|
+
});
|
|
123
|
+
controller.replace(deferredTask);
|
|
124
|
+
if (autoStart) deferredTask.start();
|
|
125
|
+
return () => {
|
|
126
|
+
deferredTask.cancel();
|
|
127
|
+
controller.replace(null);
|
|
128
|
+
};
|
|
129
|
+
}, [
|
|
130
|
+
...deps,
|
|
131
|
+
timeSliceMs,
|
|
132
|
+
autoStart,
|
|
133
|
+
controller
|
|
134
|
+
]);
|
|
135
|
+
const start = useCallback(() => controller.getCurrent()?.start(), [controller]);
|
|
136
|
+
const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);
|
|
137
|
+
const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);
|
|
138
|
+
const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);
|
|
139
|
+
const value = controller.getCurrent()?.getValue();
|
|
140
|
+
return {
|
|
141
|
+
status: snapshot.status,
|
|
142
|
+
error: snapshot.error,
|
|
143
|
+
value,
|
|
144
|
+
isIdle: snapshot.status === "idle",
|
|
145
|
+
isProcessing: snapshot.status === "processing",
|
|
146
|
+
isPaused: snapshot.status === "paused",
|
|
147
|
+
isDone: snapshot.status === "done",
|
|
148
|
+
isCancelled: snapshot.status === "cancelled",
|
|
149
|
+
isError: snapshot.status === "error",
|
|
150
|
+
start,
|
|
151
|
+
pause,
|
|
152
|
+
resume,
|
|
153
|
+
cancel
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export { useChunkedTask, useDeferredTask };
|
|
157
|
+
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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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"}
|