react-inp-chunker 0.1.1 → 0.2.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 +141 -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-CwjZA3K-.js +713 -0
  8. package/dist/deferred-task-CwjZA3K-.js.map +1 -0
  9. package/dist/deferred-task-EWH55ZyL.cjs +796 -0
  10. package/dist/deferred-task-EWH55ZyL.cjs.map +1 -0
  11. package/dist/{index-C6B_Nm3i.d.ts → index-DShDP2VN.d.cts} +51 -6
  12. package/dist/index-DShDP2VN.d.cts.map +1 -0
  13. package/dist/{index-C6B_Nm3i.d.cts → index-DShDP2VN.d.ts} +51 -6
  14. package/dist/index-DShDP2VN.d.ts.map +1 -0
  15. package/dist/index.cjs +47 -12
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +20 -1
  18. package/dist/index.d.cts.map +1 -1
  19. package/dist/index.d.ts +20 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +24 -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-BGsfee-L.js} +177 -86
  26. package/dist/production/deferred-task-BGsfee-L.js.map +1 -0
  27. package/dist/{deferred-task-BXVzapQg.cjs → production/deferred-task-zVnq18IN.cjs} +177 -86
  28. package/dist/production/deferred-task-zVnq18IN.cjs.map +1 -0
  29. package/dist/production/index.cjs +195 -0
  30. package/dist/production/index.cjs.map +1 -0
  31. package/dist/production/index.js +172 -0
  32. package/dist/production/index.js.map +1 -0
  33. package/package.json +65 -22
  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/README.md CHANGED
