react-inp-chunker 0.1.2 → 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.
- package/README.md +141 -53
- package/core/package.json +10 -0
- package/dist/core/index.cjs +1 -1
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/deferred-task-CwjZA3K-.js +713 -0
- package/dist/deferred-task-CwjZA3K-.js.map +1 -0
- package/dist/deferred-task-EWH55ZyL.cjs +796 -0
- package/dist/deferred-task-EWH55ZyL.cjs.map +1 -0
- package/dist/{index-C6B_Nm3i.d.ts → index-DShDP2VN.d.cts} +51 -6
- package/dist/index-DShDP2VN.d.cts.map +1 -0
- package/dist/{index-C6B_Nm3i.d.cts → index-DShDP2VN.d.ts} +51 -6
- package/dist/index-DShDP2VN.d.ts.map +1 -0
- package/dist/index.cjs +47 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -10
- package/dist/index.js.map +1 -1
- package/dist/production/core/index.cjs +16 -0
- package/dist/production/core/index.js +1 -0
- package/dist/{deferred-task-5kfztF2G.js → production/deferred-task-BGsfee-L.js} +177 -86
- package/dist/production/deferred-task-BGsfee-L.js.map +1 -0
- package/dist/{deferred-task-BXVzapQg.cjs → production/deferred-task-zVnq18IN.cjs} +177 -86
- package/dist/production/deferred-task-zVnq18IN.cjs.map +1 -0
- package/dist/production/index.cjs +195 -0
- package/dist/production/index.cjs.map +1 -0
- package/dist/production/index.js +172 -0
- package/dist/production/index.js.map +1 -0
- package/package.json +65 -22
- package/dist/deferred-task-5kfztF2G.js.map +0 -1
- package/dist/deferred-task-BXVzapQg.cjs.map +0 -1
- package/dist/index-C6B_Nm3i.d.cts.map +0 -1
- package/dist/index-C6B_Nm3i.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -3,26 +3,42 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-inp-chunker)
|
|
4
4
|
[](https://github.com/suprabhat02/react-inp-chunker/actions/workflows/ci.yml)
|
|
5
5
|
|
|
6
|
-
**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
49
|
+
Works with React 18 and 19; CI runs the full test suite on both.
|
|
34
50
|
|
|
35
51
|
## Usage
|
|
36
52
|
|
|
37
|
-
###
|
|
53
|
+
### Process a list: `useChunkedTask`
|
|
38
54
|
|
|
39
55
|
```tsx
|
|
40
56
|
import { useChunkedTask } from 'react-inp-chunker';
|
|
41
57
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
68
|
+
return <ResultsTable scores={task.results} />;
|
|
55
69
|
}
|
|
56
70
|
```
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`
|
|
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
|
-
###
|
|
99
|
+
### Work that isn't a list: `useDeferredTask`
|
|
65
100
|
|
|
66
|
-
For
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}
|
package/dist/core/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_deferred_task = require("../deferred-task-
|
|
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;
|
package/dist/core/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as yieldToMain, D as
|
|
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 };
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as yieldToMain, D as
|
|
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 };
|
package/dist/core/index.js
CHANGED
|
@@ -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-
|
|
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";
|