react-inp-chunker 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -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-6oTEnHWa.js +778 -0
- package/dist/deferred-task-6oTEnHWa.js.map +1 -0
- package/dist/deferred-task-DcnC99_Y.cjs +861 -0
- package/dist/deferred-task-DcnC99_Y.cjs.map +1 -0
- package/dist/{index-C6B_Nm3i.d.ts → index-B4gKjLYR.d.cts} +117 -6
- package/dist/index-B4gKjLYR.d.cts.map +1 -0
- package/dist/{index-C6B_Nm3i.d.cts → index-B4gKjLYR.d.ts} +117 -6
- package/dist/index-B4gKjLYR.d.ts.map +1 -0
- package/dist/index.cjs +68 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +49 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -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-DkHdsliy.js} +227 -99
- package/dist/production/deferred-task-DkHdsliy.js.map +1 -0
- package/dist/{deferred-task-BXVzapQg.cjs → production/deferred-task-yh5pWlSj.cjs} +227 -99
- package/dist/production/deferred-task-yh5pWlSj.cjs.map +1 -0
- package/dist/production/index.cjs +216 -0
- package/dist/production/index.cjs.map +1 -0
- package/dist/production/index.js +193 -0
- package/dist/production/index.js.map +1 -0
- package/package.json +69 -23
- 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
|
@@ -0,0 +1,861 @@
|
|
|
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/diagnostics.ts
|
|
59
|
+
/**
|
|
60
|
+
* Development-only guidance for the two ways a chunked task quietly fails
|
|
61
|
+
* to do what the caller wanted:
|
|
62
|
+
*
|
|
63
|
+
* 1. A single item takes longer than a long task. Chunking cannot help:
|
|
64
|
+
* a slice is never shorter than one item, so the page still freezes.
|
|
65
|
+
* 2. Items are so cheap that reading the clock — around 300ns in a
|
|
66
|
+
* browser, where it is coarsened to 0.1ms anyway — costs more than the
|
|
67
|
+
* work between reads. `batchSize` exists for exactly this.
|
|
68
|
+
*
|
|
69
|
+
* Neither is visible from the outside without a profiler, and both have a
|
|
70
|
+
* concrete fix, which is what makes them worth a warning.
|
|
71
|
+
*
|
|
72
|
+
* Nothing here costs anything in production. The engines guard every call
|
|
73
|
+
* with `isDevelopment`, which the production build (see tsdown.config.ts)
|
|
74
|
+
* replaces with a literal `false`, so the calls and this module both
|
|
75
|
+
* disappear. The checks also ride on clock reads the loop already makes and
|
|
76
|
+
* keep their own baselines, so the hot loop needs no extra bookkeeping.
|
|
77
|
+
*/
|
|
78
|
+
const isDevelopment = typeof process !== "undefined" && process.env["NODE_ENV"] !== "production";
|
|
79
|
+
/** The Long Tasks API's definition: past this, the browser calls it a long task. */
|
|
80
|
+
const LONG_UNIT_MS = 50;
|
|
81
|
+
/** Below this average cost, the clock read dominates the work. */
|
|
82
|
+
const CLOCK_BOUND_UNIT_MS = .002;
|
|
83
|
+
/** Units per slice below which the sample is too small to judge. */
|
|
84
|
+
const MIN_UNITS_TO_JUDGE = 64;
|
|
85
|
+
/** A suggested batch should take about this long, bounding the overshoot it buys. */
|
|
86
|
+
const TARGET_BATCH_MS = .1;
|
|
87
|
+
const MAX_SUGGESTED_BATCH = 512;
|
|
88
|
+
function warn(message) {
|
|
89
|
+
console.warn(`[react-inp-chunker] ${message}`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The same prefixed channel as the slice-timing guidance below, exposed for
|
|
93
|
+
* the one-shot misuse warnings in chunk-scheduler.ts (an async
|
|
94
|
+
* `processItem`, a `collectResults: false` with no `onItem` to catch the
|
|
95
|
+
* results it discards). Those checks live next to the code they're
|
|
96
|
+
* guarding rather than in this file, but they go through the same
|
|
97
|
+
* development-only gate: calling it has to compile away exactly like
|
|
98
|
+
* `createDiagnostics` does, so `diagnostics.production.ts` carries a
|
|
99
|
+
* matching no-op and the `_sameShape` check below catches drift between
|
|
100
|
+
* them.
|
|
101
|
+
*/
|
|
102
|
+
function warnDev(message) {
|
|
103
|
+
warn(message);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A batch that should take about TARGET_BATCH_MS, which is also the
|
|
107
|
+
* overshoot the caller takes on by accepting it. No lower bound is needed:
|
|
108
|
+
* the warning only fires below CLOCK_BOUND_UNIT_MS, so the result is
|
|
109
|
+
* always at least TARGET_BATCH_MS / CLOCK_BOUND_UNIT_MS.
|
|
110
|
+
*/
|
|
111
|
+
function suggestedBatchSize(unitCostMs) {
|
|
112
|
+
const target = Math.round(TARGET_BATCH_MS / unitCostMs);
|
|
113
|
+
return target > MAX_SUGGESTED_BATCH ? MAX_SUGGESTED_BATCH : target;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `unit` names what one step of this task is, so the message can say
|
|
117
|
+
* "item" for a chunked array and "segment" for a generator.
|
|
118
|
+
*/
|
|
119
|
+
function createDiagnostics(unit, batchSize) {
|
|
120
|
+
let lastCheckAt = 0;
|
|
121
|
+
let lastCheckUnits = 0;
|
|
122
|
+
let sliceStartedAt = 0;
|
|
123
|
+
let sliceStartUnits = 0;
|
|
124
|
+
let started = false;
|
|
125
|
+
let warnedAboutLongUnit = false;
|
|
126
|
+
let warnedAboutClockCost = false;
|
|
127
|
+
const rebase = (at, unitsDone) => {
|
|
128
|
+
lastCheckAt = at;
|
|
129
|
+
lastCheckUnits = unitsDone;
|
|
130
|
+
sliceStartedAt = at;
|
|
131
|
+
sliceStartUnits = unitsDone;
|
|
132
|
+
started = true;
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
check(at, unitsDone) {
|
|
136
|
+
if (!started) {
|
|
137
|
+
rebase(at, unitsDone);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const batchMs = at - lastCheckAt;
|
|
141
|
+
const unitsInBatch = unitsDone - lastCheckUnits;
|
|
142
|
+
lastCheckAt = at;
|
|
143
|
+
lastCheckUnits = unitsDone;
|
|
144
|
+
if (warnedAboutLongUnit || batchMs <= LONG_UNIT_MS) return;
|
|
145
|
+
warnedAboutLongUnit = true;
|
|
146
|
+
warn(`${unitsInBatch === 1 ? `One ${unit} (index ${String(unitsDone - 1)}) took ${batchMs.toFixed(0)}ms` : `A batch of ${String(unitsInBatch)} ${unit}s took ${batchMs.toFixed(0)}ms`}. A slice can never be shorter than that, so this still blocks the main thread. ${unit === "item" ? "Split the work inside processItem into smaller items, or move it to useDeferredTask and yield part-way through." : "Add more yield points, so each segment is shorter."} (development only)`);
|
|
147
|
+
},
|
|
148
|
+
sliceEnded(at, unitsDone) {
|
|
149
|
+
const sliceMs = at - sliceStartedAt;
|
|
150
|
+
const unitsInSlice = unitsDone - sliceStartUnits;
|
|
151
|
+
sliceStartedAt = at;
|
|
152
|
+
sliceStartUnits = unitsDone;
|
|
153
|
+
if (warnedAboutClockCost || batchSize > 1) return;
|
|
154
|
+
if (unitsInSlice < MIN_UNITS_TO_JUDGE || sliceMs <= 0) return;
|
|
155
|
+
const unitCostMs = sliceMs / unitsInSlice;
|
|
156
|
+
if (unitCostMs >= CLOCK_BOUND_UNIT_MS) return;
|
|
157
|
+
warnedAboutClockCost = true;
|
|
158
|
+
warn(`${unit}s are averaging ${(unitCostMs * 1e3).toFixed(2)}µs, so checking the clock after each one costs more than the work itself. Pass batchSize: ${String(suggestedBatchSize(unitCostMs))} to check once per batch instead. (development only)`);
|
|
159
|
+
},
|
|
160
|
+
resumed(at, unitsDone) {
|
|
161
|
+
rebase(at, unitsDone);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// src/core/scheduler.ts
|
|
166
|
+
/** Default time slice: roughly the granularity React's own scheduler uses. */
|
|
167
|
+
const DEFAULT_TIME_SLICE_MS = 5;
|
|
168
|
+
/** True when `scheduler.yield()` is present on this runtime. */
|
|
169
|
+
function supportsSchedulerYield() {
|
|
170
|
+
return typeof globalThis.scheduler?.yield === "function";
|
|
171
|
+
}
|
|
172
|
+
/** True when the `MessageChannel` constructor is present on this runtime. */
|
|
173
|
+
function supportsMessageChannel() {
|
|
174
|
+
return typeof MessageChannel === "function";
|
|
175
|
+
}
|
|
176
|
+
/** True when `navigator.scheduling.isInputPending()` is present. */
|
|
177
|
+
function supportsInputPending() {
|
|
178
|
+
return typeof globalThis.navigator?.scheduling?.isInputPending === "function";
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Resolves the input-pending probe once, for callers that would otherwise
|
|
182
|
+
* run the feature detection on every item of a long task: walking
|
|
183
|
+
* `navigator.scheduling.isInputPending` through optional calls is wasted
|
|
184
|
+
* work a million times over. Returns a function that always answers
|
|
185
|
+
* `false` where the API is missing, so callers need no second branch.
|
|
186
|
+
*/
|
|
187
|
+
function resolveInputPending() {
|
|
188
|
+
return supportsInputPending() ? isInputPending : returnsFalse;
|
|
189
|
+
}
|
|
190
|
+
function returnsFalse() {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Best-effort read of whether the browser has pending input right now.
|
|
195
|
+
* Returns `false` — never throws — when the API is unavailable or errors,
|
|
196
|
+
* since the caller's fallback (a plain time budget) is always safe.
|
|
197
|
+
*/
|
|
198
|
+
function isInputPending() {
|
|
199
|
+
try {
|
|
200
|
+
return globalThis.navigator?.scheduling?.isInputPending?.() ?? false;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Picks the best strategy this runtime actually supports. */
|
|
206
|
+
function resolveYieldStrategy() {
|
|
207
|
+
if (supportsSchedulerYield()) return "scheduler-yield";
|
|
208
|
+
if (supportsMessageChannel()) return "message-channel";
|
|
209
|
+
return "set-timeout";
|
|
210
|
+
}
|
|
211
|
+
function yieldViaMessageChannel() {
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
const channel = new MessageChannel();
|
|
214
|
+
channel.port1.onmessage = () => {
|
|
215
|
+
channel.port1.close();
|
|
216
|
+
resolve();
|
|
217
|
+
};
|
|
218
|
+
channel.port2.postMessage(null);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function yieldViaSetTimeout() {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
setTimeout(resolve, 0);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Yields control back to the browser's main thread, resolving once the
|
|
228
|
+
* browser has had a chance to handle any pending input, then resuming the
|
|
229
|
+
* caller. Never rejects: a runtime that half-implements `scheduler.yield()`
|
|
230
|
+
* (the property exists but the call throws, or the returned promise
|
|
231
|
+
* rejects) falls through to the next strategy rather than propagating the
|
|
232
|
+
* failure into unrelated application code.
|
|
233
|
+
*/
|
|
234
|
+
async function yieldToMain(options) {
|
|
235
|
+
const strategy = options?.strategy ?? resolveYieldStrategy();
|
|
236
|
+
if (strategy === "scheduler-yield") {
|
|
237
|
+
const schedulerGlobal = globalThis.scheduler;
|
|
238
|
+
if (typeof schedulerGlobal?.yield === "function") try {
|
|
239
|
+
await schedulerGlobal.yield();
|
|
240
|
+
return;
|
|
241
|
+
} catch {}
|
|
242
|
+
await (supportsMessageChannel() ? yieldViaMessageChannel() : yieldViaSetTimeout());
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (strategy === "message-channel") {
|
|
246
|
+
if (supportsMessageChannel()) {
|
|
247
|
+
await yieldViaMessageChannel();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await yieldViaSetTimeout();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
await yieldViaSetTimeout();
|
|
254
|
+
}
|
|
255
|
+
/** `performance.now()` when available, `Date.now()` otherwise. Never throws. */
|
|
256
|
+
function now() {
|
|
257
|
+
try {
|
|
258
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
|
|
259
|
+
} catch {}
|
|
260
|
+
return Date.now();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* A time slice of zero, a negative number or NaN is meaningless, so those
|
|
264
|
+
* fall back to the default rather than producing a task that yields after
|
|
265
|
+
* every item (or never).
|
|
266
|
+
*/
|
|
267
|
+
function normalizeTimeSliceMs(value) {
|
|
268
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 5;
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Batch sizes are whole items, at least one. Anything else — a fraction,
|
|
273
|
+
* zero, a negative, NaN — falls back to 1, the safe default.
|
|
274
|
+
*/
|
|
275
|
+
function normalizeBatchSize(value) {
|
|
276
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return 1;
|
|
277
|
+
return Math.floor(value);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Whether the caller should yield right now: either the time slice has run
|
|
281
|
+
* out, or the browser is reporting pending input (a stronger, earlier
|
|
282
|
+
* signal than the clock — used only when the runtime exposes it).
|
|
283
|
+
*/
|
|
284
|
+
function shouldYieldNow(options) {
|
|
285
|
+
const timeSliceMs = options.timeSliceMs ?? 5;
|
|
286
|
+
if (now() - options.sliceStartedAt >= timeSliceMs) return true;
|
|
287
|
+
return isInputPending();
|
|
288
|
+
}
|
|
289
|
+
// src/core/chunk-scheduler.ts
|
|
290
|
+
/**
|
|
291
|
+
* `items[index]` for an index the caller has already bounds-checked.
|
|
292
|
+
*
|
|
293
|
+
* The loop below only ever indexes below `items.length`, but
|
|
294
|
+
* `noUncheckedIndexedAccess` widens every index read to `T | undefined`,
|
|
295
|
+
* and `T` may itself include `undefined` — so no runtime check can narrow
|
|
296
|
+
* it. This states the invariant in one place instead of asserting at each
|
|
297
|
+
* use, and compiles to a plain index read.
|
|
298
|
+
*/
|
|
299
|
+
function at(items, index) {
|
|
300
|
+
return items[index];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Loose, `typeof`-only thenable check — the same test `await` itself uses,
|
|
304
|
+
* not an `instanceof Promise` (which would miss any other Promise-like:
|
|
305
|
+
* a different realm's Promise, a userland polyfill). Good enough for a
|
|
306
|
+
* one-shot development warning; never throws on anything a caller could
|
|
307
|
+
* plausibly have returned.
|
|
308
|
+
*/
|
|
309
|
+
function isThenable(value) {
|
|
310
|
+
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Builds a one-shot, controllable scheduler that processes `items` through
|
|
314
|
+
* `processItem`, yielding to the main thread on a time budget (and sooner,
|
|
315
|
+
* if the browser reports pending input) so a large array never produces a
|
|
316
|
+
* single long task.
|
|
317
|
+
*
|
|
318
|
+
* One-shot means exactly that: once the task reaches `done`, `cancelled` or
|
|
319
|
+
* `error`, this instance is finished — start()/pause()/resume() on it are
|
|
320
|
+
* no-ops from then on. To run another task, create another scheduler. For a
|
|
321
|
+
* React hook, that falls out naturally: new deps → new effect → new
|
|
322
|
+
* scheduler, which is the idiomatic React data-flow anyway.
|
|
323
|
+
*/
|
|
324
|
+
function createChunkScheduler(options) {
|
|
325
|
+
const { items, processItem, onItem, onError, signal, yieldStrategy, onSlice, onYield } = options;
|
|
326
|
+
const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
|
|
327
|
+
const batchSize = normalizeBatchSize(options.batchSize);
|
|
328
|
+
const collectResults = options.collectResults ?? true;
|
|
329
|
+
const total = items.length;
|
|
330
|
+
if (isDevelopment && !collectResults && onItem === void 0) warnDev("collectResults is false and no onItem was passed — every result will be computed and then discarded, with nothing to retrieve once the task settles. Pass onItem to consume results as they finish, or drop collectResults: false if you need the final array.");
|
|
331
|
+
const inputPending = resolveInputPending();
|
|
332
|
+
const diagnostics = createDiagnostics("item", batchSize);
|
|
333
|
+
let warnedAboutAsyncResult = false;
|
|
334
|
+
let machineState = initialChunkState;
|
|
335
|
+
let processed = 0;
|
|
336
|
+
let loopStarted = false;
|
|
337
|
+
let settledFlag = false;
|
|
338
|
+
const results = [];
|
|
339
|
+
let finalResults;
|
|
340
|
+
const skipped = [];
|
|
341
|
+
let cancelReason;
|
|
342
|
+
let snapshot = {
|
|
343
|
+
status: machineState.status,
|
|
344
|
+
processed,
|
|
345
|
+
total
|
|
346
|
+
};
|
|
347
|
+
let snapshotStale = false;
|
|
348
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
349
|
+
let settleFn;
|
|
350
|
+
const settledPromise = new Promise((resolve) => {
|
|
351
|
+
settleFn = resolve;
|
|
352
|
+
});
|
|
353
|
+
let pauseResolve;
|
|
354
|
+
let pausePromise;
|
|
355
|
+
function waitForWake() {
|
|
356
|
+
pausePromise ??= new Promise((resolve) => {
|
|
357
|
+
pauseResolve = resolve;
|
|
358
|
+
});
|
|
359
|
+
return pausePromise;
|
|
360
|
+
}
|
|
361
|
+
function openGate() {
|
|
362
|
+
pauseResolve?.();
|
|
363
|
+
pauseResolve = void 0;
|
|
364
|
+
pausePromise = void 0;
|
|
365
|
+
}
|
|
366
|
+
function notify() {
|
|
367
|
+
for (const listener of listeners) listener();
|
|
368
|
+
}
|
|
369
|
+
function getSnapshot() {
|
|
370
|
+
if (snapshotStale) {
|
|
371
|
+
snapshotStale = false;
|
|
372
|
+
snapshot = {
|
|
373
|
+
status: machineState.status,
|
|
374
|
+
processed,
|
|
375
|
+
total,
|
|
376
|
+
...machineState.error !== void 0 ? { error: machineState.error } : {},
|
|
377
|
+
...finalResults !== void 0 ? { results: finalResults } : {},
|
|
378
|
+
...skipped.length > 0 ? { skipped: skipped.slice() } : {},
|
|
379
|
+
...cancelReason !== void 0 ? { reason: cancelReason } : {}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return snapshot;
|
|
383
|
+
}
|
|
384
|
+
function dispatch(event) {
|
|
385
|
+
const next = transition(machineState, event);
|
|
386
|
+
const changed = next !== machineState;
|
|
387
|
+
if (changed) {
|
|
388
|
+
machineState = next;
|
|
389
|
+
snapshotStale = true;
|
|
390
|
+
notify();
|
|
391
|
+
}
|
|
392
|
+
return { changed };
|
|
393
|
+
}
|
|
394
|
+
function settle(result) {
|
|
395
|
+
/* v8 ignore next */
|
|
396
|
+
if (settledFlag) return;
|
|
397
|
+
settledFlag = true;
|
|
398
|
+
detachSignal();
|
|
399
|
+
settleFn(result);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Publishes the accumulated results exactly once, at the moment the task
|
|
403
|
+
* settles — not per item, and never as a second copy. Always called
|
|
404
|
+
* *before* the terminal status is announced, so the notification that
|
|
405
|
+
* reports `done` already carries the results: a subscriber can never
|
|
406
|
+
* observe one without the other.
|
|
407
|
+
*/
|
|
408
|
+
function publishResults() {
|
|
409
|
+
finalResults = results;
|
|
410
|
+
snapshotStale = true;
|
|
411
|
+
return finalResults;
|
|
412
|
+
}
|
|
413
|
+
function settleWith(status, published, error) {
|
|
414
|
+
settle({
|
|
415
|
+
status,
|
|
416
|
+
results: published,
|
|
417
|
+
...error !== void 0 ? { error } : {},
|
|
418
|
+
...skipped.length > 0 ? { skipped: skipped.slice() } : {},
|
|
419
|
+
...status === "cancelled" && cancelReason !== void 0 ? { reason: cancelReason } : {}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Settling as cancelled, for both places that can do it: the loop
|
|
424
|
+
* noticing, and cancel() on a task whose loop never ran. The CANCEL
|
|
425
|
+
* transition was announced earlier, before there were any results to
|
|
426
|
+
* carry, so this notifies again now that there are.
|
|
427
|
+
*/
|
|
428
|
+
function finishCancelled() {
|
|
429
|
+
settleWith("cancelled", publishResults());
|
|
430
|
+
notify();
|
|
431
|
+
}
|
|
432
|
+
function settleIfNeverStarted() {
|
|
433
|
+
const { status } = machineState;
|
|
434
|
+
if (loopStarted || !isTerminal(status)) return;
|
|
435
|
+
loopStarted = true;
|
|
436
|
+
finishCancelled();
|
|
437
|
+
}
|
|
438
|
+
function handleAbort() {
|
|
439
|
+
cancelReason = signal?.reason;
|
|
440
|
+
cancel();
|
|
441
|
+
}
|
|
442
|
+
function detachSignal() {
|
|
443
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
444
|
+
}
|
|
445
|
+
if (signal?.aborted) {
|
|
446
|
+
cancelReason = signal.reason;
|
|
447
|
+
dispatch({ type: "CANCEL" });
|
|
448
|
+
} else signal?.addEventListener("abort", handleAbort);
|
|
449
|
+
async function runLoop() {
|
|
450
|
+
let sliceStartedAt = now();
|
|
451
|
+
let index = 0;
|
|
452
|
+
try {
|
|
453
|
+
while (index < total) {
|
|
454
|
+
const status = machineState.status;
|
|
455
|
+
if (status === "cancelled") break;
|
|
456
|
+
if (status === "paused") {
|
|
457
|
+
await waitForWake();
|
|
458
|
+
sliceStartedAt = now();
|
|
459
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, index);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const batchEnd = index + batchSize <= total ? index + batchSize : total;
|
|
463
|
+
while (index < batchEnd) {
|
|
464
|
+
const item = at(items, index);
|
|
465
|
+
let result;
|
|
466
|
+
try {
|
|
467
|
+
result = processItem(item, index);
|
|
468
|
+
} catch (caught) {
|
|
469
|
+
if ((onError?.(caught, item, index) ?? "abort") !== "skip") throw caught;
|
|
470
|
+
skipped.push({
|
|
471
|
+
index,
|
|
472
|
+
error: caught
|
|
473
|
+
});
|
|
474
|
+
processed = index + 1;
|
|
475
|
+
snapshotStale = true;
|
|
476
|
+
index += 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (isDevelopment && !warnedAboutAsyncResult && isThenable(result)) {
|
|
480
|
+
warnedAboutAsyncResult = true;
|
|
481
|
+
warnDev("processItem returned a Promise. createChunkScheduler calls it synchronously and never awaits the result, so results will contain unresolved Promise objects instead of the values they resolve to. Resolve the value before returning it, or chunk the async work a different way.");
|
|
482
|
+
}
|
|
483
|
+
if (collectResults) results.push(result);
|
|
484
|
+
processed = index + 1;
|
|
485
|
+
onItem?.(result, item, index);
|
|
486
|
+
index += 1;
|
|
487
|
+
}
|
|
488
|
+
snapshotStale = true;
|
|
489
|
+
const checkedAt = now();
|
|
490
|
+
if (isDevelopment) diagnostics.check(checkedAt, index);
|
|
491
|
+
if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
|
|
492
|
+
if (isDevelopment) diagnostics.sliceEnded(checkedAt, index);
|
|
493
|
+
onSlice?.(checkedAt - sliceStartedAt, index);
|
|
494
|
+
await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
|
|
495
|
+
notify();
|
|
496
|
+
sliceStartedAt = now();
|
|
497
|
+
onYield?.();
|
|
498
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, index);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch (error) {
|
|
502
|
+
const published = publishResults();
|
|
503
|
+
dispatch({
|
|
504
|
+
type: "ERROR",
|
|
505
|
+
error
|
|
506
|
+
});
|
|
507
|
+
settleWith("error", published, error);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (machineState.status === "cancelled") {
|
|
511
|
+
finishCancelled();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const published = publishResults();
|
|
515
|
+
dispatch({ type: "COMPLETE" });
|
|
516
|
+
settleWith("done", published);
|
|
517
|
+
}
|
|
518
|
+
function start() {
|
|
519
|
+
dispatch({ type: "START" });
|
|
520
|
+
if (machineState.status === "processing" && !loopStarted) {
|
|
521
|
+
loopStarted = true;
|
|
522
|
+
runLoop();
|
|
523
|
+
}
|
|
524
|
+
settleIfNeverStarted();
|
|
525
|
+
}
|
|
526
|
+
function pause() {
|
|
527
|
+
dispatch({ type: "PAUSE" });
|
|
528
|
+
}
|
|
529
|
+
function resume() {
|
|
530
|
+
const { changed } = dispatch({ type: "RESUME" });
|
|
531
|
+
if (changed) openGate();
|
|
532
|
+
}
|
|
533
|
+
function cancel() {
|
|
534
|
+
dispatch({ type: "CANCEL" });
|
|
535
|
+
openGate();
|
|
536
|
+
settleIfNeverStarted();
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
start,
|
|
540
|
+
pause,
|
|
541
|
+
resume,
|
|
542
|
+
cancel,
|
|
543
|
+
getSnapshot,
|
|
544
|
+
getResults: () => finalResults,
|
|
545
|
+
subscribe(listener) {
|
|
546
|
+
listeners.add(listener);
|
|
547
|
+
return () => {
|
|
548
|
+
listeners.delete(listener);
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
settled: settledPromise
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
// src/core/deferred-task.ts
|
|
555
|
+
/**
|
|
556
|
+
* The generator-driven sibling of `createChunkScheduler`, for heavy work
|
|
557
|
+
* that isn't shaped like "one function applied to each item of an array".
|
|
558
|
+
* Same state machine, same yield primitives, same one-shot-instance
|
|
559
|
+
* contract — see that file's doc comment for the reasoning, which applies
|
|
560
|
+
* here unchanged.
|
|
561
|
+
*/
|
|
562
|
+
function createDeferredTask(options) {
|
|
563
|
+
const { task, signal, yieldStrategy, onSlice, onYield } = options;
|
|
564
|
+
const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
|
|
565
|
+
const batchSize = normalizeBatchSize(options.batchSize);
|
|
566
|
+
const inputPending = resolveInputPending();
|
|
567
|
+
const diagnostics = createDiagnostics("segment", batchSize);
|
|
568
|
+
let machineState = initialChunkState;
|
|
569
|
+
let loopStarted = false;
|
|
570
|
+
let settledFlag = false;
|
|
571
|
+
let settledValue;
|
|
572
|
+
let cancelReason;
|
|
573
|
+
let snapshot = { status: machineState.status };
|
|
574
|
+
let snapshotStale = false;
|
|
575
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
576
|
+
let settleFn;
|
|
577
|
+
const settledPromise = new Promise((resolve) => {
|
|
578
|
+
settleFn = resolve;
|
|
579
|
+
});
|
|
580
|
+
let pauseResolve;
|
|
581
|
+
let pausePromise;
|
|
582
|
+
function waitForWake() {
|
|
583
|
+
pausePromise ??= new Promise((resolve) => {
|
|
584
|
+
pauseResolve = resolve;
|
|
585
|
+
});
|
|
586
|
+
return pausePromise;
|
|
587
|
+
}
|
|
588
|
+
function openGate() {
|
|
589
|
+
pauseResolve?.();
|
|
590
|
+
pauseResolve = void 0;
|
|
591
|
+
pausePromise = void 0;
|
|
592
|
+
}
|
|
593
|
+
function notify() {
|
|
594
|
+
for (const listener of listeners) listener();
|
|
595
|
+
}
|
|
596
|
+
function getSnapshot() {
|
|
597
|
+
if (snapshotStale) {
|
|
598
|
+
snapshotStale = false;
|
|
599
|
+
snapshot = {
|
|
600
|
+
status: machineState.status,
|
|
601
|
+
...machineState.error !== void 0 ? { error: machineState.error } : {},
|
|
602
|
+
...settledValue !== void 0 ? { value: settledValue } : {},
|
|
603
|
+
...cancelReason !== void 0 ? { reason: cancelReason } : {}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return snapshot;
|
|
607
|
+
}
|
|
608
|
+
function dispatch(event) {
|
|
609
|
+
const next = transition(machineState, event);
|
|
610
|
+
const changed = next !== machineState;
|
|
611
|
+
if (changed) {
|
|
612
|
+
machineState = next;
|
|
613
|
+
snapshotStale = true;
|
|
614
|
+
notify();
|
|
615
|
+
}
|
|
616
|
+
return { changed };
|
|
617
|
+
}
|
|
618
|
+
function settle(result) {
|
|
619
|
+
/* v8 ignore next */
|
|
620
|
+
if (settledFlag) return;
|
|
621
|
+
settledFlag = true;
|
|
622
|
+
detachSignal();
|
|
623
|
+
settleFn(result);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Publishes the returned value before the terminal status is announced,
|
|
627
|
+
* so the notification that reports `done` already carries it.
|
|
628
|
+
*/
|
|
629
|
+
function publishValue(resolvedValue) {
|
|
630
|
+
settledValue = resolvedValue;
|
|
631
|
+
snapshotStale = true;
|
|
632
|
+
}
|
|
633
|
+
function settleWith(status, resolvedValue, error) {
|
|
634
|
+
if (status === "done") {
|
|
635
|
+
settle({
|
|
636
|
+
status,
|
|
637
|
+
...resolvedValue !== void 0 ? { value: resolvedValue } : {}
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (status === "error") {
|
|
642
|
+
settle({
|
|
643
|
+
status,
|
|
644
|
+
error
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
settle({
|
|
649
|
+
status,
|
|
650
|
+
...cancelReason !== void 0 ? { reason: cancelReason } : {}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Settling as cancelled, for both places that can do it. The CANCEL
|
|
655
|
+
* transition was announced earlier, so this notifies again now that the
|
|
656
|
+
* task has actually settled.
|
|
657
|
+
*/
|
|
658
|
+
function finishCancelled() {
|
|
659
|
+
settleWith("cancelled");
|
|
660
|
+
notify();
|
|
661
|
+
}
|
|
662
|
+
function settleIfNeverStarted() {
|
|
663
|
+
const { status } = machineState;
|
|
664
|
+
if (loopStarted || !isTerminal(status)) return;
|
|
665
|
+
loopStarted = true;
|
|
666
|
+
finishCancelled();
|
|
667
|
+
}
|
|
668
|
+
function handleAbort() {
|
|
669
|
+
cancelReason = signal?.reason;
|
|
670
|
+
cancel();
|
|
671
|
+
}
|
|
672
|
+
function detachSignal() {
|
|
673
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
674
|
+
}
|
|
675
|
+
if (signal?.aborted) {
|
|
676
|
+
cancelReason = signal.reason;
|
|
677
|
+
dispatch({ type: "CANCEL" });
|
|
678
|
+
} else signal?.addEventListener("abort", handleAbort);
|
|
679
|
+
async function runLoop() {
|
|
680
|
+
let sliceStartedAt = now();
|
|
681
|
+
let segment = 0;
|
|
682
|
+
const generator = task();
|
|
683
|
+
let step = {
|
|
684
|
+
done: false,
|
|
685
|
+
value: void 0
|
|
686
|
+
};
|
|
687
|
+
try {
|
|
688
|
+
for (;;) {
|
|
689
|
+
const status = machineState.status;
|
|
690
|
+
if (status === "cancelled") {
|
|
691
|
+
generator.return(void 0);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
if (status === "paused") {
|
|
695
|
+
await waitForWake();
|
|
696
|
+
sliceStartedAt = now();
|
|
697
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, segment);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
let done = false;
|
|
701
|
+
for (let inBatch = 0; inBatch < batchSize; inBatch++) {
|
|
702
|
+
step = generator.next();
|
|
703
|
+
segment += 1;
|
|
704
|
+
if (step.done) {
|
|
705
|
+
done = true;
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (done) break;
|
|
710
|
+
const checkedAt = now();
|
|
711
|
+
if (isDevelopment) diagnostics.check(checkedAt, segment);
|
|
712
|
+
if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
|
|
713
|
+
if (isDevelopment) diagnostics.sliceEnded(checkedAt, segment);
|
|
714
|
+
onSlice?.(checkedAt - sliceStartedAt, segment);
|
|
715
|
+
await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
|
|
716
|
+
notify();
|
|
717
|
+
sliceStartedAt = now();
|
|
718
|
+
onYield?.();
|
|
719
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, segment);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
dispatch({
|
|
724
|
+
type: "ERROR",
|
|
725
|
+
error
|
|
726
|
+
});
|
|
727
|
+
settleWith("error", void 0, error);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (machineState.status === "cancelled") {
|
|
731
|
+
finishCancelled();
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
/* v8 ignore next */
|
|
735
|
+
const returned = step.done ? step.value : void 0;
|
|
736
|
+
publishValue(returned);
|
|
737
|
+
dispatch({ type: "COMPLETE" });
|
|
738
|
+
settleWith("done", returned);
|
|
739
|
+
}
|
|
740
|
+
function start() {
|
|
741
|
+
dispatch({ type: "START" });
|
|
742
|
+
if (machineState.status === "processing" && !loopStarted) {
|
|
743
|
+
loopStarted = true;
|
|
744
|
+
runLoop();
|
|
745
|
+
}
|
|
746
|
+
settleIfNeverStarted();
|
|
747
|
+
}
|
|
748
|
+
function pause() {
|
|
749
|
+
dispatch({ type: "PAUSE" });
|
|
750
|
+
}
|
|
751
|
+
function resume() {
|
|
752
|
+
const { changed } = dispatch({ type: "RESUME" });
|
|
753
|
+
if (changed) openGate();
|
|
754
|
+
}
|
|
755
|
+
function cancel() {
|
|
756
|
+
dispatch({ type: "CANCEL" });
|
|
757
|
+
openGate();
|
|
758
|
+
settleIfNeverStarted();
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
start,
|
|
762
|
+
pause,
|
|
763
|
+
resume,
|
|
764
|
+
cancel,
|
|
765
|
+
getSnapshot,
|
|
766
|
+
getValue: () => settledValue,
|
|
767
|
+
subscribe(listener) {
|
|
768
|
+
listeners.add(listener);
|
|
769
|
+
return () => {
|
|
770
|
+
listeners.delete(listener);
|
|
771
|
+
};
|
|
772
|
+
},
|
|
773
|
+
settled: settledPromise
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
Object.defineProperty(exports, "DEFAULT_TIME_SLICE_MS", {
|
|
777
|
+
enumerable: true,
|
|
778
|
+
get: function() {
|
|
779
|
+
return DEFAULT_TIME_SLICE_MS;
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
Object.defineProperty(exports, "createChunkScheduler", {
|
|
783
|
+
enumerable: true,
|
|
784
|
+
get: function() {
|
|
785
|
+
return createChunkScheduler;
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
Object.defineProperty(exports, "createDeferredTask", {
|
|
789
|
+
enumerable: true,
|
|
790
|
+
get: function() {
|
|
791
|
+
return createDeferredTask;
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
Object.defineProperty(exports, "initialChunkState", {
|
|
795
|
+
enumerable: true,
|
|
796
|
+
get: function() {
|
|
797
|
+
return initialChunkState;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
Object.defineProperty(exports, "isInputPending", {
|
|
801
|
+
enumerable: true,
|
|
802
|
+
get: function() {
|
|
803
|
+
return isInputPending;
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
Object.defineProperty(exports, "isTerminal", {
|
|
807
|
+
enumerable: true,
|
|
808
|
+
get: function() {
|
|
809
|
+
return isTerminal;
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
Object.defineProperty(exports, "now", {
|
|
813
|
+
enumerable: true,
|
|
814
|
+
get: function() {
|
|
815
|
+
return now;
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
Object.defineProperty(exports, "resolveYieldStrategy", {
|
|
819
|
+
enumerable: true,
|
|
820
|
+
get: function() {
|
|
821
|
+
return resolveYieldStrategy;
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
Object.defineProperty(exports, "shouldYieldNow", {
|
|
825
|
+
enumerable: true,
|
|
826
|
+
get: function() {
|
|
827
|
+
return shouldYieldNow;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
Object.defineProperty(exports, "supportsInputPending", {
|
|
831
|
+
enumerable: true,
|
|
832
|
+
get: function() {
|
|
833
|
+
return supportsInputPending;
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
Object.defineProperty(exports, "supportsMessageChannel", {
|
|
837
|
+
enumerable: true,
|
|
838
|
+
get: function() {
|
|
839
|
+
return supportsMessageChannel;
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
Object.defineProperty(exports, "supportsSchedulerYield", {
|
|
843
|
+
enumerable: true,
|
|
844
|
+
get: function() {
|
|
845
|
+
return supportsSchedulerYield;
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
Object.defineProperty(exports, "transition", {
|
|
849
|
+
enumerable: true,
|
|
850
|
+
get: function() {
|
|
851
|
+
return transition;
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
Object.defineProperty(exports, "yieldToMain", {
|
|
855
|
+
enumerable: true,
|
|
856
|
+
get: function() {
|
|
857
|
+
return yieldToMain;
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
//# sourceMappingURL=deferred-task-DcnC99_Y.cjs.map
|