@@ -3,26 +3,42 @@
3
3
  [![npm version](https://img.shields.io/npm/v/react-inp-chunker.svg)](https://www.npmjs.com/package/react-inp-chunker)
4
4
  [![CI](https://github.com/suprabhat02/react-inp-chunker/actions/workflows/ci.yml/badge.svg)](https://github.com/suprabhat02/react-inp-chunker/actions/workflows/ci.yml)
5
5
 
6
- **Headless React/TypeScript hooks that chunk heavy synchronous work and yield to
7
- the main thread, so a long update never costs you Interaction to Next Paint (INP).**
8
-
9
- Processing a big array, running a multi-phase algorithm, or rendering a large
10
- canvas frame-by-frame inside a React update blocks the main thread for as long
11
- as it takes — every click, keypress, and tap queued behind it waits too. This
12
- package breaks that work into small slices and hands control back to the
13
- browser between them, using the best yielding primitive the current runtime
14
- actually has:
15
-
16
- 1. [`scheduler.yield()`](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield) —
17
- the purpose-built API, Baseline across Chromium and Firefox.
18
- 2. `MessageChannel` — not clamped to the ~4ms floor nested `setTimeout` calls
19
- get, so it yields sooner.
20
- 3. `setTimeout(0)` — the universal fallback.
21
-
22
- No dependencies beyond React itself. Nothing here touches the DOM directly, so
23
- the scheduling engine (`react-inp-chunker/core`) works just as well outside
24
- React — a Node script, a Web Worker, anywhere a long synchronous task would
25
- otherwise block something else.
6
+ **Run heavy JavaScript without freezing your React app.** React/TypeScript hooks
7
+ that split long-running work into slices of a few milliseconds and hand the main
8
+ thread back to the browser between them, so clicks and typing stay responsive and
9
+ Interaction to Next Paint (INP) stays good.
10
+
11
+ **[Documentation and live playground](https://suprabhat02.github.io/react-inp-chunker/)**:
12
+ run the same work with and without chunking, type while it runs, and see the
13
+ difference measured in your own browser.
14
+
15
+ ## The problem
16
+
17
+ One thread runs your JavaScript, handles every click and key press, and paints the
18
+ screen. While a function runs, everything else waits. Filter 20,000 rows, parse a
19
+ large CSV or build a search index inside a React component, and a click in that
20
+ window sits in a queue until the function returns. Any task over 50 ms is a _long
21
+ task_, and an interaction that lands in one is the most common way to fail
22
+ [INP](https://web.dev/articles/inp), the Core Web Vital for responsiveness.
23
+
24
+ ## What this does
25
+
26
+ It doesn't make the work faster. It makes sure the work never holds the main
27
+ thread for more than a few milliseconds at a time: process items until the time
28
+ slice (5 ms by default) is used up, yield so the browser can handle input and
29
+ paint, then carry on where it stopped. It yields with the best tool the browser
30
+ has:
31
+
32
+ 1. [`scheduler.yield()`](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield)
33
+ in Chrome 129+, Edge 129+ and Firefox 142+. Made for exactly this: the work
34
+ resumes ahead of other queued tasks.
35
+ 2. `MessageChannel` in Safari and older browsers. Not clamped to the ~4 ms floor
36
+ that nested `setTimeout` calls get.
37
+ 3. `setTimeout(0)` as the last resort.
38
+
39
+ No dependencies beyond React. The scheduling engine (`react-inp-chunker/core`)
40
+ doesn't import React at all, so it also works in plain TypeScript, a Web Worker or
41
+ a Node script.
26
42
 
27
43
  ## Install
28
44
 
@@ -30,47 +46,65 @@ otherwise block something else.
30
46
  npm install react-inp-chunker
31
47
  ```
32
48
 
33
- Requires React 18 or 19 as a peer dependency.
49
+ Works with React 18 and 19; CI runs the full test suite on both.
34
50
 
35
51
  ## Usage
36
52
 
37
- ### Chunking an array — `useChunkedTask`
53
+ ### Process a list: `useChunkedTask`
38
54
 
39
55
  ```tsx
40
56
  import { useChunkedTask } from 'react-inp-chunker';
41
57
 
42
- function RowScorer({ rows }: { rows: Row[] }) {
43
- const { status, processed, total, results, isDone, pause, resume, cancel } =
44
- useChunkedTask(rows, (row) => expensiveScore(row));
58
+ function ScoredRows({ rows }: { rows: readonly Row[] }) {
59
+ // scoreRow runs once per row, in slices of up to 5 ms.
60
+ const task = useChunkedTask(rows, scoreRow);
45
61
 
46
- if (!isDone) {
47
- return (
48
- <p>
49
- Scoring… {processed}/{total}
50
- </p>
51
- );
62
+ if (task.isError) return <p role="alert">Couldn't score the rows.</p>;
63
+
64
+ if (!task.isDone || task.results === undefined) {
65
+ return <progress value={task.processed} max={task.total} aria-label="Scoring rows" />;
52
66
  }
53
67
 
54
- return <ResultsTable rows={results} />;
68
+ return <ResultsTable scores={task.results} />;
55
69
  }
56
70
  ```
57
71
 
58
- `processItem` is called once per item, each call in its own chunk, yielding to
59
- the browser whenever a time slice (5ms by default) runs out or the browser
60
- reports pending input. Pass a fresh closure every render — it's always the
61
- latest one that runs, without restarting the task. What _does_ restart it is
62
- `rows` becoming a new array reference, or `timeSliceMs`/`autoStart` changing.
72
+ Items are processed back to back until the time slice runs out, or until the
73
+ browser reports pending input; then the hook yields, and resumes with the next
74
+ item. Your component re-renders with progress once per slice, not once per item.
75
+
76
+ - `scoreRow` can be a fresh closure every render: the latest one always runs,
77
+ without restarting the task.
78
+ - What _does_ restart it is `rows` becoming a new array reference, or
79
+ `timeSliceMs`/`autoStart` changing. Memoize derived arrays.
80
+ - Pass `{ autoStart: false }` to wait for `start()`, and `onItem` to consume
81
+ results while the task is still running. `pause()`, `resume()` and `cancel()`
82
+ work at any point, and unmounting cancels.
83
+ - `results` is `undefined` until the task settles, and is then set in the same
84
+ render that first reports `done`, `cancelled` or `error` — never a render
85
+ later, so there is nothing to tear under concurrent React.
86
+
87
+ Two options exist for work at the extremes, and most code needs neither:
88
+
89
+ | Option | Default | Use it when |
90
+ | ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `batchSize: number` | `1` | An item takes nanoseconds, so reading the clock costs more than the work. A slice may then overrun its budget by up to `batchSize` items — that is the whole trade. |
92
+ | `collectResults: boolean` | `true` | You already take results through `onItem` and don't want a million-entry array retained. `results` settles as an empty array. |
93
+
94
+ In development, the library measures itself and says so: a single item that
95
+ blocks for over 50 ms gets a warning naming its index, and work cheap enough that
96
+ the clock dominates gets a concrete `batchSize` suggestion. Both are compiled out
97
+ of production builds.
63
98
 
64
- ### Chunking arbitrary work — `useDeferredTask`
99
+ ### Work that isn't a list: `useDeferredTask`
65
100
 
66
- For work that isn't "one function applied to every item of an array" — a
67
- multi-phase algorithm, a big canvas render — mark your own safe-to-pause
68
- checkpoints with a generator:
101
+ For a long computation that isn't one function over an array, write it as a
102
+ generator and put `yield` wherever it's safe to pause:
69
103
 
70
104
  ```tsx
71
- import { useDeferredTask } from 'react-inp-chunker';
105
+ import { type DeferredGenerator, useDeferredTask } from 'react-inp-chunker';
72
106
 
73
- function* renderFrames(frameCount: number) {
107
+ function* renderFrames(frameCount: number): DeferredGenerator<number> {
74
108
  for (let i = 0; i < frameCount; i++) {
75
109
  renderOneExpensiveFrame(i);
76
110
  yield; // "it's safe to pause here if we're over budget"
@@ -84,7 +118,7 @@ function FrameRenderer({ frameCount }: { frameCount: number }) {
84
118
  }
85
119
  ```
86
120
 
87
- ### Outside React — `react-inp-chunker/core`
121
+ ### Outside React: `react-inp-chunker/core`
88
122
 
89
123
  ```ts
90
124
  import { createChunkScheduler } from 'react-inp-chunker/core';
@@ -98,17 +132,71 @@ const { status, results } = await scheduler.settled;
98
132
  ```
99
133
 
100
134
  Both engines expose `start()`, `pause()`, `resume()`, `cancel()`, a
101
- `subscribe()`/`getSnapshot()` pair, and a `settled` promise that always
102
- resolves — never rejects — with `'done' | 'cancelled' | 'error'`. An
103
- `AbortSignal` can drive cancellation too.
135
+ `subscribe()`/`getSnapshot()` pair, and a `settled` promise that always resolves
136
+ (never rejects) with `'done' | 'cancelled' | 'error'`. An `AbortSignal` can drive
137
+ cancellation too.
138
+
139
+ The [documentation](https://suprabhat02.github.io/react-inp-chunker/#api) has the
140
+ full API, when to reach for something else (a Web Worker, `startTransition`,
141
+ virtualization), and answers to common questions.
142
+
143
+ ## In production
144
+
145
+ | What | Supported | Proven by |
146
+ | ---------------- | ------------------------------ | -------------------------------------------------------- |
147
+ | React | 18 and 19 | the whole test suite runs on both majors |
148
+ | Node | 18, 20, 22, 24 | the packed tarball is imported and required on each |
149
+ | TypeScript | 5.0 to 7 | the published declarations compile at both ends |
150
+ | Module systems | ESM and CommonJS | separate builds and declarations, both loaded in CI |
151
+ | Resolution | bundler, NodeNext, node10 | `publint` and `are-the-types-wrong` run over the tarball |
152
+ | Browsers | anything with `MessageChannel` | yielding measured in real Chromium and WebKit |
153
+ | Server rendering | yes | both hooks render an idle state and run nothing |
154
+
155
+ Dropping any of these is a breaking change; [SUPPORT.md](SUPPORT.md) has the
156
+ versioning rules.
157
+
158
+ **Size**, minified and brotli-compressed, with no dependencies of its own:
159
+ `useChunkedTask` 1.83 kB, `useDeferredTask` 1.72 kB, `createChunkScheduler`
160
+ without React 1.34 kB, everything together 2.35 kB. CI fails if a change pushes
161
+ any of them over budget.
162
+
163
+ Bundlers that set the `production` export condition — webpack, Vite, Rspack,
164
+ Parcel — resolve to a build with the development warnings compiled out, not
165
+ merely guarded by a flag.
166
+
167
+ **What it guarantees.** A slice overruns by at most one item (or `batchSize`
168
+ items, if you raised it), because the clock is read after every item rather than
169
+ on an average. A settled task never reports its results a render late. Nothing
170
+ runs after `cancel()`, no state update arrives after unmount, and `settled`
171
+ resolves exactly once and never rejects. The loop allocates nothing per item.
172
+
173
+ **Supply chain.** Published from CI through npm Trusted Publishing, so no
174
+ long-lived token exists to leak, with signed provenance you can verify yourself:
175
+
176
+ ```sh
177
+ npm audit signatures
178
+ gh attestation verify "$(npm pack react-inp-chunker --silent)" \
179
+ -R suprabhat02/react-inp-chunker
180
+ ```
181
+
182
+ GitHub Actions are pinned to commit SHAs, CI fails on a known vulnerability at
183
+ any severity, and there are no runtime dependencies to audit.
184
+
185
+ ## Quality
104
186
 
105
- ## Status
187
+ An exhaustive test suite (state-machine truth table, every yield fallback path,
188
+ slice bounds on a virtual clock, property tests, pause/resume/cancel races,
189
+ reentrancy, SSR safety) at 100% statement, branch, function and line coverage,
190
+ run on React 19 and React 18. On top of that, every push checks the packed
191
+ tarball end to end: imported and required on four Node versions, compiled by two
192
+ TypeScript majors, linted by `publint`, resolved by `are-the-types-wrong`,
193
+ measured for real input delay in Chromium and WebKit, and diffed against a
194
+ golden public API surface so no export changes by accident. See
195
+ [CONTRIBUTING.md](CONTRIBUTING.md) for how the project is organised and verified.
106
196
 
107
- Phase 1 — the core scheduling engine, both hooks, and an exhaustive test
108
- suite (state-machine truth table, every yield fallback path, pause/resume/
109
- cancel race conditions, reentrancy, SSR safety) at 100% statement, branch,
110
- function, and line coverage. See [CONTRIBUTING.md](CONTRIBUTING.md) for how
111
- the project is organised and verified.
197
+ If react-inp-chunker saves you a long task, a
198
+ [star on GitHub](https://github.com/suprabhat02/react-inp-chunker) helps other
199
+ developers find it.
112
200
 
113
201
  ## License
114
202
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "react-inp-chunker-core",
3
+ "description": "Resolution shim for `react-inp-chunker/core` under TypeScript's classic (node10) module resolution, which predates the exports map and looks for a directory of this name. Modern resolvers read the exports map in the root package.json and never see this file. Keep the paths below in step with that map.",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "../dist/core/index.cjs",
7
+ "module": "../dist/core/index.js",
8
+ "types": "../dist/core/index.d.ts",
9
+ "sideEffects": false
10
+ }
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_deferred_task = require("../deferred-task-BXVzapQg.cjs");
2
+ const require_deferred_task = require("../deferred-task-EWH55ZyL.cjs");
3
3
  exports.DEFAULT_TIME_SLICE_MS = require_deferred_task.DEFAULT_TIME_SLICE_MS;
4
4
  exports.createChunkScheduler = require_deferred_task.createChunkScheduler;
5
5
  exports.createDeferredTask = require_deferred_task.createDeferredTask;
@@ -1,2 +1,2 @@
1
- import { C as yieldToMain, D as initialChunkState, E as ChunkStatus, O as isTerminal, S as supportsSchedulerYield, T as ChunkMachineState, _ as now, a as DeferredTaskSnapshot, b as supportsInputPending, c as ChunkSchedulerOptions, d as createChunkScheduler, f as DEFAULT_TIME_SLICE_MS, g as isInputPending, h as YieldToMainOptions, i as DeferredTaskResult, k as transition, l as ChunkSchedulerResult, m as YieldStrategy, n as DeferredTask, o as createDeferredTask, p as ShouldYieldOptions, r as DeferredTaskOptions, s as ChunkScheduler, t as DeferredGenerator, u as ChunkSnapshot, v as resolveYieldStrategy, w as ChunkEvent, x as supportsMessageChannel, y as shouldYieldNow } from "../index-C6B_Nm3i.cjs";
2
- export { type ChunkEvent, type ChunkMachineState, type ChunkScheduler, type ChunkSchedulerOptions, type ChunkSchedulerResult, type ChunkSnapshot, type ChunkStatus, DEFAULT_TIME_SLICE_MS, type DeferredGenerator, type DeferredTask, type DeferredTaskOptions, type DeferredTaskResult, type DeferredTaskSnapshot, type ShouldYieldOptions, type YieldStrategy, type YieldToMainOptions, createChunkScheduler, createDeferredTask, initialChunkState, isInputPending, isTerminal, now, resolveYieldStrategy, shouldYieldNow, supportsInputPending, supportsMessageChannel, supportsSchedulerYield, transition, yieldToMain };
1
+ import { A as transition, C as yieldToMain, D as TerminalChunkStatus, E as ChunkStatus, O as initialChunkState, S as supportsSchedulerYield, T as ChunkMachineState, _ as now, a as DeferredTaskSnapshot, b as supportsInputPending, c as ChunkSchedulerOptions, d as createChunkScheduler, f as DEFAULT_TIME_SLICE_MS, g as isInputPending, h as YieldToMainOptions, i as DeferredTaskResult, k as isTerminal, l as ChunkSchedulerResult, m as YieldStrategy, n as DeferredTask, o as createDeferredTask, p as ShouldYieldOptions, r as DeferredTaskOptions, s as ChunkScheduler, t as DeferredGenerator, u as ChunkSnapshot, v as resolveYieldStrategy, w as ChunkEvent, x as supportsMessageChannel, y as shouldYieldNow } from "../index-DShDP2VN.cjs";
2
+ export { type ChunkEvent, type ChunkMachineState, type ChunkScheduler, type ChunkSchedulerOptions, type ChunkSchedulerResult, type ChunkSnapshot, type ChunkStatus, DEFAULT_TIME_SLICE_MS, type DeferredGenerator, type DeferredTask, type DeferredTaskOptions, type DeferredTaskResult, type DeferredTaskSnapshot, type ShouldYieldOptions, type TerminalChunkStatus, type YieldStrategy, type YieldToMainOptions, createChunkScheduler, createDeferredTask, initialChunkState, isInputPending, isTerminal, now, resolveYieldStrategy, shouldYieldNow, supportsInputPending, supportsMessageChannel, supportsSchedulerYield, transition, yieldToMain };
@@ -1,2 +1,2 @@
1
- import { C as yieldToMain, D as initialChunkState, E as ChunkStatus, O as isTerminal, S as supportsSchedulerYield, T as ChunkMachineState, _ as now, a as DeferredTaskSnapshot, b as supportsInputPending, c as ChunkSchedulerOptions, d as createChunkScheduler, f as DEFAULT_TIME_SLICE_MS, g as isInputPending, h as YieldToMainOptions, i as DeferredTaskResult, k as transition, l as ChunkSchedulerResult, m as YieldStrategy, n as DeferredTask, o as createDeferredTask, p as ShouldYieldOptions, r as DeferredTaskOptions, s as ChunkScheduler, t as DeferredGenerator, u as ChunkSnapshot, v as resolveYieldStrategy, w as ChunkEvent, x as supportsMessageChannel, y as shouldYieldNow } from "../index-C6B_Nm3i.js";
2
- export { type ChunkEvent, type ChunkMachineState, type ChunkScheduler, type ChunkSchedulerOptions, type ChunkSchedulerResult, type ChunkSnapshot, type ChunkStatus, DEFAULT_TIME_SLICE_MS, type DeferredGenerator, type DeferredTask, type DeferredTaskOptions, type DeferredTaskResult, type DeferredTaskSnapshot, type ShouldYieldOptions, type YieldStrategy, type YieldToMainOptions, createChunkScheduler, createDeferredTask, initialChunkState, isInputPending, isTerminal, now, resolveYieldStrategy, shouldYieldNow, supportsInputPending, supportsMessageChannel, supportsSchedulerYield, transition, yieldToMain };
1
+ import { A as transition, C as yieldToMain, D as TerminalChunkStatus, E as ChunkStatus, O as initialChunkState, S as supportsSchedulerYield, T as ChunkMachineState, _ as now, a as DeferredTaskSnapshot, b as supportsInputPending, c as ChunkSchedulerOptions, d as createChunkScheduler, f as DEFAULT_TIME_SLICE_MS, g as isInputPending, h as YieldToMainOptions, i as DeferredTaskResult, k as isTerminal, l as ChunkSchedulerResult, m as YieldStrategy, n as DeferredTask, o as createDeferredTask, p as ShouldYieldOptions, r as DeferredTaskOptions, s as ChunkScheduler, t as DeferredGenerator, u as ChunkSnapshot, v as resolveYieldStrategy, w as ChunkEvent, x as supportsMessageChannel, y as shouldYieldNow } from "../index-DShDP2VN.js";
2
+ export { type ChunkEvent, type ChunkMachineState, type ChunkScheduler, type ChunkSchedulerOptions, type ChunkSchedulerResult, type ChunkSnapshot, type ChunkStatus, DEFAULT_TIME_SLICE_MS, type DeferredGenerator, type DeferredTask, type DeferredTaskOptions, type DeferredTaskResult, type DeferredTaskSnapshot, type ShouldYieldOptions, type TerminalChunkStatus, type YieldStrategy, type YieldToMainOptions, createChunkScheduler, createDeferredTask, initialChunkState, isInputPending, isTerminal, now, resolveYieldStrategy, shouldYieldNow, supportsInputPending, supportsMessageChannel, supportsSchedulerYield, transition, yieldToMain };
@@ -1 +1 @@
1
- export { r as DEFAULT_TIME_SLICE_MS, n as createChunkScheduler, t as createDeferredTask, f as initialChunkState, i as isInputPending, p as isTerminal, a as now, o as resolveYieldStrategy, s as shouldYieldNow, c as supportsInputPending, l as supportsMessageChannel, u as supportsSchedulerYield, m as transition, d as yieldToMain } from "../deferred-task-5kfztF2G.js";
1
+ export { r as DEFAULT_TIME_SLICE_MS, n as createChunkScheduler, t as createDeferredTask, f as initialChunkState, i as isInputPending, p as isTerminal, a as now, o as resolveYieldStrategy, s as shouldYieldNow, c as supportsInputPending, l as supportsMessageChannel, u as supportsSchedulerYield, m as transition, d as yieldToMain } from "../deferred-task-CwjZA3K-.js";