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.
Files changed (37) hide show
  1. package/README.md +165 -53
  2. package/core/package.json +10 -0
  3. package/dist/core/index.cjs +1 -1
  4. package/dist/core/index.d.cts +2 -2
  5. package/dist/core/index.d.ts +2 -2
  6. package/dist/core/index.js +1 -1
  7. package/dist/deferred-task-6oTEnHWa.js +778 -0
  8. package/dist/deferred-task-6oTEnHWa.js.map +1 -0
  9. package/dist/deferred-task-DcnC99_Y.cjs +861 -0
  10. package/dist/deferred-task-DcnC99_Y.cjs.map +1 -0
  11. package/dist/{index-C6B_Nm3i.d.ts → index-B4gKjLYR.d.cts} +117 -6
  12. package/dist/index-B4gKjLYR.d.cts.map +1 -0
  13. package/dist/{index-C6B_Nm3i.d.cts → index-B4gKjLYR.d.ts} +117 -6
  14. package/dist/index-B4gKjLYR.d.ts.map +1 -0
  15. package/dist/index.cjs +68 -12
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +49 -2
  18. package/dist/index.d.cts.map +1 -1
  19. package/dist/index.d.ts +49 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +45 -10
  22. package/dist/index.js.map +1 -1
  23. package/dist/production/core/index.cjs +16 -0
  24. package/dist/production/core/index.js +1 -0
  25. package/dist/{deferred-task-5kfztF2G.js → production/deferred-task-DkHdsliy.js} +227 -99
  26. package/dist/production/deferred-task-DkHdsliy.js.map +1 -0
  27. package/dist/{deferred-task-BXVzapQg.cjs → production/deferred-task-yh5pWlSj.cjs} +227 -99
  28. package/dist/production/deferred-task-yh5pWlSj.cjs.map +1 -0
  29. package/dist/production/index.cjs +216 -0
  30. package/dist/production/index.cjs.map +1 -0
  31. package/dist/production/index.js +193 -0
  32. package/dist/production/index.js.map +1 -0
  33. package/package.json +69 -23
  34. package/dist/deferred-task-5kfztF2G.js.map +0 -1
  35. package/dist/deferred-task-BXVzapQg.cjs.map +0 -1
  36. package/dist/index-C6B_Nm3i.d.cts.map +0 -1
  37. package/dist/index-C6B_Nm3i.d.ts.map +0 -1
@@ -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