@zingage/scheduling 1.0.0 → 1.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/README.md +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/synchronous-result.d.ts +10 -0
- package/dist/synchronous-result.d.ts.map +1 -0
- package/dist/synchronous-result.js +18 -0
- package/dist/synchronous-result.js.map +1 -0
- package/dist/time-sliced-search.d.ts +47 -0
- package/dist/time-sliced-search.d.ts.map +1 -0
- package/dist/time-sliced-search.js +41 -0
- package/dist/time-sliced-search.js.map +1 -0
- package/dist/time-slicing.d.ts +20 -7
- package/dist/time-slicing.d.ts.map +1 -1
- package/dist/time-slicing.js +14 -8
- package/dist/time-slicing.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/synchronous-result.ts +28 -0
- package/src/time-sliced-search.ts +98 -0
- package/src/time-slicing.ts +50 -26
package/README.md
CHANGED
|
@@ -29,7 +29,11 @@ The package measures elapsed time around synchronous callbacks and result accumu
|
|
|
29
29
|
|
|
30
30
|
If A uses 1 ms and finishes, connected B gets the remaining 3 ms. If A exhausts the budget, A replenishes it before yielding. B can then use the new 4 ms while A waits. A does not replenish the budget when it resumes. Unrelated C uses a separate budget and cannot consume A/B's allowance.
|
|
31
31
|
|
|
32
|
-
One callback or iterator step cannot be interrupted. Nested helpers started inside a synchronous callback cannot interrupt that containing callback. Their work counts once toward a shared budget. Garbage collection and operating-system pauses inside a measured step count as elapsed processing time. This is not CPU-time accounting or a hard event-loop delay bound. Work outside the helpers does not count.
|
|
32
|
+
One callback or iterator step cannot be interrupted. Nested helpers started inside a synchronous callback cannot interrupt that containing callback. Their work counts once toward a shared budget. Garbage collection and operating-system pauses inside a measured step count as elapsed processing time. This is not CPU-time accounting or a hard event-loop delay bound. Work outside the helpers does not count. `timeSlicedMap` and `timeSlicedForEach` also accept generator callbacks.
|
|
33
|
+
|
|
34
|
+
## Synchronous callbacks
|
|
35
|
+
|
|
36
|
+
Ordinary callbacks, predicates, key functions, and `measure` work must be synchronous. The types accept any result, including generic results such as `(item) => item`. TypeScript cannot reject a promise result without also rejecting generic results. So the rule is checked at runtime: if a non-generator callback returns a promise or other thenable (an object or function with a `then` method), the helper throws a `TypeError` before it uses the value. An `async` callback compiles, and throws on its first call. For async work, use a generator callback with `yield* waitFor(...)`.
|
|
33
37
|
|
|
34
38
|
The constructor is `new TimeSliceBudget(sliceMs = DEFAULT_TIME_SLICE_MS, now = performance.now, yieldToEventLoop = setImmediate)`. `sliceMs` must be positive and finite. `now` must be a monotonic clock in milliseconds. The budget binds the clock to `performance` because Node's default clock requires that receiver. The default yield function is Node's promise-based `setImmediate`. A custom yield function must let the event loop advance; `Promise.resolve()` does not provide that behavior. Optionally pass a budget directly as each helper's last argument to share its allowance.
|
|
35
39
|
|
|
@@ -41,6 +45,8 @@ The constructor is `new TimeSliceBudget(sliceMs = DEFAULT_TIME_SLICE_MS, now = p
|
|
|
41
45
|
| `timeSlicedMap` | Returns transformed values in input order. |
|
|
42
46
|
| `timeSlicedFlatMap` | Flattens each callback's list incrementally, including large lists. |
|
|
43
47
|
| `timeSlicedFilter` | Returns matching items in input order. |
|
|
48
|
+
| `timeSlicedSome` | Returns `true` at the first match; stops there. |
|
|
49
|
+
| `timeSlicedFind` | Returns the first match, or `undefined`; stops there. |
|
|
44
50
|
| `timeSlicedGroupBy` | Returns a `Map` with first-seen key order and input order within each group. |
|
|
45
51
|
| `timeSlicedSort` | Returns a new stable sorted array with a synchronous comparator. |
|
|
46
52
|
| `timeSlicedMapFromEntries` | Builds a `Map`; the last value wins for repeated keys. |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { waitFor, timeSlicedFilter, timeSlicedFlatMap, timeSlicedForEach, timeSlicedGroupBy, timeSlicedMap, timeSlicedMapFromEntries, TimeSliceBudget, } from "./time-slicing.js";
|
|
2
2
|
export { timeSlicedSort } from "./time-sliced-sort.js";
|
|
3
|
+
export { timeSlicedFind, timeSlicedSome } from "./time-sliced-search.js";
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,wBAAwB,EACxB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,wBAAwB,EACxB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { waitFor, timeSlicedFilter, timeSlicedFlatMap, timeSlicedForEach, timeSlicedGroupBy, timeSlicedMap, timeSlicedMapFromEntries, TimeSliceBudget, } from "./time-slicing.js";
|
|
2
2
|
export { timeSlicedSort } from "./time-sliced-sort.js";
|
|
3
|
+
export { timeSlicedFind, timeSlicedSome } from "./time-sliced-search.js";
|
|
3
4
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,wBAAwB,EACxB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,wBAAwB,EACxB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const GENERATOR_HINT = "For async work, use a generator callback with `yield* waitFor(...)`.";
|
|
2
|
+
export declare const TRAVERSAL_HINT = "For async work, use a generator callback with `yield* waitFor(...)` in timeSlicedMap or timeSlicedForEach first.";
|
|
3
|
+
/**
|
|
4
|
+
* Returns `value` unchanged, or throws a `TypeError` if it is a thenable.
|
|
5
|
+
*
|
|
6
|
+
* TypeScript cannot reject a promise result without also rejecting generic
|
|
7
|
+
* results, so the synchronous callback rule is checked here at runtime.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertNotThenable<Value>(value: Value, helper: string, hint: string): Value;
|
|
10
|
+
//# sourceMappingURL=synchronous-result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synchronous-result.d.ts","sourceRoot":"","sources":["../src/synchronous-result.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,yEAC6C,CAAC;AACzE,eAAO,MAAM,cAAc,qHACyF,CAAC;AAErH;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EACrC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,KAAK,CAYP"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const GENERATOR_HINT = "For async work, use a generator callback with `yield* waitFor(...)`.";
|
|
2
|
+
export const TRAVERSAL_HINT = "For async work, use a generator callback with `yield* waitFor(...)` in timeSlicedMap or timeSlicedForEach first.";
|
|
3
|
+
/**
|
|
4
|
+
* Returns `value` unchanged, or throws a `TypeError` if it is a thenable.
|
|
5
|
+
*
|
|
6
|
+
* TypeScript cannot reject a promise result without also rejecting generic
|
|
7
|
+
* results, so the synchronous callback rule is checked here at runtime.
|
|
8
|
+
*/
|
|
9
|
+
export function assertNotThenable(value, helper, hint) {
|
|
10
|
+
if (((typeof value === "object" && value !== null) ||
|
|
11
|
+
typeof value === "function") &&
|
|
12
|
+
"then" in value &&
|
|
13
|
+
typeof value.then === "function") {
|
|
14
|
+
throw new TypeError(`${helper} received a promise or other thenable from a synchronous callback. ${hint}`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=synchronous-result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synchronous-result.js","sourceRoot":"","sources":["../src/synchronous-result.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GACzB,sEAAsE,CAAC;AACzE,MAAM,CAAC,MAAM,cAAc,GACzB,kHAAkH,CAAC;AAErH;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAY,EACZ,MAAc,EACd,IAAY;IAEZ,IACE,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;QAC5C,OAAO,KAAK,KAAK,UAAU,CAAC;QAC9B,MAAM,IAAI,KAAK;QACf,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,EAChC,CAAC;QACD,MAAM,IAAI,SAAS,CACjB,GAAG,MAAM,sEAAsE,IAAI,EAAE,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { TimeSliceBudget } from "./time-slicing.js";
|
|
2
|
+
/**
|
|
3
|
+
* Returns whether any item matches a synchronous predicate, in input order.
|
|
4
|
+
*
|
|
5
|
+
* Stops at the first match. It does not advance the iterable past that item,
|
|
6
|
+
* and it builds no intermediate array. Measures each predicate call, including
|
|
7
|
+
* calls that throw. One predicate call cannot be interrupted. Throws a
|
|
8
|
+
* `TypeError` if the predicate returns a promise or other thenable.
|
|
9
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
10
|
+
*
|
|
11
|
+
* @example Check for a negative number.
|
|
12
|
+
* const hasNegative = await timeSlicedSome([3, -1, 2], (value) => value < 0);
|
|
13
|
+
*
|
|
14
|
+
* @example Share a budget with other work, and stop early on a lazy source.
|
|
15
|
+
* const budget = new TimeSliceBudget();
|
|
16
|
+
* const found = await timeSlicedSome(map.values(), (entry) => entry.active, budget);
|
|
17
|
+
*/
|
|
18
|
+
export declare function timeSlicedSome<Item>(items: Iterable<Item>, predicate: (item: Item, index: number) => boolean, budget?: TimeSliceBudget): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the first item that matches a type-guard predicate, narrowed.
|
|
21
|
+
*
|
|
22
|
+
* @example Find the first string.
|
|
23
|
+
* const text = await timeSlicedFind(
|
|
24
|
+
* [1, "a", 2],
|
|
25
|
+
* (value): value is string => typeof value === "string",
|
|
26
|
+
* );
|
|
27
|
+
*/
|
|
28
|
+
export declare function timeSlicedFind<Item, Found extends Item>(items: Iterable<Item>, predicate: (item: Item, index: number) => item is Found, budget?: TimeSliceBudget): Promise<Found | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the first item that matches a synchronous predicate, or `undefined`.
|
|
31
|
+
*
|
|
32
|
+
* Stops at the first match. It does not advance the iterable past that item,
|
|
33
|
+
* and it builds no intermediate array. Measures each predicate call, including
|
|
34
|
+
* calls that throw. One predicate call cannot be interrupted. Throws a
|
|
35
|
+
* `TypeError` if the predicate returns a promise or other thenable. As with
|
|
36
|
+
* `Array.prototype.find`, a matching `undefined` item also returns `undefined`.
|
|
37
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
38
|
+
*
|
|
39
|
+
* @example Find the first even number.
|
|
40
|
+
* const even = await timeSlicedFind([1, 4, 6], (value) => value % 2 === 0);
|
|
41
|
+
*
|
|
42
|
+
* @example Use the index and a shared budget.
|
|
43
|
+
* const budget = new TimeSliceBudget();
|
|
44
|
+
* const late = await timeSlicedFind(["a", "b"], (_, index) => index > 0, budget);
|
|
45
|
+
*/
|
|
46
|
+
export declare function timeSlicedFind<Item>(items: Iterable<Item>, predicate: (item: Item, index: number) => boolean, budget?: TimeSliceBudget): Promise<Item | undefined>;
|
|
47
|
+
//# sourceMappingURL=time-sliced-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-sliced-search.d.ts","sourceRoot":"","sources":["../src/time-sliced-search.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AA0BpD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAAC,IAAI,EACvC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,EACjD,MAAM,kBAAwB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAKlB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,KAAK,SAAS,IAAI,EACrD,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,KAAK,EACvD,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;AAC9B;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,EACjD,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TRAVERSAL_HINT, assertNotThenable } from "./synchronous-result.js";
|
|
2
|
+
import { TimeSliceBudget } from "./time-slicing.js";
|
|
3
|
+
async function findFirstMatch(helper, items, predicate, budget) {
|
|
4
|
+
const position = { index: 0 };
|
|
5
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Stop at the first match; native iteration closes the iterator without advancing it.
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
const matched = budget.measure(() => assertNotThenable(predicate(item, position.index), helper, TRAVERSAL_HINT));
|
|
8
|
+
position.index += 1;
|
|
9
|
+
const pending = budget.yieldIfNeeded();
|
|
10
|
+
if (pending !== undefined)
|
|
11
|
+
await pending;
|
|
12
|
+
if (matched)
|
|
13
|
+
return { item };
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns whether any item matches a synchronous predicate, in input order.
|
|
19
|
+
*
|
|
20
|
+
* Stops at the first match. It does not advance the iterable past that item,
|
|
21
|
+
* and it builds no intermediate array. Measures each predicate call, including
|
|
22
|
+
* calls that throw. One predicate call cannot be interrupted. Throws a
|
|
23
|
+
* `TypeError` if the predicate returns a promise or other thenable.
|
|
24
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
25
|
+
*
|
|
26
|
+
* @example Check for a negative number.
|
|
27
|
+
* const hasNegative = await timeSlicedSome([3, -1, 2], (value) => value < 0);
|
|
28
|
+
*
|
|
29
|
+
* @example Share a budget with other work, and stop early on a lazy source.
|
|
30
|
+
* const budget = new TimeSliceBudget();
|
|
31
|
+
* const found = await timeSlicedSome(map.values(), (entry) => entry.active, budget);
|
|
32
|
+
*/
|
|
33
|
+
export async function timeSlicedSome(items, predicate, budget = new TimeSliceBudget()) {
|
|
34
|
+
return ((await findFirstMatch("timeSlicedSome", items, predicate, budget)) !==
|
|
35
|
+
undefined);
|
|
36
|
+
}
|
|
37
|
+
export async function timeSlicedFind(items, predicate, budget = new TimeSliceBudget()) {
|
|
38
|
+
return (await findFirstMatch("timeSlicedFind", items, predicate, budget))
|
|
39
|
+
?.item;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=time-sliced-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-sliced-search.js","sourceRoot":"","sources":["../src/time-sliced-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,KAAK,UAAU,cAAc,CAC3B,MAAc,EACd,KAAqB,EACrB,SAAiD,EACjD,MAAuB;IAEvB,MAAM,QAAQ,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAC9B,+IAA+I;IAC/I,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAClC,iBAAiB,CACf,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,EAC/B,MAAM,EACN,cAAc,CACf,CACF,CAAC;QACF,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;QACvC,IAAI,OAAO,KAAK,SAAS;YAAE,MAAM,OAAO,CAAC;QACzC,IAAI,OAAO;YAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC/B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAqB,EACrB,SAAiD,EACjD,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,OAAO,CACL,CAAC,MAAM,cAAc,CAAC,gBAAgB,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QAClE,SAAS,CACV,CAAC;AACJ,CAAC;AAsCD,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAqB,EACrB,SAAiD,EACjD,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,OAAO,CAAC,MAAM,cAAc,CAAC,gBAAgB,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACvE,EAAE,IAAI,CAAC;AACX,CAAC"}
|
package/dist/time-slicing.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
type Synchronous<Result> = Result extends PromiseLike<unknown> ? never : Result;
|
|
2
1
|
/**
|
|
3
2
|
* Tracks measured synchronous work before yielding to the event loop.
|
|
4
3
|
*
|
|
@@ -29,6 +28,8 @@ export declare class TimeSliceBudget {
|
|
|
29
28
|
*
|
|
30
29
|
* Nested measurements on this budget count once. This method does not yield.
|
|
31
30
|
* Call `yieldIfNeeded` at a safe boundary after handling the result.
|
|
31
|
+
* `work` must be synchronous. If it returns a promise or other thenable, this
|
|
32
|
+
* method throws a `TypeError` after charging the elapsed time.
|
|
32
33
|
*
|
|
33
34
|
* @example Measure one segment and then check the budget.
|
|
34
35
|
* const budget = new TimeSliceBudget();
|
|
@@ -36,7 +37,7 @@ export declare class TimeSliceBudget {
|
|
|
36
37
|
* const pending = budget.yieldIfNeeded();
|
|
37
38
|
* if (pending !== undefined) await pending;
|
|
38
39
|
*/
|
|
39
|
-
measure<Result>(work: () => Result
|
|
40
|
+
measure<Result>(work: () => Result): Result;
|
|
40
41
|
/**
|
|
41
42
|
* Returns a yield promise if the allowance is exhausted, or `undefined` otherwise.
|
|
42
43
|
*
|
|
@@ -81,6 +82,9 @@ export declare function waitFor<Value>(promise: PromiseLike<Value>): CallbackGen
|
|
|
81
82
|
* finishes before the next item starts. Measures synchronous callback work and
|
|
82
83
|
* every generator `next()`/`throw()` segment, excluding promise waits.
|
|
83
84
|
* Iterator advancement is unmeasured. Unhandled failures stop the traversal.
|
|
85
|
+
* A non-generator callback must be synchronous. If it returns a promise or
|
|
86
|
+
* other thenable (an `async` function, for example), the traversal throws a
|
|
87
|
+
* `TypeError` on that first call. The type checker does not catch this.
|
|
84
88
|
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
85
89
|
*
|
|
86
90
|
* @example Run a synchronous visitor.
|
|
@@ -96,17 +100,21 @@ export declare function timeSlicedForEach<Item, Result>(items: Iterable<Item>, c
|
|
|
96
100
|
/**
|
|
97
101
|
* Runs a synchronous visitor in input order with an optional processing budget.
|
|
98
102
|
*
|
|
103
|
+
* Throws a `TypeError` if the visitor returns a promise or other thenable.
|
|
104
|
+
*
|
|
99
105
|
* @example
|
|
100
106
|
* await timeSlicedForEach([1, 2], (value) => console.log(value));
|
|
101
107
|
*/
|
|
102
|
-
export declare function timeSlicedForEach<Item, Result>(items: Iterable<Item>, callback: (item: Item, index: number) => Result
|
|
108
|
+
export declare function timeSlicedForEach<Item, Result>(items: Iterable<Item>, callback: (item: Item, index: number) => Result, budget?: TimeSliceBudget): Promise<void>;
|
|
103
109
|
/**
|
|
104
110
|
* Returns transformed values in input order using sequential callbacks.
|
|
105
111
|
*
|
|
106
112
|
* Accepts synchronous callbacks or generators that yield promises. Each generator
|
|
107
113
|
* finishes before the next item starts. Measures every synchronous segment,
|
|
108
114
|
* including `throw()` recovery and result accumulation. Promise waits and iterator
|
|
109
|
-
* advancement are unmeasured. Ordinary async callbacks are not supported
|
|
115
|
+
* advancement are unmeasured. Ordinary async callbacks are not supported: if a
|
|
116
|
+
* non-generator callback returns a promise or other thenable, the traversal
|
|
117
|
+
* throws a `TypeError` on that first call. The type checker does not catch this.
|
|
110
118
|
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
111
119
|
*
|
|
112
120
|
* @example Transform synchronous values.
|
|
@@ -122,10 +130,12 @@ export declare function timeSlicedMap<Item, Result>(items: Iterable<Item>, callb
|
|
|
122
130
|
/**
|
|
123
131
|
* Maps synchronous values in input order with an optional processing budget.
|
|
124
132
|
*
|
|
133
|
+
* Throws a `TypeError` if the callback returns a promise or other thenable.
|
|
134
|
+
*
|
|
125
135
|
* @example
|
|
126
136
|
* const doubled = await timeSlicedMap([1, 2], (value) => value * 2);
|
|
127
137
|
*/
|
|
128
|
-
export declare function timeSlicedMap<Item, Result>(items: Iterable<Item>, callback: (item: Item, index: number) => Result
|
|
138
|
+
export declare function timeSlicedMap<Item, Result>(items: Iterable<Item>, callback: (item: Item, index: number) => Result, budget?: TimeSliceBudget): Promise<Result[]>;
|
|
129
139
|
/**
|
|
130
140
|
* Maps each item to a synchronous list and flattens the lists in input order.
|
|
131
141
|
*
|
|
@@ -149,7 +159,8 @@ export declare function timeSlicedFlatMap<Item, Result>(items: Iterable<Item>, c
|
|
|
149
159
|
* Returns items whose synchronous predicate is true, preserving input order.
|
|
150
160
|
*
|
|
151
161
|
* Measures predicate work and result accumulation. One predicate call cannot be
|
|
152
|
-
* interrupted.
|
|
162
|
+
* interrupted. Throws a `TypeError` if the predicate returns a promise or other
|
|
163
|
+
* thenable. The optional budget defaults to a new independent instance.
|
|
153
164
|
*
|
|
154
165
|
* @example Keep odd numbers.
|
|
155
166
|
* const odd = await timeSlicedFilter([1, 2, 3], (value) => value % 2 === 1);
|
|
@@ -165,6 +176,8 @@ export declare function timeSlicedFilter<Item>(items: Iterable<Item>, predicate:
|
|
|
165
176
|
* Groups items by a synchronous key, preserving key and group insertion order.
|
|
166
177
|
*
|
|
167
178
|
* Uses `Map` key equality. Measures key selection and group accumulation.
|
|
179
|
+
* `keyOf` must be synchronous. If it returns a promise or other thenable, the
|
|
180
|
+
* operation throws a `TypeError` before it stores the key.
|
|
168
181
|
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
169
182
|
*
|
|
170
183
|
* @example Group strings by length.
|
|
@@ -176,7 +189,7 @@ export declare function timeSlicedFilter<Item>(items: Iterable<Item>, predicate:
|
|
|
176
189
|
* return yield* waitFor(timeSlicedGroupBy(values, (value) => value % 2, budget));
|
|
177
190
|
* }, budget);
|
|
178
191
|
*/
|
|
179
|
-
export declare function timeSlicedGroupBy<Item, Key>(items: Iterable<Item>, keyOf: (item: Item, index: number) => Key
|
|
192
|
+
export declare function timeSlicedGroupBy<Item, Key>(items: Iterable<Item>, keyOf: (item: Item, index: number) => Key, budget?: TimeSliceBudget): Promise<Map<Key, Item[]>>;
|
|
180
193
|
/**
|
|
181
194
|
* Builds a Map from entries in input order; the last value for a repeated key wins.
|
|
182
195
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-slicing.d.ts","sourceRoot":"","sources":["../src/time-slicing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-slicing.d.ts","sourceRoot":"","sources":["../src/time-slicing.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;GAWG;AACH,qBAAa,eAAe;;IAO1B;;;;;;;;;OASG;IACH,YACE,OAAO,SAAwB,EAC/B,GAAG,4BAAiB,EACpB,gBAAgB,GAAE,MAAM,OAAO,CAAC,IAAI,CAAgB,EAUrD;IAED;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,MAAM,GAAG,MAAM,CAc1C;IAED;;;;;;;;;;OAUG;IACH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAIzC;CACF;AAED,KAAK,iBAAiB,CAAC,MAAM,IAAI,SAAS,CACxC,WAAW,CAAC,OAAO,CAAC,EACpB,MAAM,EACN,OAAO,CACR,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAiB,OAAO,CAAC,KAAK,EAC5B,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,GAC1B,iBAAiB,CAAC,KAAK,CAAC,CAO1B;AA0FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAC5C,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,iBAAiB,CAAC,MAAM,CAAC,EAClE,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAAC;AACjB;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAC5C,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EAC/C,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAAC;AASjB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EACxC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,iBAAiB,CAAC,MAAM,CAAC,EAClE,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AACrB;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EACxC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EAC/C,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAoBrB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAClD,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,MAAM,EAAE,EAC1D,MAAM,kBAAwB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAqBnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EACzC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,EACjD,MAAM,kBAAwB,GAC7B,OAAO,CAAC,IAAI,EAAE,CAAC,CAmBjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,GAAG,EAC/C,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,GAAG,EACzC,MAAM,kBAAwB,GAC7B,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAkB3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,KAAK,EACvD,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,EACxC,MAAM,kBAAwB,GAC7B,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAU1B"}
|
package/dist/time-slicing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { setImmediate } from "node:timers/promises";
|
|
3
|
+
import { GENERATOR_HINT, TRAVERSAL_HINT, assertNotThenable, } from "./synchronous-result.js";
|
|
3
4
|
const DEFAULT_TIME_SLICE_MS = 4;
|
|
4
5
|
const performanceNow = performance.now.bind(performance);
|
|
5
6
|
/**
|
|
@@ -42,6 +43,8 @@ export class TimeSliceBudget {
|
|
|
42
43
|
*
|
|
43
44
|
* Nested measurements on this budget count once. This method does not yield.
|
|
44
45
|
* Call `yieldIfNeeded` at a safe boundary after handling the result.
|
|
46
|
+
* `work` must be synchronous. If it returns a promise or other thenable, this
|
|
47
|
+
* method throws a `TypeError` after charging the elapsed time.
|
|
45
48
|
*
|
|
46
49
|
* @example Measure one segment and then check the budget.
|
|
47
50
|
* const budget = new TimeSliceBudget();
|
|
@@ -54,7 +57,7 @@ export class TimeSliceBudget {
|
|
|
54
57
|
const startedAt = outermost ? this.#now() : 0;
|
|
55
58
|
this.#depth += 1;
|
|
56
59
|
try {
|
|
57
|
-
return work();
|
|
60
|
+
return assertNotThenable(work(), "TimeSliceBudget.measure", GENERATOR_HINT);
|
|
58
61
|
}
|
|
59
62
|
finally {
|
|
60
63
|
this.#depth -= 1;
|
|
@@ -159,11 +162,11 @@ async function runCallbackGenerator(generator, budget) {
|
|
|
159
162
|
}
|
|
160
163
|
return state.step.value;
|
|
161
164
|
}
|
|
162
|
-
async function traverse(items, callback, accept, budget) {
|
|
165
|
+
async function traverse(helper, items, callback, accept, budget) {
|
|
163
166
|
const position = { index: 0 };
|
|
164
167
|
// oxlint-disable-next-line zingage/no-restricted-syntax -- Native iteration owns cleanup while callbacks and budget yields settle sequentially.
|
|
165
168
|
for (const item of items) {
|
|
166
|
-
const value = budget.measure(() => callback(item, position.index));
|
|
169
|
+
const value = budget.measure(() => assertNotThenable(callback(item, position.index), helper, GENERATOR_HINT));
|
|
167
170
|
const result = isCallbackGenerator(value)
|
|
168
171
|
? await runCallbackGenerator(value, budget)
|
|
169
172
|
: value;
|
|
@@ -175,11 +178,11 @@ async function traverse(items, callback, accept, budget) {
|
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
export async function timeSlicedForEach(items, callback, budget = new TimeSliceBudget()) {
|
|
178
|
-
await traverse(items, callback, () => { }, budget);
|
|
181
|
+
await traverse("timeSlicedForEach", items, callback, () => { }, budget);
|
|
179
182
|
}
|
|
180
183
|
export async function timeSlicedMap(items, callback, budget = new TimeSliceBudget()) {
|
|
181
184
|
const results = [];
|
|
182
|
-
await traverse(items, callback, (result) => {
|
|
185
|
+
await traverse("timeSlicedMap", items, callback, (result) => {
|
|
183
186
|
// oxlint-disable-next-line zingage/no-restricted-syntax -- Append to the local result to avoid quadratic copying.
|
|
184
187
|
results.push(result);
|
|
185
188
|
}, budget);
|
|
@@ -227,7 +230,8 @@ export async function timeSlicedFlatMap(items, callback, budget = new TimeSliceB
|
|
|
227
230
|
* Returns items whose synchronous predicate is true, preserving input order.
|
|
228
231
|
*
|
|
229
232
|
* Measures predicate work and result accumulation. One predicate call cannot be
|
|
230
|
-
* interrupted.
|
|
233
|
+
* interrupted. Throws a `TypeError` if the predicate returns a promise or other
|
|
234
|
+
* thenable. The optional budget defaults to a new independent instance.
|
|
231
235
|
*
|
|
232
236
|
* @example Keep odd numbers.
|
|
233
237
|
* const odd = await timeSlicedFilter([1, 2, 3], (value) => value % 2 === 1);
|
|
@@ -241,7 +245,7 @@ export async function timeSlicedFlatMap(items, callback, budget = new TimeSliceB
|
|
|
241
245
|
export async function timeSlicedFilter(items, predicate, budget = new TimeSliceBudget()) {
|
|
242
246
|
const results = [];
|
|
243
247
|
await timeSlicedForEach(items, (item, index) => {
|
|
244
|
-
if (predicate(item, index)) {
|
|
248
|
+
if (assertNotThenable(predicate(item, index), "timeSlicedFilter", TRAVERSAL_HINT)) {
|
|
245
249
|
// oxlint-disable-next-line zingage/no-restricted-syntax -- Append to the local result without quadratic copying.
|
|
246
250
|
results.push(item);
|
|
247
251
|
}
|
|
@@ -252,6 +256,8 @@ export async function timeSlicedFilter(items, predicate, budget = new TimeSliceB
|
|
|
252
256
|
* Groups items by a synchronous key, preserving key and group insertion order.
|
|
253
257
|
*
|
|
254
258
|
* Uses `Map` key equality. Measures key selection and group accumulation.
|
|
259
|
+
* `keyOf` must be synchronous. If it returns a promise or other thenable, the
|
|
260
|
+
* operation throws a `TypeError` before it stores the key.
|
|
255
261
|
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
256
262
|
*
|
|
257
263
|
* @example Group strings by length.
|
|
@@ -266,7 +272,7 @@ export async function timeSlicedFilter(items, predicate, budget = new TimeSliceB
|
|
|
266
272
|
export async function timeSlicedGroupBy(items, keyOf, budget = new TimeSliceBudget()) {
|
|
267
273
|
const groups = new Map();
|
|
268
274
|
await timeSlicedForEach(items, (item, index) => {
|
|
269
|
-
const key = keyOf(item, index);
|
|
275
|
+
const key = assertNotThenable(keyOf(item, index), "timeSlicedGroupBy", TRAVERSAL_HINT);
|
|
270
276
|
const group = groups.get(key);
|
|
271
277
|
if (group === undefined)
|
|
272
278
|
groups.set(key, [item]);
|
package/dist/time-slicing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-slicing.js","sourceRoot":"","sources":["../src/time-slicing.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"time-slicing.js","sourceRoot":"","sources":["../src/time-slicing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const GENERATOR_HINT =
|
|
2
|
+
"For async work, use a generator callback with `yield* waitFor(...)`.";
|
|
3
|
+
export const TRAVERSAL_HINT =
|
|
4
|
+
"For async work, use a generator callback with `yield* waitFor(...)` in timeSlicedMap or timeSlicedForEach first.";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns `value` unchanged, or throws a `TypeError` if it is a thenable.
|
|
8
|
+
*
|
|
9
|
+
* TypeScript cannot reject a promise result without also rejecting generic
|
|
10
|
+
* results, so the synchronous callback rule is checked here at runtime.
|
|
11
|
+
*/
|
|
12
|
+
export function assertNotThenable<Value>(
|
|
13
|
+
value: Value,
|
|
14
|
+
helper: string,
|
|
15
|
+
hint: string,
|
|
16
|
+
): Value {
|
|
17
|
+
if (
|
|
18
|
+
((typeof value === "object" && value !== null) ||
|
|
19
|
+
typeof value === "function") &&
|
|
20
|
+
"then" in value &&
|
|
21
|
+
typeof value.then === "function"
|
|
22
|
+
) {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`${helper} received a promise or other thenable from a synchronous callback. ${hint}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { TRAVERSAL_HINT, assertNotThenable } from "./synchronous-result.js";
|
|
2
|
+
import { TimeSliceBudget } from "./time-slicing.js";
|
|
3
|
+
|
|
4
|
+
async function findFirstMatch<Item>(
|
|
5
|
+
helper: string,
|
|
6
|
+
items: Iterable<Item>,
|
|
7
|
+
predicate: (item: Item, index: number) => boolean,
|
|
8
|
+
budget: TimeSliceBudget,
|
|
9
|
+
): Promise<{ item: Item } | undefined> {
|
|
10
|
+
const position = { index: 0 };
|
|
11
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Stop at the first match; native iteration closes the iterator without advancing it.
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
const matched = budget.measure(() =>
|
|
14
|
+
assertNotThenable(
|
|
15
|
+
predicate(item, position.index),
|
|
16
|
+
helper,
|
|
17
|
+
TRAVERSAL_HINT,
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
position.index += 1;
|
|
21
|
+
const pending = budget.yieldIfNeeded();
|
|
22
|
+
if (pending !== undefined) await pending;
|
|
23
|
+
if (matched) return { item };
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns whether any item matches a synchronous predicate, in input order.
|
|
30
|
+
*
|
|
31
|
+
* Stops at the first match. It does not advance the iterable past that item,
|
|
32
|
+
* and it builds no intermediate array. Measures each predicate call, including
|
|
33
|
+
* calls that throw. One predicate call cannot be interrupted. Throws a
|
|
34
|
+
* `TypeError` if the predicate returns a promise or other thenable.
|
|
35
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
36
|
+
*
|
|
37
|
+
* @example Check for a negative number.
|
|
38
|
+
* const hasNegative = await timeSlicedSome([3, -1, 2], (value) => value < 0);
|
|
39
|
+
*
|
|
40
|
+
* @example Share a budget with other work, and stop early on a lazy source.
|
|
41
|
+
* const budget = new TimeSliceBudget();
|
|
42
|
+
* const found = await timeSlicedSome(map.values(), (entry) => entry.active, budget);
|
|
43
|
+
*/
|
|
44
|
+
export async function timeSlicedSome<Item>(
|
|
45
|
+
items: Iterable<Item>,
|
|
46
|
+
predicate: (item: Item, index: number) => boolean,
|
|
47
|
+
budget = new TimeSliceBudget(),
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
return (
|
|
50
|
+
(await findFirstMatch("timeSlicedSome", items, predicate, budget)) !==
|
|
51
|
+
undefined
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the first item that matches a type-guard predicate, narrowed.
|
|
57
|
+
*
|
|
58
|
+
* @example Find the first string.
|
|
59
|
+
* const text = await timeSlicedFind(
|
|
60
|
+
* [1, "a", 2],
|
|
61
|
+
* (value): value is string => typeof value === "string",
|
|
62
|
+
* );
|
|
63
|
+
*/
|
|
64
|
+
export function timeSlicedFind<Item, Found extends Item>(
|
|
65
|
+
items: Iterable<Item>,
|
|
66
|
+
predicate: (item: Item, index: number) => item is Found,
|
|
67
|
+
budget?: TimeSliceBudget,
|
|
68
|
+
): Promise<Found | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Returns the first item that matches a synchronous predicate, or `undefined`.
|
|
71
|
+
*
|
|
72
|
+
* Stops at the first match. It does not advance the iterable past that item,
|
|
73
|
+
* and it builds no intermediate array. Measures each predicate call, including
|
|
74
|
+
* calls that throw. One predicate call cannot be interrupted. Throws a
|
|
75
|
+
* `TypeError` if the predicate returns a promise or other thenable. As with
|
|
76
|
+
* `Array.prototype.find`, a matching `undefined` item also returns `undefined`.
|
|
77
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
78
|
+
*
|
|
79
|
+
* @example Find the first even number.
|
|
80
|
+
* const even = await timeSlicedFind([1, 4, 6], (value) => value % 2 === 0);
|
|
81
|
+
*
|
|
82
|
+
* @example Use the index and a shared budget.
|
|
83
|
+
* const budget = new TimeSliceBudget();
|
|
84
|
+
* const late = await timeSlicedFind(["a", "b"], (_, index) => index > 0, budget);
|
|
85
|
+
*/
|
|
86
|
+
export function timeSlicedFind<Item>(
|
|
87
|
+
items: Iterable<Item>,
|
|
88
|
+
predicate: (item: Item, index: number) => boolean,
|
|
89
|
+
budget?: TimeSliceBudget,
|
|
90
|
+
): Promise<Item | undefined>;
|
|
91
|
+
export async function timeSlicedFind<Item>(
|
|
92
|
+
items: Iterable<Item>,
|
|
93
|
+
predicate: (item: Item, index: number) => boolean,
|
|
94
|
+
budget = new TimeSliceBudget(),
|
|
95
|
+
): Promise<Item | undefined> {
|
|
96
|
+
return (await findFirstMatch("timeSlicedFind", items, predicate, budget))
|
|
97
|
+
?.item;
|
|
98
|
+
}
|
package/src/time-slicing.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { setImmediate } from "node:timers/promises";
|
|
3
|
+
import {
|
|
4
|
+
GENERATOR_HINT,
|
|
5
|
+
TRAVERSAL_HINT,
|
|
6
|
+
assertNotThenable,
|
|
7
|
+
} from "./synchronous-result.js";
|
|
3
8
|
|
|
4
9
|
const DEFAULT_TIME_SLICE_MS = 4;
|
|
5
10
|
const performanceNow = performance.now.bind(performance);
|
|
6
11
|
|
|
7
|
-
type Synchronous<Result> = Result extends PromiseLike<unknown> ? never : Result;
|
|
8
|
-
|
|
9
12
|
/**
|
|
10
13
|
* Tracks measured synchronous work before yielding to the event loop.
|
|
11
14
|
*
|
|
@@ -55,6 +58,8 @@ export class TimeSliceBudget {
|
|
|
55
58
|
*
|
|
56
59
|
* Nested measurements on this budget count once. This method does not yield.
|
|
57
60
|
* Call `yieldIfNeeded` at a safe boundary after handling the result.
|
|
61
|
+
* `work` must be synchronous. If it returns a promise or other thenable, this
|
|
62
|
+
* method throws a `TypeError` after charging the elapsed time.
|
|
58
63
|
*
|
|
59
64
|
* @example Measure one segment and then check the budget.
|
|
60
65
|
* const budget = new TimeSliceBudget();
|
|
@@ -62,12 +67,16 @@ export class TimeSliceBudget {
|
|
|
62
67
|
* const pending = budget.yieldIfNeeded();
|
|
63
68
|
* if (pending !== undefined) await pending;
|
|
64
69
|
*/
|
|
65
|
-
measure<Result>(work: () => Result
|
|
70
|
+
measure<Result>(work: () => Result): Result {
|
|
66
71
|
const outermost = this.#depth === 0;
|
|
67
72
|
const startedAt = outermost ? this.#now() : 0;
|
|
68
73
|
this.#depth += 1;
|
|
69
74
|
try {
|
|
70
|
-
return
|
|
75
|
+
return assertNotThenable(
|
|
76
|
+
work(),
|
|
77
|
+
"TimeSliceBudget.measure",
|
|
78
|
+
GENERATOR_HINT,
|
|
79
|
+
);
|
|
71
80
|
} finally {
|
|
72
81
|
this.#depth -= 1;
|
|
73
82
|
if (outermost) this.#remainingMs -= this.#now() - startedAt;
|
|
@@ -197,19 +206,17 @@ async function runCallbackGenerator<Result>(
|
|
|
197
206
|
}
|
|
198
207
|
|
|
199
208
|
async function traverse<Item, Result>(
|
|
209
|
+
helper: string,
|
|
200
210
|
items: Iterable<Item>,
|
|
201
|
-
callback: (
|
|
202
|
-
item: Item,
|
|
203
|
-
index: number,
|
|
204
|
-
) => (Result & Synchronous<Result>) | CallbackGenerator<Result>,
|
|
211
|
+
callback: (item: Item, index: number) => Result | CallbackGenerator<Result>,
|
|
205
212
|
accept: (result: Result) => void,
|
|
206
213
|
budget: TimeSliceBudget,
|
|
207
214
|
): Promise<void> {
|
|
208
215
|
const position = { index: 0 };
|
|
209
216
|
// oxlint-disable-next-line zingage/no-restricted-syntax -- Native iteration owns cleanup while callbacks and budget yields settle sequentially.
|
|
210
217
|
for (const item of items) {
|
|
211
|
-
const value = budget.measure
|
|
212
|
-
callback(item, position.index),
|
|
218
|
+
const value = budget.measure(() =>
|
|
219
|
+
assertNotThenable(callback(item, position.index), helper, GENERATOR_HINT),
|
|
213
220
|
);
|
|
214
221
|
const result = isCallbackGenerator<Result>(value)
|
|
215
222
|
? await runCallbackGenerator(value, budget)
|
|
@@ -228,6 +235,9 @@ async function traverse<Item, Result>(
|
|
|
228
235
|
* finishes before the next item starts. Measures synchronous callback work and
|
|
229
236
|
* every generator `next()`/`throw()` segment, excluding promise waits.
|
|
230
237
|
* Iterator advancement is unmeasured. Unhandled failures stop the traversal.
|
|
238
|
+
* A non-generator callback must be synchronous. If it returns a promise or
|
|
239
|
+
* other thenable (an `async` function, for example), the traversal throws a
|
|
240
|
+
* `TypeError` on that first call. The type checker does not catch this.
|
|
231
241
|
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
232
242
|
*
|
|
233
243
|
* @example Run a synchronous visitor.
|
|
@@ -247,23 +257,22 @@ export function timeSlicedForEach<Item, Result>(
|
|
|
247
257
|
/**
|
|
248
258
|
* Runs a synchronous visitor in input order with an optional processing budget.
|
|
249
259
|
*
|
|
260
|
+
* Throws a `TypeError` if the visitor returns a promise or other thenable.
|
|
261
|
+
*
|
|
250
262
|
* @example
|
|
251
263
|
* await timeSlicedForEach([1, 2], (value) => console.log(value));
|
|
252
264
|
*/
|
|
253
265
|
export function timeSlicedForEach<Item, Result>(
|
|
254
266
|
items: Iterable<Item>,
|
|
255
|
-
callback: (item: Item, index: number) => Result
|
|
267
|
+
callback: (item: Item, index: number) => Result,
|
|
256
268
|
budget?: TimeSliceBudget,
|
|
257
269
|
): Promise<void>;
|
|
258
270
|
export async function timeSlicedForEach<Item, Result>(
|
|
259
271
|
items: Iterable<Item>,
|
|
260
|
-
callback: (
|
|
261
|
-
item: Item,
|
|
262
|
-
index: number,
|
|
263
|
-
) => (Result & Synchronous<Result>) | CallbackGenerator<Result>,
|
|
272
|
+
callback: (item: Item, index: number) => Result | CallbackGenerator<Result>,
|
|
264
273
|
budget = new TimeSliceBudget(),
|
|
265
274
|
): Promise<void> {
|
|
266
|
-
await traverse(items, callback, () => {}, budget);
|
|
275
|
+
await traverse("timeSlicedForEach", items, callback, () => {}, budget);
|
|
267
276
|
}
|
|
268
277
|
|
|
269
278
|
/**
|
|
@@ -272,7 +281,9 @@ export async function timeSlicedForEach<Item, Result>(
|
|
|
272
281
|
* Accepts synchronous callbacks or generators that yield promises. Each generator
|
|
273
282
|
* finishes before the next item starts. Measures every synchronous segment,
|
|
274
283
|
* including `throw()` recovery and result accumulation. Promise waits and iterator
|
|
275
|
-
* advancement are unmeasured. Ordinary async callbacks are not supported
|
|
284
|
+
* advancement are unmeasured. Ordinary async callbacks are not supported: if a
|
|
285
|
+
* non-generator callback returns a promise or other thenable, the traversal
|
|
286
|
+
* throws a `TypeError` on that first call. The type checker does not catch this.
|
|
276
287
|
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
277
288
|
*
|
|
278
289
|
* @example Transform synchronous values.
|
|
@@ -292,24 +303,24 @@ export function timeSlicedMap<Item, Result>(
|
|
|
292
303
|
/**
|
|
293
304
|
* Maps synchronous values in input order with an optional processing budget.
|
|
294
305
|
*
|
|
306
|
+
* Throws a `TypeError` if the callback returns a promise or other thenable.
|
|
307
|
+
*
|
|
295
308
|
* @example
|
|
296
309
|
* const doubled = await timeSlicedMap([1, 2], (value) => value * 2);
|
|
297
310
|
*/
|
|
298
311
|
export function timeSlicedMap<Item, Result>(
|
|
299
312
|
items: Iterable<Item>,
|
|
300
|
-
callback: (item: Item, index: number) => Result
|
|
313
|
+
callback: (item: Item, index: number) => Result,
|
|
301
314
|
budget?: TimeSliceBudget,
|
|
302
315
|
): Promise<Result[]>;
|
|
303
316
|
export async function timeSlicedMap<Item, Result>(
|
|
304
317
|
items: Iterable<Item>,
|
|
305
|
-
callback: (
|
|
306
|
-
item: Item,
|
|
307
|
-
index: number,
|
|
308
|
-
) => (Result & Synchronous<Result>) | CallbackGenerator<Result>,
|
|
318
|
+
callback: (item: Item, index: number) => Result | CallbackGenerator<Result>,
|
|
309
319
|
budget = new TimeSliceBudget(),
|
|
310
320
|
): Promise<Result[]> {
|
|
311
321
|
const results: Result[] = [];
|
|
312
322
|
await traverse(
|
|
323
|
+
"timeSlicedMap",
|
|
313
324
|
items,
|
|
314
325
|
callback,
|
|
315
326
|
(result) => {
|
|
@@ -370,7 +381,8 @@ export async function timeSlicedFlatMap<Item, Result>(
|
|
|
370
381
|
* Returns items whose synchronous predicate is true, preserving input order.
|
|
371
382
|
*
|
|
372
383
|
* Measures predicate work and result accumulation. One predicate call cannot be
|
|
373
|
-
* interrupted.
|
|
384
|
+
* interrupted. Throws a `TypeError` if the predicate returns a promise or other
|
|
385
|
+
* thenable. The optional budget defaults to a new independent instance.
|
|
374
386
|
*
|
|
375
387
|
* @example Keep odd numbers.
|
|
376
388
|
* const odd = await timeSlicedFilter([1, 2, 3], (value) => value % 2 === 1);
|
|
@@ -390,7 +402,13 @@ export async function timeSlicedFilter<Item>(
|
|
|
390
402
|
await timeSlicedForEach(
|
|
391
403
|
items,
|
|
392
404
|
(item, index) => {
|
|
393
|
-
if (
|
|
405
|
+
if (
|
|
406
|
+
assertNotThenable(
|
|
407
|
+
predicate(item, index),
|
|
408
|
+
"timeSlicedFilter",
|
|
409
|
+
TRAVERSAL_HINT,
|
|
410
|
+
)
|
|
411
|
+
) {
|
|
394
412
|
// oxlint-disable-next-line zingage/no-restricted-syntax -- Append to the local result without quadratic copying.
|
|
395
413
|
results.push(item);
|
|
396
414
|
}
|
|
@@ -404,6 +422,8 @@ export async function timeSlicedFilter<Item>(
|
|
|
404
422
|
* Groups items by a synchronous key, preserving key and group insertion order.
|
|
405
423
|
*
|
|
406
424
|
* Uses `Map` key equality. Measures key selection and group accumulation.
|
|
425
|
+
* `keyOf` must be synchronous. If it returns a promise or other thenable, the
|
|
426
|
+
* operation throws a `TypeError` before it stores the key.
|
|
407
427
|
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
408
428
|
*
|
|
409
429
|
* @example Group strings by length.
|
|
@@ -417,14 +437,18 @@ export async function timeSlicedFilter<Item>(
|
|
|
417
437
|
*/
|
|
418
438
|
export async function timeSlicedGroupBy<Item, Key>(
|
|
419
439
|
items: Iterable<Item>,
|
|
420
|
-
keyOf: (item: Item, index: number) => Key
|
|
440
|
+
keyOf: (item: Item, index: number) => Key,
|
|
421
441
|
budget = new TimeSliceBudget(),
|
|
422
442
|
): Promise<Map<Key, Item[]>> {
|
|
423
443
|
const groups = new Map<Key, Item[]>();
|
|
424
444
|
await timeSlicedForEach(
|
|
425
445
|
items,
|
|
426
446
|
(item, index) => {
|
|
427
|
-
const key =
|
|
447
|
+
const key = assertNotThenable(
|
|
448
|
+
keyOf(item, index),
|
|
449
|
+
"timeSlicedGroupBy",
|
|
450
|
+
TRAVERSAL_HINT,
|
|
451
|
+
);
|
|
428
452
|
const group = groups.get(key);
|
|
429
453
|
if (group === undefined) groups.set(key, [item]);
|
|
430
454
|
// oxlint-disable-next-line zingage/no-restricted-syntax -- Each local group accumulates without quadratic copying.
|