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
|
@@ -0,0 +1,713 @@
|
|
|
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
|
+
* A batch that should take about TARGET_BATCH_MS, which is also the
|
|
93
|
+
* overshoot the caller takes on by accepting it. No lower bound is needed:
|
|
94
|
+
* the warning only fires below CLOCK_BOUND_UNIT_MS, so the result is
|
|
95
|
+
* always at least TARGET_BATCH_MS / CLOCK_BOUND_UNIT_MS.
|
|
96
|
+
*/
|
|
97
|
+
function suggestedBatchSize(unitCostMs) {
|
|
98
|
+
const target = Math.round(TARGET_BATCH_MS / unitCostMs);
|
|
99
|
+
return target > MAX_SUGGESTED_BATCH ? MAX_SUGGESTED_BATCH : target;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `unit` names what one step of this task is, so the message can say
|
|
103
|
+
* "item" for a chunked array and "segment" for a generator.
|
|
104
|
+
*/
|
|
105
|
+
function createDiagnostics(unit, batchSize) {
|
|
106
|
+
let lastCheckAt = 0;
|
|
107
|
+
let lastCheckUnits = 0;
|
|
108
|
+
let sliceStartedAt = 0;
|
|
109
|
+
let sliceStartUnits = 0;
|
|
110
|
+
let started = false;
|
|
111
|
+
let warnedAboutLongUnit = false;
|
|
112
|
+
let warnedAboutClockCost = false;
|
|
113
|
+
const rebase = (at, unitsDone) => {
|
|
114
|
+
lastCheckAt = at;
|
|
115
|
+
lastCheckUnits = unitsDone;
|
|
116
|
+
sliceStartedAt = at;
|
|
117
|
+
sliceStartUnits = unitsDone;
|
|
118
|
+
started = true;
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
check(at, unitsDone) {
|
|
122
|
+
if (!started) {
|
|
123
|
+
rebase(at, unitsDone);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const batchMs = at - lastCheckAt;
|
|
127
|
+
const unitsInBatch = unitsDone - lastCheckUnits;
|
|
128
|
+
lastCheckAt = at;
|
|
129
|
+
lastCheckUnits = unitsDone;
|
|
130
|
+
if (warnedAboutLongUnit || batchMs <= LONG_UNIT_MS) return;
|
|
131
|
+
warnedAboutLongUnit = true;
|
|
132
|
+
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)`);
|
|
133
|
+
},
|
|
134
|
+
sliceEnded(at, unitsDone) {
|
|
135
|
+
const sliceMs = at - sliceStartedAt;
|
|
136
|
+
const unitsInSlice = unitsDone - sliceStartUnits;
|
|
137
|
+
sliceStartedAt = at;
|
|
138
|
+
sliceStartUnits = unitsDone;
|
|
139
|
+
if (warnedAboutClockCost || batchSize > 1) return;
|
|
140
|
+
if (unitsInSlice < MIN_UNITS_TO_JUDGE || sliceMs <= 0) return;
|
|
141
|
+
const unitCostMs = sliceMs / unitsInSlice;
|
|
142
|
+
if (unitCostMs >= CLOCK_BOUND_UNIT_MS) return;
|
|
143
|
+
warnedAboutClockCost = true;
|
|
144
|
+
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)`);
|
|
145
|
+
},
|
|
146
|
+
resumed(at, unitsDone) {
|
|
147
|
+
rebase(at, unitsDone);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// src/core/scheduler.ts
|
|
152
|
+
/** Default time slice: roughly the granularity React's own scheduler uses. */
|
|
153
|
+
const DEFAULT_TIME_SLICE_MS = 5;
|
|
154
|
+
/** True when `scheduler.yield()` is present on this runtime. */
|
|
155
|
+
function supportsSchedulerYield() {
|
|
156
|
+
return typeof globalThis.scheduler?.yield === "function";
|
|
157
|
+
}
|
|
158
|
+
/** True when the `MessageChannel` constructor is present on this runtime. */
|
|
159
|
+
function supportsMessageChannel() {
|
|
160
|
+
return typeof MessageChannel === "function";
|
|
161
|
+
}
|
|
162
|
+
/** True when `navigator.scheduling.isInputPending()` is present. */
|
|
163
|
+
function supportsInputPending() {
|
|
164
|
+
return typeof globalThis.navigator?.scheduling?.isInputPending === "function";
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolves the input-pending probe once, for callers that would otherwise
|
|
168
|
+
* run the feature detection on every item of a long task: walking
|
|
169
|
+
* `navigator.scheduling.isInputPending` through optional calls is wasted
|
|
170
|
+
* work a million times over. Returns a function that always answers
|
|
171
|
+
* `false` where the API is missing, so callers need no second branch.
|
|
172
|
+
*/
|
|
173
|
+
function resolveInputPending() {
|
|
174
|
+
return supportsInputPending() ? isInputPending : returnsFalse;
|
|
175
|
+
}
|
|
176
|
+
function returnsFalse() {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Best-effort read of whether the browser has pending input right now.
|
|
181
|
+
* Returns `false` — never throws — when the API is unavailable or errors,
|
|
182
|
+
* since the caller's fallback (a plain time budget) is always safe.
|
|
183
|
+
*/
|
|
184
|
+
function isInputPending() {
|
|
185
|
+
try {
|
|
186
|
+
return globalThis.navigator?.scheduling?.isInputPending?.() ?? false;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Picks the best strategy this runtime actually supports. */
|
|
192
|
+
function resolveYieldStrategy() {
|
|
193
|
+
if (supportsSchedulerYield()) return "scheduler-yield";
|
|
194
|
+
if (supportsMessageChannel()) return "message-channel";
|
|
195
|
+
return "set-timeout";
|
|
196
|
+
}
|
|
197
|
+
function yieldViaMessageChannel() {
|
|
198
|
+
return new Promise((resolve) => {
|
|
199
|
+
const channel = new MessageChannel();
|
|
200
|
+
channel.port1.onmessage = () => {
|
|
201
|
+
channel.port1.close();
|
|
202
|
+
resolve();
|
|
203
|
+
};
|
|
204
|
+
channel.port2.postMessage(null);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function yieldViaSetTimeout() {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
setTimeout(resolve, 0);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Yields control back to the browser's main thread, resolving once the
|
|
214
|
+
* browser has had a chance to handle any pending input, then resuming the
|
|
215
|
+
* caller. Never rejects: a runtime that half-implements `scheduler.yield()`
|
|
216
|
+
* (the property exists but the call throws, or the returned promise
|
|
217
|
+
* rejects) falls through to the next strategy rather than propagating the
|
|
218
|
+
* failure into unrelated application code.
|
|
219
|
+
*/
|
|
220
|
+
async function yieldToMain(options) {
|
|
221
|
+
const strategy = options?.strategy ?? resolveYieldStrategy();
|
|
222
|
+
if (strategy === "scheduler-yield") {
|
|
223
|
+
const schedulerGlobal = globalThis.scheduler;
|
|
224
|
+
if (typeof schedulerGlobal?.yield === "function") try {
|
|
225
|
+
await schedulerGlobal.yield();
|
|
226
|
+
return;
|
|
227
|
+
} catch {}
|
|
228
|
+
await (supportsMessageChannel() ? yieldViaMessageChannel() : yieldViaSetTimeout());
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (strategy === "message-channel") {
|
|
232
|
+
if (supportsMessageChannel()) {
|
|
233
|
+
await yieldViaMessageChannel();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await yieldViaSetTimeout();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await yieldViaSetTimeout();
|
|
240
|
+
}
|
|
241
|
+
/** `performance.now()` when available, `Date.now()` otherwise. Never throws. */
|
|
242
|
+
function now() {
|
|
243
|
+
try {
|
|
244
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
|
|
245
|
+
} catch {}
|
|
246
|
+
return Date.now();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A time slice of zero, a negative number or NaN is meaningless, so those
|
|
250
|
+
* fall back to the default rather than producing a task that yields after
|
|
251
|
+
* every item (or never).
|
|
252
|
+
*/
|
|
253
|
+
function normalizeTimeSliceMs(value) {
|
|
254
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 5;
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Batch sizes are whole items, at least one. Anything else — a fraction,
|
|
259
|
+
* zero, a negative, NaN — falls back to 1, the safe default.
|
|
260
|
+
*/
|
|
261
|
+
function normalizeBatchSize(value) {
|
|
262
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return 1;
|
|
263
|
+
return Math.floor(value);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Whether the caller should yield right now: either the time slice has run
|
|
267
|
+
* out, or the browser is reporting pending input (a stronger, earlier
|
|
268
|
+
* signal than the clock — used only when the runtime exposes it).
|
|
269
|
+
*/
|
|
270
|
+
function shouldYieldNow(options) {
|
|
271
|
+
const timeSliceMs = options.timeSliceMs ?? 5;
|
|
272
|
+
if (now() - options.sliceStartedAt >= timeSliceMs) return true;
|
|
273
|
+
return isInputPending();
|
|
274
|
+
}
|
|
275
|
+
// src/core/chunk-scheduler.ts
|
|
276
|
+
/**
|
|
277
|
+
* `items[index]` for an index the caller has already bounds-checked.
|
|
278
|
+
*
|
|
279
|
+
* The loop below only ever indexes below `items.length`, but
|
|
280
|
+
* `noUncheckedIndexedAccess` widens every index read to `T | undefined`,
|
|
281
|
+
* and `T` may itself include `undefined` — so no runtime check can narrow
|
|
282
|
+
* it. This states the invariant in one place instead of asserting at each
|
|
283
|
+
* use, and compiles to a plain index read.
|
|
284
|
+
*/
|
|
285
|
+
function at(items, index) {
|
|
286
|
+
return items[index];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Builds a one-shot, controllable scheduler that processes `items` through
|
|
290
|
+
* `processItem`, yielding to the main thread on a time budget (and sooner,
|
|
291
|
+
* if the browser reports pending input) so a large array never produces a
|
|
292
|
+
* single long task.
|
|
293
|
+
*
|
|
294
|
+
* One-shot means exactly that: once the task reaches `done`, `cancelled` or
|
|
295
|
+
* `error`, this instance is finished — start()/pause()/resume() on it are
|
|
296
|
+
* no-ops from then on. To run another task, create another scheduler. For a
|
|
297
|
+
* React hook, that falls out naturally: new deps → new effect → new
|
|
298
|
+
* scheduler, which is the idiomatic React data-flow anyway.
|
|
299
|
+
*/
|
|
300
|
+
function createChunkScheduler(options) {
|
|
301
|
+
const { items, processItem, onItem, signal, yieldStrategy } = options;
|
|
302
|
+
const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
|
|
303
|
+
const batchSize = normalizeBatchSize(options.batchSize);
|
|
304
|
+
const collectResults = options.collectResults ?? true;
|
|
305
|
+
const total = items.length;
|
|
306
|
+
const inputPending = resolveInputPending();
|
|
307
|
+
const diagnostics = createDiagnostics("item", batchSize);
|
|
308
|
+
let machineState = initialChunkState;
|
|
309
|
+
let processed = 0;
|
|
310
|
+
let loopStarted = false;
|
|
311
|
+
let settledFlag = false;
|
|
312
|
+
const results = [];
|
|
313
|
+
let finalResults;
|
|
314
|
+
let snapshot = {
|
|
315
|
+
status: machineState.status,
|
|
316
|
+
processed,
|
|
317
|
+
total
|
|
318
|
+
};
|
|
319
|
+
let snapshotStale = false;
|
|
320
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
321
|
+
let settleFn;
|
|
322
|
+
const settledPromise = new Promise((resolve) => {
|
|
323
|
+
settleFn = resolve;
|
|
324
|
+
});
|
|
325
|
+
let pauseResolve;
|
|
326
|
+
let pausePromise;
|
|
327
|
+
function waitForWake() {
|
|
328
|
+
pausePromise ??= new Promise((resolve) => {
|
|
329
|
+
pauseResolve = resolve;
|
|
330
|
+
});
|
|
331
|
+
return pausePromise;
|
|
332
|
+
}
|
|
333
|
+
function openGate() {
|
|
334
|
+
pauseResolve?.();
|
|
335
|
+
pauseResolve = void 0;
|
|
336
|
+
pausePromise = void 0;
|
|
337
|
+
}
|
|
338
|
+
function notify() {
|
|
339
|
+
for (const listener of listeners) listener();
|
|
340
|
+
}
|
|
341
|
+
function getSnapshot() {
|
|
342
|
+
if (snapshotStale) {
|
|
343
|
+
snapshotStale = false;
|
|
344
|
+
snapshot = {
|
|
345
|
+
status: machineState.status,
|
|
346
|
+
processed,
|
|
347
|
+
total,
|
|
348
|
+
...machineState.error !== void 0 ? { error: machineState.error } : {},
|
|
349
|
+
...finalResults !== void 0 ? { results: finalResults } : {}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return snapshot;
|
|
353
|
+
}
|
|
354
|
+
function dispatch(event) {
|
|
355
|
+
const next = transition(machineState, event);
|
|
356
|
+
const changed = next !== machineState;
|
|
357
|
+
if (changed) {
|
|
358
|
+
machineState = next;
|
|
359
|
+
snapshotStale = true;
|
|
360
|
+
notify();
|
|
361
|
+
}
|
|
362
|
+
return { changed };
|
|
363
|
+
}
|
|
364
|
+
function settle(result) {
|
|
365
|
+
/* v8 ignore next */
|
|
366
|
+
if (settledFlag) return;
|
|
367
|
+
settledFlag = true;
|
|
368
|
+
detachSignal();
|
|
369
|
+
settleFn(result);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Publishes the accumulated results exactly once, at the moment the task
|
|
373
|
+
* settles — not per item, and never as a second copy. Always called
|
|
374
|
+
* *before* the terminal status is announced, so the notification that
|
|
375
|
+
* reports `done` already carries the results: a subscriber can never
|
|
376
|
+
* observe one without the other.
|
|
377
|
+
*/
|
|
378
|
+
function publishResults() {
|
|
379
|
+
finalResults = results;
|
|
380
|
+
snapshotStale = true;
|
|
381
|
+
return finalResults;
|
|
382
|
+
}
|
|
383
|
+
function settleWith(status, published, error) {
|
|
384
|
+
settle(status === "error" ? {
|
|
385
|
+
status,
|
|
386
|
+
error,
|
|
387
|
+
results: published
|
|
388
|
+
} : {
|
|
389
|
+
status,
|
|
390
|
+
results: published
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Settling as cancelled, for both places that can do it: the loop
|
|
395
|
+
* noticing, and cancel() on a task whose loop never ran. The CANCEL
|
|
396
|
+
* transition was announced earlier, before there were any results to
|
|
397
|
+
* carry, so this notifies again now that there are.
|
|
398
|
+
*/
|
|
399
|
+
function finishCancelled() {
|
|
400
|
+
settleWith("cancelled", publishResults());
|
|
401
|
+
notify();
|
|
402
|
+
}
|
|
403
|
+
function settleIfNeverStarted() {
|
|
404
|
+
const { status } = machineState;
|
|
405
|
+
if (loopStarted || !isTerminal(status)) return;
|
|
406
|
+
loopStarted = true;
|
|
407
|
+
finishCancelled();
|
|
408
|
+
}
|
|
409
|
+
function handleAbort() {
|
|
410
|
+
cancel();
|
|
411
|
+
}
|
|
412
|
+
function detachSignal() {
|
|
413
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
414
|
+
}
|
|
415
|
+
if (signal?.aborted) dispatch({ type: "CANCEL" });
|
|
416
|
+
else signal?.addEventListener("abort", handleAbort);
|
|
417
|
+
async function runLoop() {
|
|
418
|
+
let sliceStartedAt = now();
|
|
419
|
+
let index = 0;
|
|
420
|
+
try {
|
|
421
|
+
while (index < total) {
|
|
422
|
+
const status = machineState.status;
|
|
423
|
+
if (status === "cancelled") break;
|
|
424
|
+
if (status === "paused") {
|
|
425
|
+
await waitForWake();
|
|
426
|
+
sliceStartedAt = now();
|
|
427
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, index);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const batchEnd = index + batchSize <= total ? index + batchSize : total;
|
|
431
|
+
while (index < batchEnd) {
|
|
432
|
+
const item = at(items, index);
|
|
433
|
+
const result = processItem(item, index);
|
|
434
|
+
if (collectResults) results.push(result);
|
|
435
|
+
processed = index + 1;
|
|
436
|
+
onItem?.(result, item, index);
|
|
437
|
+
index += 1;
|
|
438
|
+
}
|
|
439
|
+
snapshotStale = true;
|
|
440
|
+
const checkedAt = now();
|
|
441
|
+
if (isDevelopment) diagnostics.check(checkedAt, index);
|
|
442
|
+
if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
|
|
443
|
+
if (isDevelopment) diagnostics.sliceEnded(checkedAt, index);
|
|
444
|
+
await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
|
|
445
|
+
notify();
|
|
446
|
+
sliceStartedAt = now();
|
|
447
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, index);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const published = publishResults();
|
|
452
|
+
dispatch({
|
|
453
|
+
type: "ERROR",
|
|
454
|
+
error
|
|
455
|
+
});
|
|
456
|
+
settleWith("error", published, error);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (machineState.status === "cancelled") {
|
|
460
|
+
finishCancelled();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const published = publishResults();
|
|
464
|
+
dispatch({ type: "COMPLETE" });
|
|
465
|
+
settleWith("done", published);
|
|
466
|
+
}
|
|
467
|
+
function start() {
|
|
468
|
+
dispatch({ type: "START" });
|
|
469
|
+
if (machineState.status === "processing" && !loopStarted) {
|
|
470
|
+
loopStarted = true;
|
|
471
|
+
runLoop();
|
|
472
|
+
}
|
|
473
|
+
settleIfNeverStarted();
|
|
474
|
+
}
|
|
475
|
+
function pause() {
|
|
476
|
+
dispatch({ type: "PAUSE" });
|
|
477
|
+
}
|
|
478
|
+
function resume() {
|
|
479
|
+
const { changed } = dispatch({ type: "RESUME" });
|
|
480
|
+
if (changed) openGate();
|
|
481
|
+
}
|
|
482
|
+
function cancel() {
|
|
483
|
+
dispatch({ type: "CANCEL" });
|
|
484
|
+
openGate();
|
|
485
|
+
settleIfNeverStarted();
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
start,
|
|
489
|
+
pause,
|
|
490
|
+
resume,
|
|
491
|
+
cancel,
|
|
492
|
+
getSnapshot,
|
|
493
|
+
getResults: () => finalResults,
|
|
494
|
+
subscribe(listener) {
|
|
495
|
+
listeners.add(listener);
|
|
496
|
+
return () => {
|
|
497
|
+
listeners.delete(listener);
|
|
498
|
+
};
|
|
499
|
+
},
|
|
500
|
+
settled: settledPromise
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
// src/core/deferred-task.ts
|
|
504
|
+
/**
|
|
505
|
+
* The generator-driven sibling of `createChunkScheduler`, for heavy work
|
|
506
|
+
* that isn't shaped like "one function applied to each item of an array".
|
|
507
|
+
* Same state machine, same yield primitives, same one-shot-instance
|
|
508
|
+
* contract — see that file's doc comment for the reasoning, which applies
|
|
509
|
+
* here unchanged.
|
|
510
|
+
*/
|
|
511
|
+
function createDeferredTask(options) {
|
|
512
|
+
const { task, signal, yieldStrategy } = options;
|
|
513
|
+
const timeSliceMs = normalizeTimeSliceMs(options.timeSliceMs);
|
|
514
|
+
const batchSize = normalizeBatchSize(options.batchSize);
|
|
515
|
+
const inputPending = resolveInputPending();
|
|
516
|
+
const diagnostics = createDiagnostics("segment", batchSize);
|
|
517
|
+
let machineState = initialChunkState;
|
|
518
|
+
let loopStarted = false;
|
|
519
|
+
let settledFlag = false;
|
|
520
|
+
let settledValue;
|
|
521
|
+
let snapshot = { status: machineState.status };
|
|
522
|
+
let snapshotStale = false;
|
|
523
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
524
|
+
let settleFn;
|
|
525
|
+
const settledPromise = new Promise((resolve) => {
|
|
526
|
+
settleFn = resolve;
|
|
527
|
+
});
|
|
528
|
+
let pauseResolve;
|
|
529
|
+
let pausePromise;
|
|
530
|
+
function waitForWake() {
|
|
531
|
+
pausePromise ??= new Promise((resolve) => {
|
|
532
|
+
pauseResolve = resolve;
|
|
533
|
+
});
|
|
534
|
+
return pausePromise;
|
|
535
|
+
}
|
|
536
|
+
function openGate() {
|
|
537
|
+
pauseResolve?.();
|
|
538
|
+
pauseResolve = void 0;
|
|
539
|
+
pausePromise = void 0;
|
|
540
|
+
}
|
|
541
|
+
function notify() {
|
|
542
|
+
for (const listener of listeners) listener();
|
|
543
|
+
}
|
|
544
|
+
function getSnapshot() {
|
|
545
|
+
if (snapshotStale) {
|
|
546
|
+
snapshotStale = false;
|
|
547
|
+
snapshot = {
|
|
548
|
+
status: machineState.status,
|
|
549
|
+
...machineState.error !== void 0 ? { error: machineState.error } : {},
|
|
550
|
+
...settledValue !== void 0 ? { value: settledValue } : {}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return snapshot;
|
|
554
|
+
}
|
|
555
|
+
function dispatch(event) {
|
|
556
|
+
const next = transition(machineState, event);
|
|
557
|
+
const changed = next !== machineState;
|
|
558
|
+
if (changed) {
|
|
559
|
+
machineState = next;
|
|
560
|
+
snapshotStale = true;
|
|
561
|
+
notify();
|
|
562
|
+
}
|
|
563
|
+
return { changed };
|
|
564
|
+
}
|
|
565
|
+
function settle(result) {
|
|
566
|
+
/* v8 ignore next */
|
|
567
|
+
if (settledFlag) return;
|
|
568
|
+
settledFlag = true;
|
|
569
|
+
detachSignal();
|
|
570
|
+
settleFn(result);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Publishes the returned value before the terminal status is announced,
|
|
574
|
+
* so the notification that reports `done` already carries it.
|
|
575
|
+
*/
|
|
576
|
+
function publishValue(resolvedValue) {
|
|
577
|
+
settledValue = resolvedValue;
|
|
578
|
+
snapshotStale = true;
|
|
579
|
+
}
|
|
580
|
+
function settleWith(status, resolvedValue, error) {
|
|
581
|
+
if (status === "done") {
|
|
582
|
+
settle({
|
|
583
|
+
status,
|
|
584
|
+
...resolvedValue !== void 0 ? { value: resolvedValue } : {}
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
settle(status === "error" ? {
|
|
589
|
+
status,
|
|
590
|
+
error
|
|
591
|
+
} : { status });
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Settling as cancelled, for both places that can do it. The CANCEL
|
|
595
|
+
* transition was announced earlier, so this notifies again now that the
|
|
596
|
+
* task has actually settled.
|
|
597
|
+
*/
|
|
598
|
+
function finishCancelled() {
|
|
599
|
+
settleWith("cancelled");
|
|
600
|
+
notify();
|
|
601
|
+
}
|
|
602
|
+
function settleIfNeverStarted() {
|
|
603
|
+
const { status } = machineState;
|
|
604
|
+
if (loopStarted || !isTerminal(status)) return;
|
|
605
|
+
loopStarted = true;
|
|
606
|
+
finishCancelled();
|
|
607
|
+
}
|
|
608
|
+
function handleAbort() {
|
|
609
|
+
cancel();
|
|
610
|
+
}
|
|
611
|
+
function detachSignal() {
|
|
612
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
613
|
+
}
|
|
614
|
+
if (signal?.aborted) dispatch({ type: "CANCEL" });
|
|
615
|
+
else signal?.addEventListener("abort", handleAbort);
|
|
616
|
+
async function runLoop() {
|
|
617
|
+
let sliceStartedAt = now();
|
|
618
|
+
let segment = 0;
|
|
619
|
+
const generator = task();
|
|
620
|
+
let step = {
|
|
621
|
+
done: false,
|
|
622
|
+
value: void 0
|
|
623
|
+
};
|
|
624
|
+
try {
|
|
625
|
+
for (;;) {
|
|
626
|
+
const status = machineState.status;
|
|
627
|
+
if (status === "cancelled") {
|
|
628
|
+
generator.return(void 0);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
if (status === "paused") {
|
|
632
|
+
await waitForWake();
|
|
633
|
+
sliceStartedAt = now();
|
|
634
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, segment);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
let done = false;
|
|
638
|
+
for (let inBatch = 0; inBatch < batchSize; inBatch++) {
|
|
639
|
+
step = generator.next();
|
|
640
|
+
segment += 1;
|
|
641
|
+
if (step.done) {
|
|
642
|
+
done = true;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (done) break;
|
|
647
|
+
const checkedAt = now();
|
|
648
|
+
if (isDevelopment) diagnostics.check(checkedAt, segment);
|
|
649
|
+
if (checkedAt - sliceStartedAt >= timeSliceMs || inputPending()) {
|
|
650
|
+
if (isDevelopment) diagnostics.sliceEnded(checkedAt, segment);
|
|
651
|
+
await yieldToMain(yieldStrategy !== void 0 ? { strategy: yieldStrategy } : void 0);
|
|
652
|
+
notify();
|
|
653
|
+
sliceStartedAt = now();
|
|
654
|
+
if (isDevelopment) diagnostics.resumed(sliceStartedAt, segment);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch (error) {
|
|
658
|
+
dispatch({
|
|
659
|
+
type: "ERROR",
|
|
660
|
+
error
|
|
661
|
+
});
|
|
662
|
+
settleWith("error", void 0, error);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (machineState.status === "cancelled") {
|
|
666
|
+
finishCancelled();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
/* v8 ignore next */
|
|
670
|
+
const returned = step.done ? step.value : void 0;
|
|
671
|
+
publishValue(returned);
|
|
672
|
+
dispatch({ type: "COMPLETE" });
|
|
673
|
+
settleWith("done", returned);
|
|
674
|
+
}
|
|
675
|
+
function start() {
|
|
676
|
+
dispatch({ type: "START" });
|
|
677
|
+
if (machineState.status === "processing" && !loopStarted) {
|
|
678
|
+
loopStarted = true;
|
|
679
|
+
runLoop();
|
|
680
|
+
}
|
|
681
|
+
settleIfNeverStarted();
|
|
682
|
+
}
|
|
683
|
+
function pause() {
|
|
684
|
+
dispatch({ type: "PAUSE" });
|
|
685
|
+
}
|
|
686
|
+
function resume() {
|
|
687
|
+
const { changed } = dispatch({ type: "RESUME" });
|
|
688
|
+
if (changed) openGate();
|
|
689
|
+
}
|
|
690
|
+
function cancel() {
|
|
691
|
+
dispatch({ type: "CANCEL" });
|
|
692
|
+
openGate();
|
|
693
|
+
settleIfNeverStarted();
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
start,
|
|
697
|
+
pause,
|
|
698
|
+
resume,
|
|
699
|
+
cancel,
|
|
700
|
+
getSnapshot,
|
|
701
|
+
getValue: () => settledValue,
|
|
702
|
+
subscribe(listener) {
|
|
703
|
+
listeners.add(listener);
|
|
704
|
+
return () => {
|
|
705
|
+
listeners.delete(listener);
|
|
706
|
+
};
|
|
707
|
+
},
|
|
708
|
+
settled: settledPromise
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
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 };
|
|
712
|
+
|
|
713
|
+
//# sourceMappingURL=deferred-task-CwjZA3K-.js.map
|