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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suprabhat Kumar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,115 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-inp-chunker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/react-inp-chunker)
|
|
4
|
+
[](https://github.com/suprabhat02/react-inp-chunker/actions/workflows/ci.yml)
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install react-inp-chunker
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires React 18 or 19 as a peer dependency.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Chunking an array — `useChunkedTask`
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { useChunkedTask } from 'react-inp-chunker';
|
|
41
|
+
|
|
42
|
+
function RowScorer({ rows }: { rows: Row[] }) {
|
|
43
|
+
const { status, processed, total, results, isDone, pause, resume, cancel } =
|
|
44
|
+
useChunkedTask(rows, (row) => expensiveScore(row));
|
|
45
|
+
|
|
46
|
+
if (!isDone) {
|
|
47
|
+
return (
|
|
48
|
+
<p>
|
|
49
|
+
Scoring… {processed}/{total}
|
|
50
|
+
</p>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return <ResultsTable rows={results} />;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
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.
|
|
63
|
+
|
|
64
|
+
### Chunking arbitrary work — `useDeferredTask`
|
|
65
|
+
|
|
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:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useDeferredTask } from 'react-inp-chunker';
|
|
72
|
+
|
|
73
|
+
function* renderFrames(frameCount: number) {
|
|
74
|
+
for (let i = 0; i < frameCount; i++) {
|
|
75
|
+
renderOneExpensiveFrame(i);
|
|
76
|
+
yield; // "it's safe to pause here if we're over budget"
|
|
77
|
+
}
|
|
78
|
+
return frameCount;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function FrameRenderer({ frameCount }: { frameCount: number }) {
|
|
82
|
+
const { isDone, value } = useDeferredTask(() => renderFrames(frameCount), [frameCount]);
|
|
83
|
+
return isDone ? <p>Rendered {value} frames.</p> : <p>Rendering…</p>;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Outside React — `react-inp-chunker/core`
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createChunkScheduler } from 'react-inp-chunker/core';
|
|
91
|
+
|
|
92
|
+
const scheduler = createChunkScheduler({
|
|
93
|
+
items: rows,
|
|
94
|
+
processItem: (row) => expensiveScore(row),
|
|
95
|
+
});
|
|
96
|
+
scheduler.start();
|
|
97
|
+
const { status, results } = await scheduler.settled;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
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.
|
|
104
|
+
|
|
105
|
+
## Status
|
|
106
|
+
|
|
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.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT © [Suprabhat Kumar](https://suprabhat-dev.com)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_deferred_task = require("../deferred-task-BXVzapQg.cjs");
|
|
3
|
+
exports.DEFAULT_TIME_SLICE_MS = require_deferred_task.DEFAULT_TIME_SLICE_MS;
|
|
4
|
+
exports.createChunkScheduler = require_deferred_task.createChunkScheduler;
|
|
5
|
+
exports.createDeferredTask = require_deferred_task.createDeferredTask;
|
|
6
|
+
exports.initialChunkState = require_deferred_task.initialChunkState;
|
|
7
|
+
exports.isInputPending = require_deferred_task.isInputPending;
|
|
8
|
+
exports.isTerminal = require_deferred_task.isTerminal;
|
|
9
|
+
exports.now = require_deferred_task.now;
|
|
10
|
+
exports.resolveYieldStrategy = require_deferred_task.resolveYieldStrategy;
|
|
11
|
+
exports.shouldYieldNow = require_deferred_task.shouldYieldNow;
|
|
12
|
+
exports.supportsInputPending = require_deferred_task.supportsInputPending;
|
|
13
|
+
exports.supportsMessageChannel = require_deferred_task.supportsMessageChannel;
|
|
14
|
+
exports.supportsSchedulerYield = require_deferred_task.supportsSchedulerYield;
|
|
15
|
+
exports.transition = require_deferred_task.transition;
|
|
16
|
+
exports.yieldToMain = require_deferred_task.yieldToMain;
|
|
@@ -0,0 +1,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 };
|
|
@@ -0,0 +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 };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { r as DEFAULT_TIME_SLICE_MS, n as createChunkScheduler, t as createDeferredTask, f as initialChunkState, i as isInputPending, p as isTerminal, a as now, o as resolveYieldStrategy, s as shouldYieldNow, c as supportsInputPending, l as supportsMessageChannel, u as supportsSchedulerYield, m as transition, d as yieldToMain } from "../deferred-task-5kfztF2G.js";
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
// src/core/chunk-machine.ts
|
|
2
|
+
const initialChunkState = { status: "idle" };
|
|
3
|
+
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
4
|
+
"done",
|
|
5
|
+
"cancelled",
|
|
6
|
+
"error"
|
|
7
|
+
]);
|
|
8
|
+
function isTerminal(status) {
|
|
9
|
+
return TERMINAL_STATUSES.has(status);
|
|
10
|
+
}
|
|
11
|
+
function transitionIdle(state, event) {
|
|
12
|
+
switch (event.type) {
|
|
13
|
+
case "START": return { status: "processing" };
|
|
14
|
+
case "CANCEL": return { status: "cancelled" };
|
|
15
|
+
case "PAUSE":
|
|
16
|
+
case "RESUME":
|
|
17
|
+
case "COMPLETE":
|
|
18
|
+
case "ERROR": return state;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function transitionProcessing(state, event) {
|
|
22
|
+
switch (event.type) {
|
|
23
|
+
case "PAUSE": return { status: "paused" };
|
|
24
|
+
case "CANCEL": return { status: "cancelled" };
|
|
25
|
+
case "COMPLETE": return { status: "done" };
|
|
26
|
+
case "ERROR": return {
|
|
27
|
+
status: "error",
|
|
28
|
+
error: event.error
|
|
29
|
+
};
|
|
30
|
+
case "START":
|
|
31
|
+
case "RESUME": return state;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function transitionPaused(state, event) {
|
|
35
|
+
switch (event.type) {
|
|
36
|
+
case "RESUME": return { status: "processing" };
|
|
37
|
+
case "CANCEL": return { status: "cancelled" };
|
|
38
|
+
case "START":
|
|
39
|
+
case "PAUSE":
|
|
40
|
+
case "COMPLETE":
|
|
41
|
+
case "ERROR": return state;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Advances the machine by one event. Pure: same inputs always produce an
|
|
46
|
+
* equal output, and the input state is never mutated.
|
|
47
|
+
*/
|
|
48
|
+
function transition(state, event) {
|
|
49
|
+
switch (state.status) {
|
|
50
|
+
case "idle": return transitionIdle(state, event);
|
|
51
|
+
case "processing": return transitionProcessing(state, event);
|
|
52
|
+
case "paused": return transitionPaused(state, event);
|
|
53
|
+
case "done":
|
|
54
|
+
case "cancelled":
|
|
55
|
+
case "error": return state;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// src/core/scheduler.ts
|
|
59
|
+
/** Default time slice: roughly the granularity React's own scheduler uses. */
|
|
60
|
+
const DEFAULT_TIME_SLICE_MS = 5;
|
|
61
|
+
/** True when `scheduler.yield()` is present on this runtime. */
|
|
62
|
+
function supportsSchedulerYield() {
|
|
63
|
+
return typeof globalThis.scheduler?.yield === "function";
|
|
64
|
+
}
|
|
65
|
+
/** True when the `MessageChannel` constructor is present on this runtime. */
|
|
66
|
+
function supportsMessageChannel() {
|
|
67
|
+
return typeof MessageChannel === "function";
|
|
68
|
+
}
|
|
69
|
+
/** True when `navigator.scheduling.isInputPending()` is present. */
|
|
70
|
+
function supportsInputPending() {
|
|
71
|
+
return typeof globalThis.navigator?.scheduling?.isInputPending === "function";
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Best-effort read of whether the browser has pending input right now.
|
|
75
|
+
* Returns `false` — never throws — when the API is unavailable or errors,
|
|
76
|
+
* since the caller's fallback (a plain time budget) is always safe.
|
|
77
|
+
*/
|
|
78
|
+
function isInputPending() {
|
|
79
|
+
try {
|
|
80
|
+
return globalThis.navigator?.scheduling?.isInputPending?.() ?? false;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Picks the best strategy this runtime actually supports. */
|
|
86
|
+
function resolveYieldStrategy() {
|
|
87
|
+
if (supportsSchedulerYield()) return "scheduler-yield";
|
|
88
|
+
if (supportsMessageChannel()) return "message-channel";
|
|
89
|
+
return "set-timeout";
|
|
90
|
+
}
|
|
91
|
+
function yieldViaMessageChannel() {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const channel = new MessageChannel();
|
|
94
|
+
channel.port1.onmessage = () => {
|
|
95
|
+
resolve();
|
|
96
|
+
};
|
|
97
|
+
channel.port2.postMessage(null);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function yieldViaSetTimeout() {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
setTimeout(resolve, 0);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Yields control back to the browser's main thread, resolving once the
|
|
107
|
+
* browser has had a chance to handle any pending input, then resuming the
|
|
108
|
+
* caller. Never rejects: a runtime that half-implements `scheduler.yield()`
|
|
109
|
+
* (the property exists but the call throws, or the returned promise
|
|
110
|
+
* rejects) falls through to the next strategy rather than propagating the
|
|
111
|
+
* failure into unrelated application code.
|
|
112
|
+
*/
|
|
113
|
+
async function yieldToMain(options) {
|
|
114
|
+
const strategy = options?.strategy ?? resolveYieldStrategy();
|
|
115
|
+
if (strategy === "scheduler-yield") {
|
|
116
|
+
const schedulerGlobal = globalThis.scheduler;
|
|
117
|
+
if (typeof schedulerGlobal?.yield === "function") try {
|
|
118
|
+
await schedulerGlobal.yield();
|
|
119
|
+
return;
|
|
120
|
+
} catch {}
|
|
121
|
+
await (supportsMessageChannel() ? yieldViaMessageChannel() : yieldViaSetTimeout());
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (strategy === "message-channel") {
|
|
125
|
+
if (supportsMessageChannel()) {
|
|
126
|
+
await yieldViaMessageChannel();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await yieldViaSetTimeout();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await yieldViaSetTimeout();
|
|
133
|
+
}
|
|
134
|
+
/** `performance.now()` when available, `Date.now()` otherwise. Never throws. */
|
|
135
|
+
function now() {
|
|
136
|
+
try {
|
|
137
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
|
|
138
|
+
} catch {}
|
|
139
|
+
return Date.now();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether the caller should yield right now: either the time slice has run
|
|
143
|
+
* out, or the browser is reporting pending input (a stronger, earlier
|
|
144
|
+
* signal than the clock — used only when the runtime exposes it).
|
|
145
|
+
*/
|
|
146
|
+
function shouldYieldNow(options) {
|
|
147
|
+
const timeSliceMs = options.timeSliceMs ?? 5;
|
|
148
|
+
if (now() - options.sliceStartedAt >= timeSliceMs) return true;
|
|
149
|
+
return isInputPending();
|
|
150
|
+
}
|
|
151
|
+
// src/core/chunk-scheduler.ts
|
|
152
|
+
function normalizeTimeSliceMs$1(value) {
|
|
153
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 5;
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Builds a one-shot, controllable scheduler that processes `items` through
|
|
158
|
+
* `processItem`, yielding to the main thread on a time budget (and sooner,
|
|
159
|
+
* if the browser reports pending input) so a large array never produces a
|
|
160
|
+
* single long task.
|
|
161
|
+
*
|
|
162
|
+
* One-shot means exactly that: once the task reaches `done`, `cancelled` or
|
|
163
|
+
* `error`, this instance is finished — start()/pause()/resume() on it are
|
|
164
|
+
* no-ops from then on. To run another task, create another scheduler. For a
|
|
165
|
+
* React hook, that falls out naturally: new deps → new effect → new
|
|
166
|
+
* scheduler, which is the idiomatic React data-flow anyway.
|
|
167
|
+
*/
|
|
168
|
+
function createChunkScheduler(options) {
|
|
169
|
+
const { items, processItem, onItem, signal, yieldStrategy } = options;
|
|
170
|
+
const timeSliceMs = normalizeTimeSliceMs$1(options.timeSliceMs);
|
|
171
|
+
const total = items.length;
|
|
172
|
+
let machineState = initialChunkState;
|
|
173
|
+
let processed = 0;
|
|
174
|
+
let loopStarted = false;
|
|
175
|
+
let settledFlag = false;
|
|
176
|
+
const results = [];
|
|
177
|
+
let finalResults;
|
|
178
|
+
let snapshot = {
|
|
179
|
+
status: machineState.status,
|
|
180
|
+
processed,
|
|
181
|
+
total
|
|
182
|
+
};
|
|
183
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
184
|
+
let settleFn;
|
|
185
|
+
const settledPromise = new Promise((resolve) => {
|
|
186
|
+
settleFn = resolve;
|
|
187
|
+
});
|
|
188
|
+
let pauseResolve;
|
|
189
|
+
let pausePromise;
|
|
190
|
+
function waitForWake() {
|
|
191
|
+
pausePromise ??= new Promise((resolve) => {
|
|
192
|
+
pauseResolve = resolve;
|
|
193
|
+
});
|
|
194
|
+
return pausePromise;
|
|
195
|
+
}
|
|
196
|
+
function openGate() {
|
|
197
|
+
pauseResolve?.();
|
|
198
|
+
pauseResolve = void 0;
|
|
199
|
+
pausePromise = void 0;
|
|
200
|
+
}
|
|
201
|
+
function notify() {
|
|
202
|
+
for (const listener of listeners) listener();
|
|
203
|
+
}
|
|
204
|
+
function refreshSnapshot() {
|
|
205
|
+
snapshot = {
|
|
206
|
+
status: machineState.status,
|
|
207
|
+
processed,
|
|
208
|
+
total,
|
|
209
|
+
...machineState.error !== void 0 ? { error: machineState.error } : {}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function dispatch(event) {
|
|
213
|
+
const next = transition(machineState, event);
|
|
214
|
+
const changed = next !== machineState;
|
|
215
|
+
if (changed) {
|
|
216
|
+
machineState = next;
|
|
217
|
+
refreshSnapshot();
|
|
218
|
+
notify();
|
|
219
|
+
}
|
|
220
|
+
return { changed };
|
|
221
|
+
}
|
|
222
|
+
function settle(result) {
|
|
223
|
+
/* v8 ignore next */
|
|
224
|
+
if (settledFlag) return;
|
|
225
|
+
settledFlag = true;
|
|
226
|
+
detachSignal();
|
|
227
|
+
settleFn(result);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The single path every terminal transition goes through: snapshots the
|
|
231
|
+
* results array exactly once (not per item), publishes it via
|
|
232
|
+
* getResults(), and settles the public promise with the same reference —
|
|
233
|
+
* no redundant second copy.
|
|
234
|
+
*/
|
|
235
|
+
function finish(status, error) {
|
|
236
|
+
const resultsCopy = [...results];
|
|
237
|
+
finalResults = resultsCopy;
|
|
238
|
+
settle(status === "error" ? {
|
|
239
|
+
status,
|
|
240
|
+
error,
|
|
241
|
+
results: resultsCopy
|
|
242
|
+
} : {
|
|
243
|
+
status,
|
|
244
|
+
results: resultsCopy
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function settleIfNeverStarted() {
|
|
248
|
+
const { status, error } = machineState;
|
|
249
|
+
if (loopStarted || !isTerminal(status)) return;
|
|
250
|
+
loopStarted = true;
|
|
251
|
+
/* v8 ignore next */
|
|
252
|
+
if (status === "error") finish("error", error);
|
|
253
|
+
else finish(status);
|
|
254
|
+
}
|
|
255
|
+
function handleAbort() {
|
|
256
|
+
cancel();
|
|
257
|
+
}
|
|
258
|
+
function detachSignal() {
|
|
259
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
260
|
+
}
|
|
261
|
+
if (signal?.aborted) dispatch({ type: "CANCEL" });
|
|
262
|
+
else signal?.addEventListener("abort", handleAbort);
|
|
263
|
+
async function runLoop() {
|
|
264
|
+
let sliceStartedAt = now();
|
|
265
|
+
itemLoop: for (const [index, item] of items.entries()) {
|
|
266
|
+
for (;;) {
|
|
267
|
+
const status = machineState.status;
|
|
268
|
+
if (status === "cancelled") break itemLoop;
|
|
269
|
+
if (status !== "paused") break;
|
|
270
|
+
await waitForWake();
|
|
271
|
+
sliceStartedAt = now();
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const result = processItem(item, index);
|
|
275
|
+
results.push(result);
|
|
276
|
+
processed = index + 1;
|
|
277
|
+
refreshSnapshot();
|
|
278
|
+
onItem?.(result, item, index);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
dispatch({
|
|
281
|
+
type: "ERROR",
|
|
282
|
+
error
|
|
283
|
+
});
|
|
284
|
+
finish("error", error);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (shouldYieldNow({
|
|
288
|
+
sliceStartedAt,
|
|
289
|
+
timeSliceMs
|
|
290
|
+
})) {
|
|
291
|
+
await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
|
|
292
|
+
notify();
|
|
293
|
+
sliceStartedAt = now();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (machineState.status === "cancelled") {
|
|
297
|
+
finish("cancelled");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
dispatch({ type: "COMPLETE" });
|
|
301
|
+
finish("done");
|
|
302
|
+
}
|
|
303
|
+
function start() {
|
|
304
|
+
dispatch({ type: "START" });
|
|
305
|
+
if (machineState.status === "processing" && !loopStarted) {
|
|
306
|
+
loopStarted = true;
|
|
307
|
+
runLoop();
|
|
308
|
+
}
|
|
309
|
+
settleIfNeverStarted();
|
|
310
|
+
}
|
|
311
|
+
function pause() {
|
|
312
|
+
dispatch({ type: "PAUSE" });
|
|
313
|
+
}
|
|
314
|
+
function resume() {
|
|
315
|
+
const { changed } = dispatch({ type: "RESUME" });
|
|
316
|
+
if (changed) openGate();
|
|
317
|
+
}
|
|
318
|
+
function cancel() {
|
|
319
|
+
dispatch({ type: "CANCEL" });
|
|
320
|
+
openGate();
|
|
321
|
+
settleIfNeverStarted();
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
start,
|
|
325
|
+
pause,
|
|
326
|
+
resume,
|
|
327
|
+
cancel,
|
|
328
|
+
getSnapshot: () => snapshot,
|
|
329
|
+
getResults: () => finalResults,
|
|
330
|
+
subscribe(listener) {
|
|
331
|
+
listeners.add(listener);
|
|
332
|
+
return () => {
|
|
333
|
+
listeners.delete(listener);
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
settled: settledPromise
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// src/core/deferred-task.ts
|
|
340
|
+
function normalizeTimeSliceMs(value) {
|
|
341
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 5;
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The generator-driven sibling of `createChunkScheduler`, for heavy work
|
|
346
|
+
* that isn't shaped like "one function applied to each item of an array".
|
|
347
|
+
* Same state machine, same yield primitives, same one-shot-instance
|
|
348
|
+
* contract — see that file's doc comment for the reasoning, which applies
|
|
349
|
+
* here unchanged.
|
|
350
|
+
*/
|
|
351
|
+
function createDeferredTask(options) {
|
|
352
|
+
const { task, signal, yieldStrategy } = options;
|
|
353
|
+
const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
|
|
354
|
+
let machineState = initialChunkState;
|
|
355
|
+
let loopStarted = false;
|
|
356
|
+
let settledFlag = false;
|
|
357
|
+
let value;
|
|
358
|
+
let snapshot = { status: machineState.status };
|
|
359
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
360
|
+
let settleFn;
|
|
361
|
+
const settledPromise = new Promise((resolve) => {
|
|
362
|
+
settleFn = resolve;
|
|
363
|
+
});
|
|
364
|
+
let pauseResolve;
|
|
365
|
+
let pausePromise;
|
|
366
|
+
function waitForWake() {
|
|
367
|
+
pausePromise ??= new Promise((resolve) => {
|
|
368
|
+
pauseResolve = resolve;
|
|
369
|
+
});
|
|
370
|
+
return pausePromise;
|
|
371
|
+
}
|
|
372
|
+
function openGate() {
|
|
373
|
+
pauseResolve?.();
|
|
374
|
+
pauseResolve = void 0;
|
|
375
|
+
pausePromise = void 0;
|
|
376
|
+
}
|
|
377
|
+
function notify() {
|
|
378
|
+
for (const listener of listeners) listener();
|
|
379
|
+
}
|
|
380
|
+
function refreshSnapshot() {
|
|
381
|
+
snapshot = {
|
|
382
|
+
status: machineState.status,
|
|
383
|
+
...machineState.error !== void 0 ? { error: machineState.error } : {}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function dispatch(event) {
|
|
387
|
+
const next = transition(machineState, event);
|
|
388
|
+
const changed = next !== machineState;
|
|
389
|
+
if (changed) {
|
|
390
|
+
machineState = next;
|
|
391
|
+
refreshSnapshot();
|
|
392
|
+
notify();
|
|
393
|
+
}
|
|
394
|
+
return { changed };
|
|
395
|
+
}
|
|
396
|
+
function settle(result) {
|
|
397
|
+
/* v8 ignore next */
|
|
398
|
+
if (settledFlag) return;
|
|
399
|
+
settledFlag = true;
|
|
400
|
+
detachSignal();
|
|
401
|
+
settleFn(result);
|
|
402
|
+
}
|
|
403
|
+
function finish(status, resolvedValue, error) {
|
|
404
|
+
if (status === "done") {
|
|
405
|
+
value = resolvedValue;
|
|
406
|
+
settle({
|
|
407
|
+
status,
|
|
408
|
+
...resolvedValue !== void 0 ? { value: resolvedValue } : {}
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
settle(status === "error" ? {
|
|
413
|
+
status,
|
|
414
|
+
error
|
|
415
|
+
} : { status });
|
|
416
|
+
}
|
|
417
|
+
function settleIfNeverStarted() {
|
|
418
|
+
const { status, error } = machineState;
|
|
419
|
+
if (loopStarted || !isTerminal(status)) return;
|
|
420
|
+
loopStarted = true;
|
|
421
|
+
/* v8 ignore next */
|
|
422
|
+
if (status === "error") finish("error", void 0, error);
|
|
423
|
+
else finish(status);
|
|
424
|
+
}
|
|
425
|
+
function handleAbort() {
|
|
426
|
+
cancel();
|
|
427
|
+
}
|
|
428
|
+
function detachSignal() {
|
|
429
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
430
|
+
}
|
|
431
|
+
if (signal?.aborted) dispatch({ type: "CANCEL" });
|
|
432
|
+
else signal?.addEventListener("abort", handleAbort);
|
|
433
|
+
async function runLoop() {
|
|
434
|
+
let sliceStartedAt = now();
|
|
435
|
+
const generator = task();
|
|
436
|
+
let step = {
|
|
437
|
+
done: false,
|
|
438
|
+
value: void 0
|
|
439
|
+
};
|
|
440
|
+
try {
|
|
441
|
+
for (;;) {
|
|
442
|
+
const status = machineState.status;
|
|
443
|
+
if (status === "cancelled") {
|
|
444
|
+
generator.return(void 0);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
if (status === "paused") {
|
|
448
|
+
await waitForWake();
|
|
449
|
+
sliceStartedAt = now();
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
step = generator.next();
|
|
453
|
+
if (step.done) break;
|
|
454
|
+
if (shouldYieldNow({
|
|
455
|
+
sliceStartedAt,
|
|
456
|
+
timeSliceMs
|
|
457
|
+
})) {
|
|
458
|
+
await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
|
|
459
|
+
notify();
|
|
460
|
+
sliceStartedAt = now();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
dispatch({
|
|
465
|
+
type: "ERROR",
|
|
466
|
+
error
|
|
467
|
+
});
|
|
468
|
+
finish("error", void 0, error);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (machineState.status === "cancelled") {
|
|
472
|
+
finish("cancelled");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
dispatch({ type: "COMPLETE" });
|
|
476
|
+
/* v8 ignore next */
|
|
477
|
+
finish("done", step.done ? step.value : void 0);
|
|
478
|
+
}
|
|
479
|
+
function start() {
|
|
480
|
+
dispatch({ type: "START" });
|
|
481
|
+
if (machineState.status === "processing" && !loopStarted) {
|
|
482
|
+
loopStarted = true;
|
|
483
|
+
runLoop();
|
|
484
|
+
}
|
|
485
|
+
settleIfNeverStarted();
|
|
486
|
+
}
|
|
487
|
+
function pause() {
|
|
488
|
+
dispatch({ type: "PAUSE" });
|
|
489
|
+
}
|
|
490
|
+
function resume() {
|
|
491
|
+
const { changed } = dispatch({ type: "RESUME" });
|
|
492
|
+
if (changed) openGate();
|
|
493
|
+
}
|
|
494
|
+
function cancel() {
|
|
495
|
+
dispatch({ type: "CANCEL" });
|
|
496
|
+
openGate();
|
|
497
|
+
settleIfNeverStarted();
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
start,
|
|
501
|
+
pause,
|
|
502
|
+
resume,
|
|
503
|
+
cancel,
|
|
504
|
+
getSnapshot: () => snapshot,
|
|
505
|
+
getValue: () => value,
|
|
506
|
+
subscribe(listener) {
|
|
507
|
+
listeners.add(listener);
|
|
508
|
+
return () => {
|
|
509
|
+
listeners.delete(listener);
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
settled: settledPromise
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
export { now as a, supportsInputPending as c, yieldToMain as d, initialChunkState as f, isInputPending as i, supportsMessageChannel as l, transition as m, createChunkScheduler as n, resolveYieldStrategy as o, isTerminal as p, DEFAULT_TIME_SLICE_MS as r, shouldYieldNow as s, createDeferredTask as t, supportsSchedulerYield as u };
|
|
516
|
+
|
|
517
|
+
//# sourceMappingURL=deferred-task-5kfztF2G.js.map
|