react-inp-chunker 0.0.0-stage → 0.1.0

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