react-inp-chunker 0.3.0 → 0.4.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 (38) hide show
  1. package/README.md +31 -11
  2. package/dist/core/index.cjs +1 -1
  3. package/dist/core/index.d.cts +1 -1
  4. package/dist/core/index.d.ts +1 -1
  5. package/dist/core/index.js +1 -1
  6. package/dist/{deferred-task-6oTEnHWa.js → deferred-task-Cai4fF4m.js} +5 -3
  7. package/dist/deferred-task-Cai4fF4m.js.map +1 -0
  8. package/dist/{deferred-task-DcnC99_Y.cjs → deferred-task-DRigxydg.cjs} +5 -3
  9. package/dist/deferred-task-DRigxydg.cjs.map +1 -0
  10. package/dist/{index-B4gKjLYR.d.ts → index-DuUMqnsA.d.cts} +89 -45
  11. package/dist/index-DuUMqnsA.d.cts.map +1 -0
  12. package/dist/{index-B4gKjLYR.d.cts → index-DuUMqnsA.d.ts} +89 -45
  13. package/dist/index-DuUMqnsA.d.ts.map +1 -0
  14. package/dist/index.cjs +11 -5
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +72 -11
  17. package/dist/index.d.cts.map +1 -1
  18. package/dist/index.d.ts +72 -11
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +11 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/production/core/index.cjs +1 -1
  23. package/dist/production/core/index.js +1 -1
  24. package/dist/production/{deferred-task-DkHdsliy.js → deferred-task-MsTNINAd.js} +5 -3
  25. package/dist/production/deferred-task-MsTNINAd.js.map +1 -0
  26. package/dist/production/{deferred-task-yh5pWlSj.cjs → deferred-task-j8KF7lk7.cjs} +5 -3
  27. package/dist/production/deferred-task-j8KF7lk7.cjs.map +1 -0
  28. package/dist/production/index.cjs +11 -5
  29. package/dist/production/index.cjs.map +1 -1
  30. package/dist/production/index.js +11 -5
  31. package/dist/production/index.js.map +1 -1
  32. package/package.json +6 -3
  33. package/dist/deferred-task-6oTEnHWa.js.map +0 -1
  34. package/dist/deferred-task-DcnC99_Y.cjs.map +0 -1
  35. package/dist/index-B4gKjLYR.d.cts.map +0 -1
  36. package/dist/index-B4gKjLYR.d.ts.map +0 -1
  37. package/dist/production/deferred-task-DkHdsliy.js.map +0 -1
  38. package/dist/production/deferred-task-yh5pWlSj.cjs.map +0 -1
@@ -183,10 +183,12 @@ interface SkippedChunkItem {
183
183
  readonly index: number;
184
184
  readonly error: unknown;
185
185
  }
186
- interface ChunkSchedulerResult<R> {
187
- readonly status: 'done' | 'cancelled' | 'error';
188
- readonly results: readonly R[];
189
- readonly error?: unknown;
186
+ /**
187
+ * Fields every status shares. Split out so the union below only has to spell
188
+ * out what actually differs per status: whether `results` is guaranteed yet,
189
+ * and whether `error` is.
190
+ */
191
+ interface ChunkResultShared {
190
192
  /** Items `onError` skipped. Omitted when nothing was skipped. */
191
193
  readonly skipped?: readonly SkippedChunkItem[];
192
194
  /**
@@ -195,29 +197,47 @@ interface ChunkSchedulerResult<R> {
195
197
  */
196
198
  readonly reason?: unknown;
197
199
  }
198
- interface ChunkSnapshot<R = unknown> {
199
- readonly status: ChunkStatus;
200
+ /**
201
+ * Discriminated on `status`: narrowing to `'error'` narrows `error` to
202
+ * `unknown` (defined), and narrowing to `'done' | 'cancelled'` rules it out
203
+ * entirely — `results` is a plain array either way, never `undefined`, since
204
+ * a settled task has always published it by the time anyone can observe
205
+ * this type.
206
+ */
207
+ type ChunkSchedulerResult<R> = ChunkResultShared & ({
208
+ readonly status: 'done' | 'cancelled';
209
+ readonly results: readonly R[];
210
+ readonly error?: undefined;
211
+ } | {
212
+ readonly status: 'error';
213
+ readonly results: readonly R[];
214
+ readonly error: unknown;
215
+ });
216
+ /**
217
+ * Discriminated on `status`: narrowing to `'done' | 'cancelled' | 'error'`
218
+ * narrows `results` to a defined array — exactly the statuses `isTerminal()`
219
+ * recognizes, and exactly when the engine has actually published it. Narrow
220
+ * to `'error'` specifically to also narrow `error` itself to defined.
221
+ */
222
+ type ChunkSnapshot<R = unknown> = ChunkResultShared & ({
223
+ readonly status: 'idle' | 'processing' | 'paused';
200
224
  readonly processed: number;
201
225
  readonly total: number;
202
- readonly error?: unknown;
203
- /**
204
- * Present only once the task has settled, and always in the same
205
- * snapshot that first reports a terminal status — so a reader can never
206
- * observe `status: 'done'` without the results that go with it.
207
- */
208
- readonly results?: readonly R[];
209
- /**
210
- * Items `onError` has skipped so far — growing live as skips happen,
211
- * unlike `results` above: there's nothing unsafe about reading this
212
- * mid-run. Omitted entirely until the first skip.
213
- */
214
- readonly skipped?: readonly SkippedChunkItem[];
215
- /**
216
- * The `AbortSignal`'s `reason`, present as soon as that signal aborts —
217
- * before the task finishes settling, the same as `error` above.
218
- */
219
- readonly reason?: unknown;
220
- }
226
+ readonly results?: undefined;
227
+ readonly error?: undefined;
228
+ } | {
229
+ readonly status: 'done' | 'cancelled';
230
+ readonly processed: number;
231
+ readonly total: number;
232
+ readonly results: readonly R[];
233
+ readonly error?: undefined;
234
+ } | {
235
+ readonly status: 'error';
236
+ readonly processed: number;
237
+ readonly total: number;
238
+ readonly results: readonly R[];
239
+ readonly error: unknown;
240
+ });
221
241
  interface ChunkScheduler<R> {
222
242
  start(): void;
223
243
  pause(): void;
@@ -300,31 +320,55 @@ interface DeferredTaskOptions<T> {
300
320
  /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */
301
321
  readonly onYield?: () => void;
302
322
  }
303
- interface DeferredTaskResult<T> {
304
- readonly status: 'done' | 'cancelled' | 'error';
305
- readonly value?: T;
306
- readonly error?: unknown;
323
+ /** Fields every status shares — just `reason`, since `error` and `value` differ by status. */
324
+ interface DeferredResultShared {
307
325
  /**
308
326
  * The `AbortSignal`'s `reason`, present only when this cancellation came
309
327
  * from that signal aborting rather than a direct `cancel()` call.
310
328
  */
311
329
  readonly reason?: unknown;
312
330
  }
313
- interface DeferredTaskSnapshot<T = unknown> {
314
- readonly status: ChunkStatus;
315
- readonly error?: unknown;
316
- /**
317
- * Present only once the task has settled with `done`, and always in the
318
- * same snapshot that first reports it — so a reader can never observe
319
- * `status: 'done'` without the value that goes with it.
320
- */
321
- readonly value?: T;
322
- /**
323
- * The `AbortSignal`'s `reason`, present as soon as that signal aborts —
324
- * before the task finishes settling, the same as `error` above.
325
- */
326
- readonly reason?: unknown;
327
- }
331
+ /**
332
+ * Discriminated on `status`: narrowing to `'done'` narrows `value` to `T`
333
+ * itself, not `T | undefined` — including when `T` allows `undefined`, a
334
+ * generator genuinely returning it is still a value, distinct from "no
335
+ * value yet". Narrowing to `'error'` narrows `error` to defined instead.
336
+ */
337
+ type DeferredTaskResult<T> = DeferredResultShared & ({
338
+ readonly status: 'done';
339
+ readonly value: T;
340
+ readonly error?: undefined;
341
+ } | {
342
+ readonly status: 'cancelled';
343
+ readonly value?: undefined;
344
+ readonly error?: undefined;
345
+ } | {
346
+ readonly status: 'error';
347
+ readonly value?: undefined;
348
+ readonly error: unknown;
349
+ });
350
+ /**
351
+ * Same discrimination as `DeferredTaskResult`, plus the non-terminal
352
+ * statuses a live snapshot can still be in, where neither `value` nor
353
+ * `error` exists yet.
354
+ */
355
+ type DeferredTaskSnapshot<T = unknown> = DeferredResultShared & ({
356
+ readonly status: 'idle' | 'processing' | 'paused';
357
+ readonly value?: undefined;
358
+ readonly error?: undefined;
359
+ } | {
360
+ readonly status: 'done';
361
+ readonly value: T;
362
+ readonly error?: undefined;
363
+ } | {
364
+ readonly status: 'cancelled';
365
+ readonly value?: undefined;
366
+ readonly error?: undefined;
367
+ } | {
368
+ readonly status: 'error';
369
+ readonly value?: undefined;
370
+ readonly error: unknown;
371
+ });
328
372
  interface DeferredTask<T> {
329
373
  start(): void;
330
374
  pause(): void;
@@ -345,4 +389,4 @@ interface DeferredTask<T> {
345
389
  */
346
390
  declare function createDeferredTask<T>(options: DeferredTaskOptions<T>): DeferredTask<T>;
347
391
  export { isTerminal as A, supportsSchedulerYield as C, ChunkStatus as D, ChunkMachineState as E, TerminalChunkStatus as O, supportsMessageChannel as S, ChunkEvent as T, isInputPending as _, DeferredTaskSnapshot as a, shouldYieldNow as b, ChunkSchedulerOptions as c, SkippedChunkItem as d, createChunkScheduler as f, YieldToMainOptions as g, YieldStrategy as h, DeferredTaskResult as i, transition as j, initialChunkState as k, ChunkSchedulerResult as l, ShouldYieldOptions as m, DeferredTask as n, createDeferredTask as o, DEFAULT_TIME_SLICE_MS as p, DeferredTaskOptions as r, ChunkScheduler as s, DeferredGenerator as t, ChunkSnapshot as u, now as v, yieldToMain as w, supportsInputPending as x, resolveYieldStrategy as y };
348
- //# sourceMappingURL=index-B4gKjLYR.d.ts.map
392
+ //# sourceMappingURL=index-DuUMqnsA.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-DuUMqnsA.d.cts","names":[],"sources":["../src/core/chunk-machine.ts","../src/core/scheduler.ts","../src/core/chunk-scheduler.ts","../src/core/deferred-task.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;KAyBY;KAGA;WACG;;WACA;;WACA;;WACA;;WACA;;WACA;WAAwB;;UAEtB;WACN,QAAQ;WACR;;cAGE,mBAAmB;KAEpB;iBAQI,WAAW,QAAQ,cAAc,UAAU;;;;;iBA0D3C,WACd,OAAO,mBACP,OAAO,aACN;;;;;;;;;;;;;;;;;;;;;;;;KCxFS;;cAGC;;iBAiBG;;iBAMA;;iBAKA;;;;;;iBAyBA;;iBAUA,wBAAwB;UAkCvB;;;;;;EAMf,WAAW;;;;;;;;;;iBAWS,YAAY,UAAU,qBAAqB;;iBA8BjD;UAgCC;;EAEf;;EAEA;;;;;;;iBAQc,eAAe,SAAS;UCrMvB,sBAAsB,GAAG;WAC/B,gBAAgB;WAChB,cAAc,MAAM,GAAG,kBAAkB;;WAEzC;;;;;;;;;;;;;;WAcA;;;;;;;;WAQA;;WAEA,UAAU,QAAQ,GAAG,MAAM,GAAG;;;;;;;;;;;;;WAa9B,WAAW,gBAAgB,MAAM,GAAG;;WAEpC,SAAS;;WAET,gBAAgB;;;;;;;;;WAShB,WAAW,oBAAoB;;WAE/B;;;UAIM;WACN;WACA;;;;;;;UAQD;;WAEC,mBAAmB;;;;;WAKnB;;;;;;;;;KAUC,qBAAqB,KAAK;WAGrB;WACA,kBAAkB;WAClB;;WAGA;WACA,kBAAkB;WAClB;;;;;;;;KAUL,cAAc,eAAe;WAGxB;WACA;WACA;WACA;WACA;;WAGA;WACA;WACA;WACA,kBAAkB;WAClB;;WAGA;WACA;WACA;WACA,kBAAkB;WAClB;;UAIA,eAAe;EAC9B;EACA;EACA;EACA;EACA,eAAe,cAAc;;;;;;;;;EAS7B,uBAAuB;EACvB,UAAU;;WAED,SAAS,QAAQ,qBAAqB;;;;;;;;;;;;;;iBA2CjC,qBAAqB,GAAG,GACtC,SAAS,sBAAsB,GAAG,KACjC,eAAe;;;;;;;;;;;;;;;;;;;;;;KC7KN,kBAAkB,KAAK,gBAAgB;UAElC,oBAAoB;WAC1B,YAAY,kBAAkB;;WAE9B;;;;;;;;;;;WAWA;WACA,SAAS;;WAET,gBAAgB;;;;;;;WAOhB,WAAW,oBAAoB;;WAE/B;;;UAID;;;;;WAKC;;;;;;;;KASC,mBAAmB,KAAK;WAEnB;WAAyB,OAAO;WAAY;;WAE5C;WACA;WACA;;WAEA;WAA0B;WAA4B;;;;;;;KAQ3D,qBAAqB,eAAe;WAG/B;WACA;WACA;;WAEA;WAAyB,OAAO;WAAY;;WAE5C;WACA;WACA;;WAEA;WAA0B;WAA4B;;UAGtD,aAAa;EAC5B;EACA;EACA;EACA;EACA,eAAe,qBAAqB;;EAEpC,YAAY;EACZ,UAAU;WACD,SAAS,QAAQ,mBAAmB;;;;;;;;;iBAU/B,mBAAmB,GAAG,SAAS,oBAAoB,KAAK,aAAa"}
@@ -183,10 +183,12 @@ interface SkippedChunkItem {
183
183
  readonly index: number;
184
184
  readonly error: unknown;
185
185
  }
186
- interface ChunkSchedulerResult<R> {
187
- readonly status: 'done' | 'cancelled' | 'error';
188
- readonly results: readonly R[];
189
- readonly error?: unknown;
186
+ /**
187
+ * Fields every status shares. Split out so the union below only has to spell
188
+ * out what actually differs per status: whether `results` is guaranteed yet,
189
+ * and whether `error` is.
190
+ */
191
+ interface ChunkResultShared {
190
192
  /** Items `onError` skipped. Omitted when nothing was skipped. */
191
193
  readonly skipped?: readonly SkippedChunkItem[];
192
194
  /**
@@ -195,29 +197,47 @@ interface ChunkSchedulerResult<R> {
195
197
  */
196
198
  readonly reason?: unknown;
197
199
  }
198
- interface ChunkSnapshot<R = unknown> {
199
- readonly status: ChunkStatus;
200
+ /**
201
+ * Discriminated on `status`: narrowing to `'error'` narrows `error` to
202
+ * `unknown` (defined), and narrowing to `'done' | 'cancelled'` rules it out
203
+ * entirely — `results` is a plain array either way, never `undefined`, since
204
+ * a settled task has always published it by the time anyone can observe
205
+ * this type.
206
+ */
207
+ type ChunkSchedulerResult<R> = ChunkResultShared & ({
208
+ readonly status: 'done' | 'cancelled';
209
+ readonly results: readonly R[];
210
+ readonly error?: undefined;
211
+ } | {
212
+ readonly status: 'error';
213
+ readonly results: readonly R[];
214
+ readonly error: unknown;
215
+ });
216
+ /**
217
+ * Discriminated on `status`: narrowing to `'done' | 'cancelled' | 'error'`
218
+ * narrows `results` to a defined array — exactly the statuses `isTerminal()`
219
+ * recognizes, and exactly when the engine has actually published it. Narrow
220
+ * to `'error'` specifically to also narrow `error` itself to defined.
221
+ */
222
+ type ChunkSnapshot<R = unknown> = ChunkResultShared & ({
223
+ readonly status: 'idle' | 'processing' | 'paused';
200
224
  readonly processed: number;
201
225
  readonly total: number;
202
- readonly error?: unknown;
203
- /**
204
- * Present only once the task has settled, and always in the same
205
- * snapshot that first reports a terminal status — so a reader can never
206
- * observe `status: 'done'` without the results that go with it.
207
- */
208
- readonly results?: readonly R[];
209
- /**
210
- * Items `onError` has skipped so far — growing live as skips happen,
211
- * unlike `results` above: there's nothing unsafe about reading this
212
- * mid-run. Omitted entirely until the first skip.
213
- */
214
- readonly skipped?: readonly SkippedChunkItem[];
215
- /**
216
- * The `AbortSignal`'s `reason`, present as soon as that signal aborts —
217
- * before the task finishes settling, the same as `error` above.
218
- */
219
- readonly reason?: unknown;
220
- }
226
+ readonly results?: undefined;
227
+ readonly error?: undefined;
228
+ } | {
229
+ readonly status: 'done' | 'cancelled';
230
+ readonly processed: number;
231
+ readonly total: number;
232
+ readonly results: readonly R[];
233
+ readonly error?: undefined;
234
+ } | {
235
+ readonly status: 'error';
236
+ readonly processed: number;
237
+ readonly total: number;
238
+ readonly results: readonly R[];
239
+ readonly error: unknown;
240
+ });
221
241
  interface ChunkScheduler<R> {
222
242
  start(): void;
223
243
  pause(): void;
@@ -300,31 +320,55 @@ interface DeferredTaskOptions<T> {
300
320
  /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */
301
321
  readonly onYield?: () => void;
302
322
  }
303
- interface DeferredTaskResult<T> {
304
- readonly status: 'done' | 'cancelled' | 'error';
305
- readonly value?: T;
306
- readonly error?: unknown;
323
+ /** Fields every status shares — just `reason`, since `error` and `value` differ by status. */
324
+ interface DeferredResultShared {
307
325
  /**
308
326
  * The `AbortSignal`'s `reason`, present only when this cancellation came
309
327
  * from that signal aborting rather than a direct `cancel()` call.
310
328
  */
311
329
  readonly reason?: unknown;
312
330
  }
313
- interface DeferredTaskSnapshot<T = unknown> {
314
- readonly status: ChunkStatus;
315
- readonly error?: unknown;
316
- /**
317
- * Present only once the task has settled with `done`, and always in the
318
- * same snapshot that first reports it — so a reader can never observe
319
- * `status: 'done'` without the value that goes with it.
320
- */
321
- readonly value?: T;
322
- /**
323
- * The `AbortSignal`'s `reason`, present as soon as that signal aborts —
324
- * before the task finishes settling, the same as `error` above.
325
- */
326
- readonly reason?: unknown;
327
- }
331
+ /**
332
+ * Discriminated on `status`: narrowing to `'done'` narrows `value` to `T`
333
+ * itself, not `T | undefined` — including when `T` allows `undefined`, a
334
+ * generator genuinely returning it is still a value, distinct from "no
335
+ * value yet". Narrowing to `'error'` narrows `error` to defined instead.
336
+ */
337
+ type DeferredTaskResult<T> = DeferredResultShared & ({
338
+ readonly status: 'done';
339
+ readonly value: T;
340
+ readonly error?: undefined;
341
+ } | {
342
+ readonly status: 'cancelled';
343
+ readonly value?: undefined;
344
+ readonly error?: undefined;
345
+ } | {
346
+ readonly status: 'error';
347
+ readonly value?: undefined;
348
+ readonly error: unknown;
349
+ });
350
+ /**
351
+ * Same discrimination as `DeferredTaskResult`, plus the non-terminal
352
+ * statuses a live snapshot can still be in, where neither `value` nor
353
+ * `error` exists yet.
354
+ */
355
+ type DeferredTaskSnapshot<T = unknown> = DeferredResultShared & ({
356
+ readonly status: 'idle' | 'processing' | 'paused';
357
+ readonly value?: undefined;
358
+ readonly error?: undefined;
359
+ } | {
360
+ readonly status: 'done';
361
+ readonly value: T;
362
+ readonly error?: undefined;
363
+ } | {
364
+ readonly status: 'cancelled';
365
+ readonly value?: undefined;
366
+ readonly error?: undefined;
367
+ } | {
368
+ readonly status: 'error';
369
+ readonly value?: undefined;
370
+ readonly error: unknown;
371
+ });
328
372
  interface DeferredTask<T> {
329
373
  start(): void;
330
374
  pause(): void;
@@ -345,4 +389,4 @@ interface DeferredTask<T> {
345
389
  */
346
390
  declare function createDeferredTask<T>(options: DeferredTaskOptions<T>): DeferredTask<T>;
347
391
  export { isTerminal as A, supportsSchedulerYield as C, ChunkStatus as D, ChunkMachineState as E, TerminalChunkStatus as O, supportsMessageChannel as S, ChunkEvent as T, isInputPending as _, DeferredTaskSnapshot as a, shouldYieldNow as b, ChunkSchedulerOptions as c, SkippedChunkItem as d, createChunkScheduler as f, YieldToMainOptions as g, YieldStrategy as h, DeferredTaskResult as i, transition as j, initialChunkState as k, ChunkSchedulerResult as l, ShouldYieldOptions as m, DeferredTask as n, createDeferredTask as o, DEFAULT_TIME_SLICE_MS as p, DeferredTaskOptions as r, ChunkScheduler as s, DeferredGenerator as t, ChunkSnapshot as u, now as v, yieldToMain as w, supportsInputPending as x, resolveYieldStrategy as y };
348
- //# sourceMappingURL=index-B4gKjLYR.d.cts.map
392
+ //# sourceMappingURL=index-DuUMqnsA.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-DuUMqnsA.d.ts","names":[],"sources":["../src/core/chunk-machine.ts","../src/core/scheduler.ts","../src/core/chunk-scheduler.ts","../src/core/deferred-task.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;KAyBY;KAGA;WACG;;WACA;;WACA;;WACA;;WACA;;WACA;WAAwB;;UAEtB;WACN,QAAQ;WACR;;cAGE,mBAAmB;KAEpB;iBAQI,WAAW,QAAQ,cAAc,UAAU;;;;;iBA0D3C,WACd,OAAO,mBACP,OAAO,aACN;;;;;;;;;;;;;;;;;;;;;;;;KCxFS;;cAGC;;iBAiBG;;iBAMA;;iBAKA;;;;;;iBAyBA;;iBAUA,wBAAwB;UAkCvB;;;;;;EAMf,WAAW;;;;;;;;;;iBAWS,YAAY,UAAU,qBAAqB;;iBA8BjD;UAgCC;;EAEf;;EAEA;;;;;;;iBAQc,eAAe,SAAS;UCrMvB,sBAAsB,GAAG;WAC/B,gBAAgB;WAChB,cAAc,MAAM,GAAG,kBAAkB;;WAEzC;;;;;;;;;;;;;;WAcA;;;;;;;;WAQA;;WAEA,UAAU,QAAQ,GAAG,MAAM,GAAG;;;;;;;;;;;;;WAa9B,WAAW,gBAAgB,MAAM,GAAG;;WAEpC,SAAS;;WAET,gBAAgB;;;;;;;;;WAShB,WAAW,oBAAoB;;WAE/B;;;UAIM;WACN;WACA;;;;;;;UAQD;;WAEC,mBAAmB;;;;;WAKnB;;;;;;;;;KAUC,qBAAqB,KAAK;WAGrB;WACA,kBAAkB;WAClB;;WAGA;WACA,kBAAkB;WAClB;;;;;;;;KAUL,cAAc,eAAe;WAGxB;WACA;WACA;WACA;WACA;;WAGA;WACA;WACA;WACA,kBAAkB;WAClB;;WAGA;WACA;WACA;WACA,kBAAkB;WAClB;;UAIA,eAAe;EAC9B;EACA;EACA;EACA;EACA,eAAe,cAAc;;;;;;;;;EAS7B,uBAAuB;EACvB,UAAU;;WAED,SAAS,QAAQ,qBAAqB;;;;;;;;;;;;;;iBA2CjC,qBAAqB,GAAG,GACtC,SAAS,sBAAsB,GAAG,KACjC,eAAe;;;;;;;;;;;;;;;;;;;;;;KC7KN,kBAAkB,KAAK,gBAAgB;UAElC,oBAAoB;WAC1B,YAAY,kBAAkB;;WAE9B;;;;;;;;;;;WAWA;WACA,SAAS;;WAET,gBAAgB;;;;;;;WAOhB,WAAW,oBAAoB;;WAE/B;;;UAID;;;;;WAKC;;;;;;;;KASC,mBAAmB,KAAK;WAEnB;WAAyB,OAAO;WAAY;;WAE5C;WACA;WACA;;WAEA;WAA0B;WAA4B;;;;;;;KAQ3D,qBAAqB,eAAe;WAG/B;WACA;WACA;;WAEA;WAAyB,OAAO;WAAY;;WAE5C;WACA;WACA;;WAEA;WAA0B;WAA4B;;UAGtD,aAAa;EAC5B;EACA;EACA;EACA;EACA,eAAe,qBAAqB;;EAEpC,YAAY;EACZ,UAAU;WACD,SAAS,QAAQ,mBAAmB;;;;;;;;;iBAU/B,mBAAmB,GAAG,SAAS,oBAAoB,KAAK,aAAa"}
package/dist/index.cjs CHANGED
@@ -20,7 +20,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
20
20
  value: mod,
21
21
  enumerable: true
22
22
  }) : target, mod));
23
- const require_deferred_task = require("./deferred-task-DcnC99_Y.cjs");
23
+ const require_deferred_task = require("./deferred-task-DRigxydg.cjs");
24
24
  let react = require("react");
25
25
  react = __toESM(react, 1);
26
26
  // src/react/task-controller.ts
@@ -77,7 +77,7 @@ const IDLE_STATUS = "idle";
77
77
  * same as any other React dependency.
78
78
  */
79
79
  function useChunkedTask(items, processItem, options) {
80
- const { timeSliceMs, batchSize, collectResults, autoStart = true, onItem, onError, onSlice, onYield } = options ?? {};
80
+ const { timeSliceMs, batchSize, collectResults, autoStart = true, onItem, onError, onSlice, onYield, signal } = options ?? {};
81
81
  const stableProcessItem = useEffectEvent((item, index) => processItem(item, index));
82
82
  const stableOnItem = useEffectEvent((result, item, index) => {
83
83
  onItem?.(result, item, index);
@@ -106,7 +106,8 @@ function useChunkedTask(items, processItem, options) {
106
106
  onYield: stableOnYield,
107
107
  ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
108
108
  ...batchSize !== void 0 ? { batchSize } : {},
109
- ...collectResults !== void 0 ? { collectResults } : {}
109
+ ...collectResults !== void 0 ? { collectResults } : {},
110
+ ...signal !== void 0 ? { signal } : {}
110
111
  });
111
112
  controller.replace(scheduler);
112
113
  if (autoStart) scheduler.start();
@@ -120,6 +121,7 @@ function useChunkedTask(items, processItem, options) {
120
121
  batchSize,
121
122
  collectResults,
122
123
  autoStart,
124
+ signal,
123
125
  controller
124
126
  ]);
125
127
  const start = (0, react.useCallback)(() => controller.getCurrent()?.start(), [controller]);
@@ -133,6 +135,7 @@ function useChunkedTask(items, processItem, options) {
133
135
  error: snapshot.error,
134
136
  results: snapshot.results,
135
137
  skipped: snapshot.skipped ?? NO_SKIPPED,
138
+ reason: snapshot.reason,
136
139
  isIdle: snapshot.status === "idle",
137
140
  isProcessing: snapshot.status === "processing",
138
141
  isPaused: snapshot.status === "paused",
@@ -159,7 +162,7 @@ const IDLE_SNAPSHOT = { status: "idle" };
159
162
  * you would for `useMemo` or `useEffect`.
160
163
  */
161
164
  function useDeferredTask(task, deps, options) {
162
- const { timeSliceMs, batchSize, autoStart = true, onSlice, onYield } = options ?? {};
165
+ const { timeSliceMs, batchSize, autoStart = true, onSlice, onYield, signal } = options ?? {};
163
166
  const stableTask = useEffectEvent(() => task());
164
167
  const stableOnSlice = useEffectEvent((durationMs, segmentsDone) => {
165
168
  onSlice?.(durationMs, segmentsDone);
@@ -175,7 +178,8 @@ function useDeferredTask(task, deps, options) {
175
178
  onSlice: stableOnSlice,
176
179
  onYield: stableOnYield,
177
180
  ...timeSliceMs !== void 0 ? { timeSliceMs } : {},
178
- ...batchSize !== void 0 ? { batchSize } : {}
181
+ ...batchSize !== void 0 ? { batchSize } : {},
182
+ ...signal !== void 0 ? { signal } : {}
179
183
  });
180
184
  controller.replace(deferredTask);
181
185
  if (autoStart) deferredTask.start();
@@ -188,6 +192,7 @@ function useDeferredTask(task, deps, options) {
188
192
  timeSliceMs,
189
193
  batchSize,
190
194
  autoStart,
195
+ signal,
191
196
  controller
192
197
  ]);
193
198
  const start = (0, react.useCallback)(() => controller.getCurrent()?.start(), [controller]);
@@ -198,6 +203,7 @@ function useDeferredTask(task, deps, options) {
198
203
  status: snapshot.status,
199
204
  error: snapshot.error,
200
205
  value: snapshot.value,
206
+ reason: snapshot.reason,
201
207
  isIdle: snapshot.status === "idle",
202
208
  isProcessing: snapshot.status === "processing",
203
209
  isPaused: snapshot.status === "paused",
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["useRef","useCallback","React","useState","useMemo","useSyncExternalStore","createChunkScheduler","useCallback","useState","useSyncExternalStore","createDeferredTask","useCallback"],"sources":["../src/react/task-controller.ts","../src/react/use-effect-event.ts","../src/react/useChunkedTask.ts","../src/react/useDeferredTask.ts"],"sourcesContent":["/**\n * The glue between a task handle (`ChunkScheduler<R>`, `DeferredTask<T>` —\n * anything with `subscribe`/`getSnapshot`) recreated from scratch whenever a\n * hook's deps change, inside an effect, and `useSyncExternalStore`, which\n * wants one stable `subscribe` function for a component's whole lifetime.\n *\n * `useSyncExternalStore` resubscribes whenever the `subscribe` function\n * identity changes — fine for correctness, but it means the naive approach\n * (pass the current task's own `.subscribe` directly) either churns\n * subscriptions every render or, worse, drifts: the component keeps\n * `subscribe`d to whichever task was current when it last resubscribed, not\n * necessarily the one `getSnapshot` is reading from after a swap.\n *\n * A controller sidesteps that: it's created once per hook instance (lazy\n * `useState` initializer in the hook, so it's stable without touching a\n * ref), hands `useSyncExternalStore` a `subscribe`/`getSnapshot` pair that\n * never change identity, and internally forwards to whichever task is\n * \"current\" — re-pointing itself via `replace()` every time the effect\n * swaps in a new one. Generic over the task shape so both `useChunkedTask`\n * and `useDeferredTask` share this one implementation.\n *\n * Every property below is an arrow function, deliberately — not method\n * shorthand — so none of them close over `this`, and `controller.subscribe`\n * can be handed to `useSyncExternalStore` directly as a plain function\n * reference without an unbound-method footgun.\n */\nexport interface SubscribableTask<Snapshot> {\n subscribe(listener: () => void): () => void;\n getSnapshot(): Snapshot;\n}\n\nexport interface TaskController<Task extends SubscribableTask<unknown>> {\n subscribe: (listener: () => void) => () => void;\n getSnapshot: () => ReturnType<Task['getSnapshot']> | undefined;\n getCurrent: () => Task | null;\n /** Swaps in a new task (or none), detaching from the previous one first. */\n replace: (task: Task | null) => void;\n}\n\nexport function createTaskController<\n Task extends SubscribableTask<unknown>,\n>(): TaskController<Task> {\n let current: Task | null = null;\n let detachFromCurrent: (() => void) | undefined;\n const listeners = new Set<() => void>();\n\n const notify = (): void => {\n for (const listener of listeners) listener();\n };\n\n return {\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n getSnapshot: () =>\n current?.getSnapshot() as ReturnType<Task['getSnapshot']> | undefined,\n getCurrent: () => current,\n replace: (task) => {\n detachFromCurrent?.();\n current = task;\n detachFromCurrent = task?.subscribe(notify);\n notify();\n },\n };\n}\n","import * as React from 'react';\nimport { useCallback, useInsertionEffect, useRef } from 'react';\n\n/**\n * `useEffectEvent` only became a stable React export in 19.2. On React 18\n * and 19.0–19.1 it doesn't exist at all — and a named\n * `import { useEffectEvent } from 'react'` then fails outright: at module\n * link time under Node ESM (\"Named export 'useEffectEvent' not found\"), or\n * as \"useEffectEvent is not a function\" on first render in a bundler. Both\n * versions are inside this package's declared peer range, so that has to\n * work, not just type-check.\n *\n * So the native hook is read as a property of the namespace object, where a\n * missing export is plain `undefined` rather than a link error, and used\n * whenever the running React has it. Otherwise the fallback keeps the same\n * contract: a stable function identity that always calls the latest\n * callback. The latest callback is committed from an insertion effect —\n * after render, before any layout or passive effect of the same commit can\n * observe it — so effects always see the current closure, without the\n * render-time ref mutation the React Compiler rejects.\n */\ntype EffectEventHook = <Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n) => (...args: Args) => Return;\n\nexport function useEffectEventFallback<Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n): (...args: Args) => Return {\n const latest = useRef(callback);\n useInsertionEffect(() => {\n latest.current = callback;\n });\n return useCallback((...args: Args) => latest.current(...args), []);\n}\n\n// Through `unknown` on purpose: React 18's types have no `useEffectEvent`\n// member at all, so a direct assertion wouldn't compile against them.\nconst nativeUseEffectEvent = (\n React as unknown as { readonly useEffectEvent?: EffectEventHook }\n).useEffectEvent;\n\nexport const useEffectEvent: EffectEventHook =\n nativeUseEffectEvent ?? useEffectEventFallback;\n","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createChunkScheduler } from '../core/chunk-scheduler.js';\nimport type {\n ChunkScheduler,\n ChunkSnapshot,\n SkippedChunkItem,\n} from '../core/chunk-scheduler.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\n/** Stable empty array so a never-skipped task doesn't hand out a fresh `[]` every render. */\nconst NO_SKIPPED: readonly SkippedChunkItem[] = [];\n\nexport interface UseChunkedTaskOptions<T, R> {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Items to process between clock checks. Default 1. Raise it only for\n * items measured in nanoseconds, where reading the clock costs more than\n * the work; a slice can then run up to `batchSize` items past its budget.\n * See the `batchSize` note on `createChunkScheduler`.\n */\n readonly batchSize?: number;\n /**\n * Keep every result in `results`. Default true. Set false when results\n * are consumed through `onItem`, to avoid retaining one result per item;\n * `results` is then an empty array once the task settles.\n */\n readonly collectResults?: boolean;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n /**\n * Called synchronously after each item is processed. This is the cheap\n * way to consume results incrementally while the task is still running —\n * write into a ref-backed buffer, a canvas, a chart, whatever you need —\n * without paying for an array copy on every yield checkpoint.\n */\n readonly onItem?: (result: R, item: T, index: number) => void;\n /**\n * Called when `processItem` throws. Return `'skip'` to drop that item and\n * keep going — it shows up in `skipped` — or `'abort'` to end the task as\n * an error, carrying whatever was collected before the throw. Omitting\n * this defaults to `'abort'`, so existing callers see no behavior change.\n */\n readonly onError?: (error: unknown, item: T, index: number) => 'skip' | 'abort';\n /**\n * Called every time a slice ends and the task is about to yield, with how\n * long that slice ran and how many items are done so far. Ships in the\n * production build, unlike this package's own dev-only diagnostics — wire\n * it to your own telemetry to verify, on real traffic, that this task is\n * actually staying under `timeSliceMs`.\n */\n readonly onSlice?: (durationMs: number, processed: number) => void;\n /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */\n readonly onYield?: () => void;\n}\n\nexport interface UseChunkedTaskState<R> {\n readonly status: ChunkStatus;\n readonly processed: number;\n readonly total: number;\n readonly error: unknown;\n /** The full results array once the task has settled — `undefined` until then. */\n readonly results: readonly R[] | undefined;\n /** Items `onError` has skipped so far, growing live as skips happen. Empty when unused. */\n readonly skipped: readonly SkippedChunkItem[];\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\nconst IDLE_STATUS: ChunkStatus = 'idle';\n\n/**\n * Chunks a heavy, synchronous per-item computation over `items` so it never\n * blocks the main thread for longer than a time slice — the thing that\n * costs you Interaction to Next Paint. Starts automatically by default;\n * pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.\n * behind a button).\n *\n * `processItem` and `onItem` can be fresh closures every render — the hook\n * always calls the latest version (via `useEffectEvent`) without that\n * causing the task to restart. What *does* restart the task is `items`\n * changing identity (a new array reference), or `timeSliceMs`/`autoStart`\n * changing. That's deliberate: a new `items` array means new work, and\n * memoizing it when you don't want that is the caller's call to make, the\n * same as any other React dependency.\n */\nexport function useChunkedTask<T, R>(\n items: readonly T[],\n processItem: (item: T, index: number) => R,\n options?: UseChunkedTaskOptions<T, R>,\n): UseChunkedTaskState<R> {\n const {\n timeSliceMs,\n batchSize,\n collectResults,\n autoStart = true,\n onItem,\n onError,\n onSlice,\n onYield,\n } = options ?? {};\n\n // Stable identity, always runs the latest processItem/onItem passed this\n // render — the React-Compiler-safe replacement for a ref mutated during\n // render. Effect Events are meant to be called from inside an effect\n // (here: from the scheduler's async loop, which the effect below owns),\n // never during render itself.\n const stableProcessItem = useEffectEvent((item: T, index: number) =>\n processItem(item, index),\n );\n const stableOnItem = useEffectEvent((result: R, item: T, index: number) => {\n onItem?.(result, item, index);\n });\n const stableOnError = useEffectEvent(\n (error: unknown, item: T, index: number): 'skip' | 'abort' =>\n onError?.(error, item, index) ?? 'abort',\n );\n const stableOnSlice = useEffectEvent((durationMs: number, processed: number) => {\n onSlice?.(durationMs, processed);\n });\n const stableOnYield = useEffectEvent(() => {\n onYield?.();\n });\n\n // Created once, lazily, via useState rather than a ref mutated during\n // render — the controller itself is inert (no subscriptions, no timers)\n // until an effect calls replace() on it, so a discarded render (double\n // render in dev, or one React throws away) never leaks anything.\n const [controller] = useState(() => createTaskController<ChunkScheduler<R>>());\n\n // What getSnapshot() reports before the effect below has ever run —\n // including during SSR, where effects never run at all.\n const idleSnapshot = useMemo<ChunkSnapshot<R>>(\n () => ({ status: IDLE_STATUS, processed: 0, total: items.length }),\n [items.length],\n );\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? idleSnapshot,\n () => idleSnapshot,\n );\n\n useEffect(() => {\n const scheduler = createChunkScheduler<T, R>({\n items,\n processItem: stableProcessItem,\n onItem: stableOnItem,\n onError: stableOnError,\n onSlice: stableOnSlice,\n onYield: stableOnYield,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n ...(collectResults !== undefined ? { collectResults } : {}),\n });\n controller.replace(scheduler);\n if (autoStart) scheduler.start();\n\n return () => {\n // Safe even if the task already finished on its own — cancel() on a\n // terminal scheduler is a documented no-op, not an error.\n scheduler.cancel();\n controller.replace(null);\n };\n // stableProcessItem/stableOnItem/stableOnError/stableOnSlice/stableOnYield\n // are Effect Events: stable by construction, intentionally excluded\n // from the reactive dependency set\n // (that's the whole point of useEffectEvent) rather than omitted by\n // oversight.\n }, [items, timeSliceMs, batchSize, collectResults, autoStart, controller]);\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n return {\n status: snapshot.status,\n processed: snapshot.processed,\n total: snapshot.total,\n error: snapshot.error,\n // From the snapshot, not a direct read of the scheduler: the engine\n // publishes results before it announces a terminal status, so the two\n // always arrive together and a concurrent render can never tear them\n // apart.\n results: snapshot.results,\n skipped: snapshot.skipped ?? NO_SKIPPED,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n };\n}\n","import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createDeferredTask } from '../core/deferred-task.js';\nimport type {\n DeferredGenerator,\n DeferredTask,\n DeferredTaskSnapshot,\n} from '../core/deferred-task.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\nexport interface UseDeferredTaskOptions {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Generator segments to run between clock checks. Default 1. Raise it\n * only for segments measured in nanoseconds; a slice can then run up to\n * `batchSize` segments past its budget.\n */\n readonly batchSize?: number;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n /**\n * Called every time a segment-batch ends and the task is about to yield,\n * with how long that slice ran and how many segments are done so far.\n * Ships in the production build — see the identical option on\n * `useChunkedTask`/`createChunkScheduler` for why.\n */\n readonly onSlice?: (durationMs: number, segmentsDone: number) => void;\n /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */\n readonly onYield?: () => void;\n}\n\nexport interface UseDeferredTaskState<T> {\n readonly status: ChunkStatus;\n readonly error: unknown;\n /** The returned value once the task settles with `done` — `undefined` until then. */\n readonly value: T | undefined;\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\nconst IDLE_SNAPSHOT: DeferredTaskSnapshot<never> = { status: 'idle' };\n\n/**\n * Runs an arbitrary heavy, synchronous generator function in chunks, so it\n * never produces a long task on its own. Where `useChunkedTask` chunks \"one\n * function applied to every item of an array\" automatically, this hook\n * leaves the chunk boundaries to you — put `yield` wherever your function\n * can safely pause (see `DeferredGenerator`'s doc comment for an example).\n *\n * `task` is only called when the effect (re)creates the underlying task, not\n * on every render — list whatever it closes over in `deps`, the same way\n * you would for `useMemo` or `useEffect`.\n */\nexport function useDeferredTask<T>(\n task: () => DeferredGenerator<T>,\n deps: readonly unknown[],\n options?: UseDeferredTaskOptions,\n): UseDeferredTaskState<T> {\n const { timeSliceMs, batchSize, autoStart = true, onSlice, onYield } = options ?? {};\n\n // Stable identity, always runs the latest `task` passed this render —\n // see useChunkedTask.ts for why this is useEffectEvent rather than a ref\n // mutated during render.\n const stableTask = useEffectEvent(() => task());\n const stableOnSlice = useEffectEvent((durationMs: number, segmentsDone: number) => {\n onSlice?.(durationMs, segmentsDone);\n });\n const stableOnYield = useEffectEvent(() => {\n onYield?.();\n });\n\n const [controller] = useState(() => createTaskController<DeferredTask<T>>());\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? IDLE_SNAPSHOT,\n () => IDLE_SNAPSHOT,\n );\n\n useEffect(\n () => {\n const deferredTask = createDeferredTask<T>({\n task: stableTask,\n onSlice: stableOnSlice,\n onYield: stableOnYield,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n });\n controller.replace(deferredTask);\n if (autoStart) deferredTask.start();\n\n return () => {\n deferredTask.cancel();\n controller.replace(null);\n };\n },\n // `deps` plays the role `items` plays for useChunkedTask: it's the\n // caller's declaration of \"this is new work\". `stableTask` is an Effect\n // Event — stable by construction, intentionally excluded from the\n // reactive dependency set rather than omitted by oversight. `deps` is a\n // caller-supplied array, the same pattern (and the same reason it can't\n // be statically verified) as useMemo/useEffect's own deps parameter.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [...deps, timeSliceMs, batchSize, autoStart, controller],\n );\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n return {\n status: snapshot.status,\n error: snapshot.error,\n // From the snapshot: the engine publishes the value before it\n // announces `done`, so a concurrent render can never see one without\n // the other. See useChunkedTask.ts.\n value: snapshot.value,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["useRef","useCallback","React","useState","useMemo","useSyncExternalStore","createChunkScheduler","useCallback","useState","useSyncExternalStore","createDeferredTask","useCallback"],"sources":["../src/react/task-controller.ts","../src/react/use-effect-event.ts","../src/react/useChunkedTask.ts","../src/react/useDeferredTask.ts"],"sourcesContent":["/**\n * The glue between a task handle (`ChunkScheduler<R>`, `DeferredTask<T>` —\n * anything with `subscribe`/`getSnapshot`) recreated from scratch whenever a\n * hook's deps change, inside an effect, and `useSyncExternalStore`, which\n * wants one stable `subscribe` function for a component's whole lifetime.\n *\n * `useSyncExternalStore` resubscribes whenever the `subscribe` function\n * identity changes — fine for correctness, but it means the naive approach\n * (pass the current task's own `.subscribe` directly) either churns\n * subscriptions every render or, worse, drifts: the component keeps\n * `subscribe`d to whichever task was current when it last resubscribed, not\n * necessarily the one `getSnapshot` is reading from after a swap.\n *\n * A controller sidesteps that: it's created once per hook instance (lazy\n * `useState` initializer in the hook, so it's stable without touching a\n * ref), hands `useSyncExternalStore` a `subscribe`/`getSnapshot` pair that\n * never change identity, and internally forwards to whichever task is\n * \"current\" — re-pointing itself via `replace()` every time the effect\n * swaps in a new one. Generic over the task shape so both `useChunkedTask`\n * and `useDeferredTask` share this one implementation.\n *\n * Every property below is an arrow function, deliberately — not method\n * shorthand — so none of them close over `this`, and `controller.subscribe`\n * can be handed to `useSyncExternalStore` directly as a plain function\n * reference without an unbound-method footgun.\n */\nexport interface SubscribableTask<Snapshot> {\n subscribe(listener: () => void): () => void;\n getSnapshot(): Snapshot;\n}\n\nexport interface TaskController<Task extends SubscribableTask<unknown>> {\n subscribe: (listener: () => void) => () => void;\n getSnapshot: () => ReturnType<Task['getSnapshot']> | undefined;\n getCurrent: () => Task | null;\n /** Swaps in a new task (or none), detaching from the previous one first. */\n replace: (task: Task | null) => void;\n}\n\nexport function createTaskController<\n Task extends SubscribableTask<unknown>,\n>(): TaskController<Task> {\n let current: Task | null = null;\n let detachFromCurrent: (() => void) | undefined;\n const listeners = new Set<() => void>();\n\n const notify = (): void => {\n for (const listener of listeners) listener();\n };\n\n return {\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n getSnapshot: () =>\n current?.getSnapshot() as ReturnType<Task['getSnapshot']> | undefined,\n getCurrent: () => current,\n replace: (task) => {\n detachFromCurrent?.();\n current = task;\n detachFromCurrent = task?.subscribe(notify);\n notify();\n },\n };\n}\n","import * as React from 'react';\nimport { useCallback, useInsertionEffect, useRef } from 'react';\n\n/**\n * `useEffectEvent` only became a stable React export in 19.2. On React 18\n * and 19.0–19.1 it doesn't exist at all — and a named\n * `import { useEffectEvent } from 'react'` then fails outright: at module\n * link time under Node ESM (\"Named export 'useEffectEvent' not found\"), or\n * as \"useEffectEvent is not a function\" on first render in a bundler. Both\n * versions are inside this package's declared peer range, so that has to\n * work, not just type-check.\n *\n * So the native hook is read as a property of the namespace object, where a\n * missing export is plain `undefined` rather than a link error, and used\n * whenever the running React has it. Otherwise the fallback keeps the same\n * contract: a stable function identity that always calls the latest\n * callback. The latest callback is committed from an insertion effect —\n * after render, before any layout or passive effect of the same commit can\n * observe it — so effects always see the current closure, without the\n * render-time ref mutation the React Compiler rejects.\n */\ntype EffectEventHook = <Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n) => (...args: Args) => Return;\n\nexport function useEffectEventFallback<Args extends readonly unknown[], Return>(\n callback: (...args: Args) => Return,\n): (...args: Args) => Return {\n const latest = useRef(callback);\n useInsertionEffect(() => {\n latest.current = callback;\n });\n return useCallback((...args: Args) => latest.current(...args), []);\n}\n\n// Through `unknown` on purpose: React 18's types have no `useEffectEvent`\n// member at all, so a direct assertion wouldn't compile against them.\nconst nativeUseEffectEvent = (\n React as unknown as { readonly useEffectEvent?: EffectEventHook }\n).useEffectEvent;\n\nexport const useEffectEvent: EffectEventHook =\n nativeUseEffectEvent ?? useEffectEventFallback;\n","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createChunkScheduler } from '../core/chunk-scheduler.js';\nimport type {\n ChunkScheduler,\n ChunkSnapshot,\n SkippedChunkItem,\n} from '../core/chunk-scheduler.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\n/** Stable empty array so a never-skipped task doesn't hand out a fresh `[]` every render. */\nconst NO_SKIPPED: readonly SkippedChunkItem[] = [];\n\nexport interface UseChunkedTaskOptions<T, R> {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Items to process between clock checks. Default 1. Raise it only for\n * items measured in nanoseconds, where reading the clock costs more than\n * the work; a slice can then run up to `batchSize` items past its budget.\n * See the `batchSize` note on `createChunkScheduler`.\n */\n readonly batchSize?: number;\n /**\n * Keep every result in `results`. Default true. Set false when results\n * are consumed through `onItem`, to avoid retaining one result per item;\n * `results` is then an empty array once the task settles.\n */\n readonly collectResults?: boolean;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n /**\n * Called synchronously after each item is processed. This is the cheap\n * way to consume results incrementally while the task is still running —\n * write into a ref-backed buffer, a canvas, a chart, whatever you need —\n * without paying for an array copy on every yield checkpoint.\n */\n readonly onItem?: (result: R, item: T, index: number) => void;\n /**\n * Called when `processItem` throws. Return `'skip'` to drop that item and\n * keep going — it shows up in `skipped` — or `'abort'` to end the task as\n * an error, carrying whatever was collected before the throw. Omitting\n * this defaults to `'abort'`, so existing callers see no behavior change.\n */\n readonly onError?: (error: unknown, item: T, index: number) => 'skip' | 'abort';\n /**\n * Called every time a slice ends and the task is about to yield, with how\n * long that slice ran and how many items are done so far. Ships in the\n * production build, unlike this package's own dev-only diagnostics — wire\n * it to your own telemetry to verify, on real traffic, that this task is\n * actually staying under `timeSliceMs`.\n */\n readonly onSlice?: (durationMs: number, processed: number) => void;\n /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */\n readonly onYield?: () => void;\n /**\n * External cancellation source — aborting it is equivalent to calling\n * `cancel()`, and its `reason` comes through on `reason` below, so a\n * cancellation caused by the signal is distinguishable from a direct\n * `cancel()` call. A new reference restarts the task the same way a new\n * `items` array does, so memoize it (e.g. from a ref or a parent-owned\n * `AbortController`) unless you mean to restart on every render.\n */\n readonly signal?: AbortSignal;\n}\n\n/** Fields every status shares, independent of the `status`/`results`/`error` narrowing below. */\ninterface UseChunkedTaskShared {\n readonly processed: number;\n readonly total: number;\n /** Items `onError` has skipped so far, growing live as skips happen. Empty when unused. */\n readonly skipped: readonly SkippedChunkItem[];\n /**\n * The `signal` option's `reason`, present only when cancellation came\n * from that signal aborting rather than a direct `cancel()` call.\n */\n readonly reason: unknown;\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\n/**\n * Discriminated on `status`. Narrowing to `'done'` or `'cancelled'` narrows\n * `results` to a defined array, no `=== undefined` check needed alongside\n * `isDone`; narrowing to `'error'` narrows `error` to defined instead. The\n * `is*` booleans above remain the simple way to branch when you only care\n * about one status and don't need the narrowing.\n */\nexport type UseChunkedTaskState<R> = UseChunkedTaskShared &\n (\n | {\n readonly status: Exclude<ChunkStatus, 'done' | 'cancelled' | 'error'>;\n readonly results: undefined;\n readonly error: undefined;\n }\n | {\n readonly status: 'done' | 'cancelled';\n readonly results: readonly R[];\n readonly error: undefined;\n }\n | {\n readonly status: 'error';\n readonly results: readonly R[];\n readonly error: unknown;\n }\n );\n\n// Narrowed to the literal, not widened to `ChunkStatus`, so the object\n// literal below satisfies the discriminated `ChunkSnapshot` union on its\n// own merits — no assertion needed for a status this function knows for sure.\nconst IDLE_STATUS = 'idle';\n\n/**\n * Chunks a heavy, synchronous per-item computation over `items` so it never\n * blocks the main thread for longer than a time slice — the thing that\n * costs you Interaction to Next Paint. Starts automatically by default;\n * pass `{ autoStart: false }` to wait for an explicit `start()` call (e.g.\n * behind a button).\n *\n * `processItem` and `onItem` can be fresh closures every render — the hook\n * always calls the latest version (via `useEffectEvent`) without that\n * causing the task to restart. What *does* restart the task is `items`\n * changing identity (a new array reference), or `timeSliceMs`/`autoStart`\n * changing. That's deliberate: a new `items` array means new work, and\n * memoizing it when you don't want that is the caller's call to make, the\n * same as any other React dependency.\n */\nexport function useChunkedTask<T, R>(\n items: readonly T[],\n processItem: (item: T, index: number) => R,\n options?: UseChunkedTaskOptions<T, R>,\n): UseChunkedTaskState<R> {\n const {\n timeSliceMs,\n batchSize,\n collectResults,\n autoStart = true,\n onItem,\n onError,\n onSlice,\n onYield,\n signal,\n } = options ?? {};\n\n // Stable identity, always runs the latest processItem/onItem passed this\n // render — the React-Compiler-safe replacement for a ref mutated during\n // render. Effect Events are meant to be called from inside an effect\n // (here: from the scheduler's async loop, which the effect below owns),\n // never during render itself.\n const stableProcessItem = useEffectEvent((item: T, index: number) =>\n processItem(item, index),\n );\n const stableOnItem = useEffectEvent((result: R, item: T, index: number) => {\n onItem?.(result, item, index);\n });\n const stableOnError = useEffectEvent(\n (error: unknown, item: T, index: number): 'skip' | 'abort' =>\n onError?.(error, item, index) ?? 'abort',\n );\n const stableOnSlice = useEffectEvent((durationMs: number, processed: number) => {\n onSlice?.(durationMs, processed);\n });\n const stableOnYield = useEffectEvent(() => {\n onYield?.();\n });\n\n // Created once, lazily, via useState rather than a ref mutated during\n // render — the controller itself is inert (no subscriptions, no timers)\n // until an effect calls replace() on it, so a discarded render (double\n // render in dev, or one React throws away) never leaks anything.\n const [controller] = useState(() => createTaskController<ChunkScheduler<R>>());\n\n // What getSnapshot() reports before the effect below has ever run —\n // including during SSR, where effects never run at all.\n const idleSnapshot = useMemo<ChunkSnapshot<R>>(\n () => ({ status: IDLE_STATUS, processed: 0, total: items.length }),\n [items.length],\n );\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? idleSnapshot,\n () => idleSnapshot,\n );\n\n useEffect(() => {\n const scheduler = createChunkScheduler<T, R>({\n items,\n processItem: stableProcessItem,\n onItem: stableOnItem,\n onError: stableOnError,\n onSlice: stableOnSlice,\n onYield: stableOnYield,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n ...(collectResults !== undefined ? { collectResults } : {}),\n ...(signal !== undefined ? { signal } : {}),\n });\n controller.replace(scheduler);\n if (autoStart) scheduler.start();\n\n return () => {\n // Safe even if the task already finished on its own — cancel() on a\n // terminal scheduler is a documented no-op, not an error.\n scheduler.cancel();\n controller.replace(null);\n };\n // stableProcessItem/stableOnItem/stableOnError/stableOnSlice/stableOnYield\n // are Effect Events: stable by construction, intentionally excluded\n // from the reactive dependency set\n // (that's the whole point of useEffectEvent) rather than omitted by\n // oversight.\n }, [items, timeSliceMs, batchSize, collectResults, autoStart, signal, controller]);\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n // Built from `snapshot`'s own wide-`status` fields, so — same as the core\n // engine this wraps — it can't be checked structurally against the\n // discriminated union above; `snapshot` already carries that invariant.\n return {\n status: snapshot.status,\n processed: snapshot.processed,\n total: snapshot.total,\n error: snapshot.error,\n // From the snapshot, not a direct read of the scheduler: the engine\n // publishes results before it announces a terminal status, so the two\n // always arrive together and a concurrent render can never tear them\n // apart.\n results: snapshot.results,\n skipped: snapshot.skipped ?? NO_SKIPPED,\n reason: snapshot.reason,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n } as UseChunkedTaskState<R>;\n}\n","import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';\n\nimport type { ChunkStatus } from '../core/chunk-machine.js';\nimport { createDeferredTask } from '../core/deferred-task.js';\nimport type {\n DeferredGenerator,\n DeferredTask,\n DeferredTaskSnapshot,\n} from '../core/deferred-task.js';\nimport { createTaskController } from './task-controller.js';\nimport { useEffectEvent } from './use-effect-event.js';\n\nexport interface UseDeferredTaskOptions {\n /** Max ms of uninterrupted work before yielding. Default 5. */\n readonly timeSliceMs?: number;\n /**\n * Generator segments to run between clock checks. Default 1. Raise it\n * only for segments measured in nanoseconds; a slice can then run up to\n * `batchSize` segments past its budget.\n */\n readonly batchSize?: number;\n /** Start processing automatically when the task is (re)created. Default `true`. */\n readonly autoStart?: boolean;\n /**\n * Called every time a segment-batch ends and the task is about to yield,\n * with how long that slice ran and how many segments are done so far.\n * Ships in the production build — see the identical option on\n * `useChunkedTask`/`createChunkScheduler` for why.\n */\n readonly onSlice?: (durationMs: number, segmentsDone: number) => void;\n /** Called right after each yield resolves and work is about to resume. Pairs with `onSlice`. */\n readonly onYield?: () => void;\n /**\n * External cancellation source — aborting it is equivalent to calling\n * `cancel()`, and its `reason` comes through on `reason` below, so a\n * cancellation caused by the signal is distinguishable from a direct\n * `cancel()` call. A new reference restarts the task the same way a new\n * entry in `deps` does, so memoize it unless you mean to restart on\n * every render.\n */\n readonly signal?: AbortSignal;\n}\n\n/** Fields every status shares, independent of the `status`/`value`/`error` narrowing below. */\ninterface UseDeferredTaskShared {\n /**\n * The `signal` option's `reason`, present only when cancellation came\n * from that signal aborting rather than a direct `cancel()` call.\n */\n readonly reason: unknown;\n readonly isIdle: boolean;\n readonly isProcessing: boolean;\n readonly isPaused: boolean;\n readonly isDone: boolean;\n readonly isCancelled: boolean;\n readonly isError: boolean;\n readonly start: () => void;\n readonly pause: () => void;\n readonly resume: () => void;\n readonly cancel: () => void;\n}\n\n/**\n * Discriminated on `status`. Narrowing to `'done'` narrows `value` to `T`\n * itself — no `=== undefined` check needed alongside `isDone`, even when\n * `T` allows `undefined` as a genuine return value. Narrowing to `'error'`\n * narrows `error` to defined instead.\n */\nexport type UseDeferredTaskState<T> = UseDeferredTaskShared &\n (\n | {\n readonly status: Exclude<ChunkStatus, 'done' | 'error'>;\n readonly value: undefined;\n readonly error: undefined;\n }\n | { readonly status: 'done'; readonly value: T; readonly error: undefined }\n | { readonly status: 'error'; readonly value: undefined; readonly error: unknown }\n );\n\n// Narrowed to the literal, not widened to `ChunkStatus`, so this satisfies\n// the discriminated `DeferredTaskSnapshot` union on its own merits.\nconst IDLE_SNAPSHOT: DeferredTaskSnapshot<never> = { status: 'idle' };\n\n/**\n * Runs an arbitrary heavy, synchronous generator function in chunks, so it\n * never produces a long task on its own. Where `useChunkedTask` chunks \"one\n * function applied to every item of an array\" automatically, this hook\n * leaves the chunk boundaries to you — put `yield` wherever your function\n * can safely pause (see `DeferredGenerator`'s doc comment for an example).\n *\n * `task` is only called when the effect (re)creates the underlying task, not\n * on every render — list whatever it closes over in `deps`, the same way\n * you would for `useMemo` or `useEffect`.\n */\nexport function useDeferredTask<T>(\n task: () => DeferredGenerator<T>,\n deps: readonly unknown[],\n options?: UseDeferredTaskOptions,\n): UseDeferredTaskState<T> {\n const {\n timeSliceMs,\n batchSize,\n autoStart = true,\n onSlice,\n onYield,\n signal,\n } = options ?? {};\n\n // Stable identity, always runs the latest `task` passed this render —\n // see useChunkedTask.ts for why this is useEffectEvent rather than a ref\n // mutated during render.\n const stableTask = useEffectEvent(() => task());\n const stableOnSlice = useEffectEvent((durationMs: number, segmentsDone: number) => {\n onSlice?.(durationMs, segmentsDone);\n });\n const stableOnYield = useEffectEvent(() => {\n onYield?.();\n });\n\n const [controller] = useState(() => createTaskController<DeferredTask<T>>());\n\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n () => controller.getSnapshot() ?? IDLE_SNAPSHOT,\n () => IDLE_SNAPSHOT,\n );\n\n useEffect(\n () => {\n const deferredTask = createDeferredTask<T>({\n task: stableTask,\n onSlice: stableOnSlice,\n onYield: stableOnYield,\n ...(timeSliceMs !== undefined ? { timeSliceMs } : {}),\n ...(batchSize !== undefined ? { batchSize } : {}),\n ...(signal !== undefined ? { signal } : {}),\n });\n controller.replace(deferredTask);\n if (autoStart) deferredTask.start();\n\n return () => {\n deferredTask.cancel();\n controller.replace(null);\n };\n },\n // `deps` plays the role `items` plays for useChunkedTask: it's the\n // caller's declaration of \"this is new work\". `stableTask` is an Effect\n // Event — stable by construction, intentionally excluded from the\n // reactive dependency set rather than omitted by oversight. `deps` is a\n // caller-supplied array, the same pattern (and the same reason it can't\n // be statically verified) as useMemo/useEffect's own deps parameter.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [...deps, timeSliceMs, batchSize, autoStart, signal, controller],\n );\n\n const start = useCallback(() => controller.getCurrent()?.start(), [controller]);\n const pause = useCallback(() => controller.getCurrent()?.pause(), [controller]);\n const resume = useCallback(() => controller.getCurrent()?.resume(), [controller]);\n const cancel = useCallback(() => controller.getCurrent()?.cancel(), [controller]);\n\n // Built from `snapshot`'s own wide-`status` fields — see the identical\n // note in useChunkedTask.ts; `snapshot` already carries the invariant.\n return {\n status: snapshot.status,\n error: snapshot.error,\n // From the snapshot: the engine publishes the value before it\n // announces `done`, so a concurrent render can never see one without\n // the other. See useChunkedTask.ts.\n value: snapshot.value,\n reason: snapshot.reason,\n isIdle: snapshot.status === 'idle',\n isProcessing: snapshot.status === 'processing',\n isPaused: snapshot.status === 'paused',\n isDone: snapshot.status === 'done',\n isCancelled: snapshot.status === 'cancelled',\n isError: snapshot.status === 'error',\n start,\n pause,\n resume,\n cancel,\n } as UseDeferredTaskState<T>;\n}\n"],"mappings":"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"}