@zingage/scheduling 0.0.0-stage → 1.0.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 +91 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/time-sliced-sort.d.ts +26 -0
- package/dist/time-sliced-sort.d.ts.map +1 -0
- package/dist/time-sliced-sort.js +79 -0
- package/dist/time-sliced-sort.js.map +1 -0
- package/dist/time-slicing.d.ts +197 -0
- package/dist/time-slicing.d.ts.map +1 -0
- package/dist/time-slicing.js +301 -0
- package/dist/time-slicing.js.map +1 -0
- package/package.json +45 -3
- package/src/index.ts +11 -0
- package/src/time-sliced-sort.ts +95 -0
- package/src/time-slicing.ts +466 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { setImmediate } from "node:timers/promises";
|
|
3
|
+
const DEFAULT_TIME_SLICE_MS = 4;
|
|
4
|
+
const performanceNow = performance.now.bind(performance);
|
|
5
|
+
/**
|
|
6
|
+
* Tracks measured synchronous work before yielding to the event loop.
|
|
7
|
+
*
|
|
8
|
+
* A new budget allows 4 ms by default. Related operations can share an instance.
|
|
9
|
+
* Promise waits and unmeasured work do not consume it. One synchronous segment
|
|
10
|
+
* cannot be interrupted. Nested measurements on the same budget count once.
|
|
11
|
+
*
|
|
12
|
+
* @example Share the remaining allowance across related operations.
|
|
13
|
+
* const budget = new TimeSliceBudget();
|
|
14
|
+
* const doubled = await timeSlicedMap([1, 2], (value) => value * 2, budget);
|
|
15
|
+
* await timeSlicedForEach(doubled, (value) => console.log(value), budget);
|
|
16
|
+
*/
|
|
17
|
+
export class TimeSliceBudget {
|
|
18
|
+
#sliceMs;
|
|
19
|
+
#now;
|
|
20
|
+
#yieldToEventLoop;
|
|
21
|
+
#remainingMs;
|
|
22
|
+
#depth = 0;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a budget with a positive, finite allowance in milliseconds.
|
|
25
|
+
*
|
|
26
|
+
* `now` must be monotonic and return milliseconds. The default uses
|
|
27
|
+
* `performance.now`. The default scheduler uses promise-based `setImmediate`.
|
|
28
|
+
* A custom scheduler must let the event loop advance; a resolved promise does not.
|
|
29
|
+
*
|
|
30
|
+
* @example Use a smaller allowance for one operation.
|
|
31
|
+
* const budget = new TimeSliceBudget(2);
|
|
32
|
+
*/
|
|
33
|
+
constructor(sliceMs = DEFAULT_TIME_SLICE_MS, now = performanceNow, yieldToEventLoop = setImmediate) {
|
|
34
|
+
assert.ok(Number.isFinite(sliceMs) && sliceMs > 0, "sliceMs must be finite and greater than zero");
|
|
35
|
+
this.#sliceMs = sliceMs;
|
|
36
|
+
this.#remainingMs = sliceMs;
|
|
37
|
+
this.#now = now.bind(performance);
|
|
38
|
+
this.#yieldToEventLoop = yieldToEventLoop;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runs synchronous work and charges its elapsed time, including when it throws.
|
|
42
|
+
*
|
|
43
|
+
* Nested measurements on this budget count once. This method does not yield.
|
|
44
|
+
* Call `yieldIfNeeded` at a safe boundary after handling the result.
|
|
45
|
+
*
|
|
46
|
+
* @example Measure one segment and then check the budget.
|
|
47
|
+
* const budget = new TimeSliceBudget();
|
|
48
|
+
* const value = budget.measure(() => 2 * 3);
|
|
49
|
+
* const pending = budget.yieldIfNeeded();
|
|
50
|
+
* if (pending !== undefined) await pending;
|
|
51
|
+
*/
|
|
52
|
+
measure(work) {
|
|
53
|
+
const outermost = this.#depth === 0;
|
|
54
|
+
const startedAt = outermost ? this.#now() : 0;
|
|
55
|
+
this.#depth += 1;
|
|
56
|
+
try {
|
|
57
|
+
return work();
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
this.#depth -= 1;
|
|
61
|
+
if (outermost)
|
|
62
|
+
this.#remainingMs -= this.#now() - startedAt;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns a yield promise if the allowance is exhausted, or `undefined` otherwise.
|
|
67
|
+
*
|
|
68
|
+
* Replenishes before scheduling the yield. Connected work can use the next
|
|
69
|
+
* allowance while this caller waits. Resuming does not replenish it again.
|
|
70
|
+
*
|
|
71
|
+
* @example Avoid a microtask suspension when the budget still has time.
|
|
72
|
+
* const budget = new TimeSliceBudget();
|
|
73
|
+
* const pending = budget.yieldIfNeeded();
|
|
74
|
+
* if (pending !== undefined) await pending;
|
|
75
|
+
*/
|
|
76
|
+
yieldIfNeeded() {
|
|
77
|
+
if (this.#remainingMs > 0)
|
|
78
|
+
return undefined;
|
|
79
|
+
this.#remainingMs = this.#sliceMs;
|
|
80
|
+
return this.#yieldToEventLoop();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Suspends a generator callback until a promise settles, preserving its value type.
|
|
85
|
+
*
|
|
86
|
+
* Use `yield* waitFor(promise)` inside a generator passed to `timeSlicedMap` or
|
|
87
|
+
* `timeSlicedForEach`. Rejections enter the generator through `throw()`, so its
|
|
88
|
+
* `catch` and `finally` blocks run normally. Falsy and void results are supported.
|
|
89
|
+
*
|
|
90
|
+
* @example Infer the resolved value without annotating the generator.
|
|
91
|
+
* const lengths = await timeSlicedMap(["Ada"], function* (name) {
|
|
92
|
+
* const text = yield* waitFor(Promise.resolve(name));
|
|
93
|
+
* return text.length;
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* @example Recover from a rejected promise inside the generator.
|
|
97
|
+
* const values = await timeSlicedMap([1], function* (value) {
|
|
98
|
+
* try {
|
|
99
|
+
* return yield* waitFor(Promise.reject<number>(new TypeError("unavailable")));
|
|
100
|
+
* } catch {
|
|
101
|
+
* return value;
|
|
102
|
+
* }
|
|
103
|
+
* });
|
|
104
|
+
*/
|
|
105
|
+
export function* waitFor(promise) {
|
|
106
|
+
const result = {};
|
|
107
|
+
yield promise.then((value) => {
|
|
108
|
+
result.value = { resolved: value };
|
|
109
|
+
});
|
|
110
|
+
assert.ok(result.value, "waitFor must resume after its promise resolves");
|
|
111
|
+
return result.value.resolved;
|
|
112
|
+
}
|
|
113
|
+
function isCallbackGenerator(value) {
|
|
114
|
+
return (typeof value === "object" &&
|
|
115
|
+
value !== null &&
|
|
116
|
+
"next" in value &&
|
|
117
|
+
typeof value.next === "function" &&
|
|
118
|
+
"throw" in value &&
|
|
119
|
+
typeof value.throw === "function" &&
|
|
120
|
+
Symbol.iterator in value &&
|
|
121
|
+
typeof value[Symbol.iterator] === "function");
|
|
122
|
+
}
|
|
123
|
+
async function closeCallbackGenerator(generator, budget) {
|
|
124
|
+
const state = { step: budget.measure(() => generator.return(undefined)) };
|
|
125
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Drain yielded cleanup without calling the failed scheduler again.
|
|
126
|
+
while (!state.step.done) {
|
|
127
|
+
const outcome = await Promise.resolve(state.step.value).then((value) => ({ status: "fulfilled", value }), (error) => ({ status: "rejected", error }));
|
|
128
|
+
state.step =
|
|
129
|
+
outcome.status === "fulfilled"
|
|
130
|
+
? budget.measure(() => generator.next(outcome.value))
|
|
131
|
+
: budget.measure(() => generator.throw(outcome.error));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function runCallbackGenerator(generator, budget) {
|
|
135
|
+
const state = { step: budget.measure(() => generator.next()) };
|
|
136
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Each generator segment must settle before the next measured segment starts.
|
|
137
|
+
while (!state.step.done) {
|
|
138
|
+
// Attach rejection handling before a budget yield can let the event loop advance.
|
|
139
|
+
const settled = Promise.resolve(state.step.value).then((value) => ({ status: "fulfilled", value }), (error) => ({ status: "rejected", error }));
|
|
140
|
+
try {
|
|
141
|
+
const pending = budget.yieldIfNeeded();
|
|
142
|
+
if (pending !== undefined)
|
|
143
|
+
await pending;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
try {
|
|
147
|
+
await closeCallbackGenerator(generator, budget);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Cleanup failures must not replace the scheduler failure.
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
const outcome = await settled;
|
|
155
|
+
state.step =
|
|
156
|
+
outcome.status === "fulfilled"
|
|
157
|
+
? budget.measure(() => generator.next(outcome.value))
|
|
158
|
+
: budget.measure(() => generator.throw(outcome.error));
|
|
159
|
+
}
|
|
160
|
+
return state.step.value;
|
|
161
|
+
}
|
|
162
|
+
async function traverse(items, callback, accept, budget) {
|
|
163
|
+
const position = { index: 0 };
|
|
164
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Native iteration owns cleanup while callbacks and budget yields settle sequentially.
|
|
165
|
+
for (const item of items) {
|
|
166
|
+
const value = budget.measure(() => callback(item, position.index));
|
|
167
|
+
const result = isCallbackGenerator(value)
|
|
168
|
+
? await runCallbackGenerator(value, budget)
|
|
169
|
+
: value;
|
|
170
|
+
budget.measure(() => accept(result));
|
|
171
|
+
position.index += 1;
|
|
172
|
+
const pending = budget.yieldIfNeeded();
|
|
173
|
+
if (pending !== undefined)
|
|
174
|
+
await pending;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function timeSlicedForEach(items, callback, budget = new TimeSliceBudget()) {
|
|
178
|
+
await traverse(items, callback, () => { }, budget);
|
|
179
|
+
}
|
|
180
|
+
export async function timeSlicedMap(items, callback, budget = new TimeSliceBudget()) {
|
|
181
|
+
const results = [];
|
|
182
|
+
await traverse(items, callback, (result) => {
|
|
183
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Append to the local result to avoid quadratic copying.
|
|
184
|
+
results.push(result);
|
|
185
|
+
}, budget);
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Maps each item to a synchronous list and flattens the lists in input order.
|
|
190
|
+
*
|
|
191
|
+
* Measures callbacks and appends. It can yield within one large returned list.
|
|
192
|
+
* One callback cannot be interrupted. For asynchronous mapping, first use a
|
|
193
|
+
* generator callback with `timeSlicedMap`, then flatten the returned lists.
|
|
194
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
195
|
+
*
|
|
196
|
+
* @example Flatten synchronous results.
|
|
197
|
+
* const values = await timeSlicedFlatMap([1, 2], (value) => [value, value * 2]);
|
|
198
|
+
*
|
|
199
|
+
* @example Fetch lists with generators and share their budget with flattening.
|
|
200
|
+
* const budget = new TimeSliceBudget();
|
|
201
|
+
* const pages = await timeSlicedMap([1, 2], function* (value) {
|
|
202
|
+
* return yield* waitFor(Promise.resolve([value, value * 2]));
|
|
203
|
+
* }, budget);
|
|
204
|
+
* const values = await timeSlicedFlatMap(pages, (page) => page, budget);
|
|
205
|
+
*/
|
|
206
|
+
export async function timeSlicedFlatMap(items, callback, budget = new TimeSliceBudget()) {
|
|
207
|
+
const results = [];
|
|
208
|
+
const position = { index: 0 };
|
|
209
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Native outer iteration owns cleanup; inner traversal can also yield.
|
|
210
|
+
for (const item of items) {
|
|
211
|
+
const mapped = budget.measure(() => callback(item, position.index));
|
|
212
|
+
position.index += 1;
|
|
213
|
+
const pending = budget.yieldIfNeeded();
|
|
214
|
+
if (pending !== undefined)
|
|
215
|
+
await pending;
|
|
216
|
+
await timeSlicedForEach(mapped, (value) => {
|
|
217
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Append incrementally without argument-spread limits or quadratic copying.
|
|
218
|
+
results.push(value);
|
|
219
|
+
}, budget);
|
|
220
|
+
}
|
|
221
|
+
const pending = budget.yieldIfNeeded();
|
|
222
|
+
if (pending !== undefined)
|
|
223
|
+
await pending;
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Returns items whose synchronous predicate is true, preserving input order.
|
|
228
|
+
*
|
|
229
|
+
* Measures predicate work and result accumulation. One predicate call cannot be
|
|
230
|
+
* interrupted. The optional budget defaults to a new independent instance.
|
|
231
|
+
*
|
|
232
|
+
* @example Keep odd numbers.
|
|
233
|
+
* const odd = await timeSlicedFilter([1, 2, 3], (value) => value % 2 === 1);
|
|
234
|
+
*
|
|
235
|
+
* @example Compose filtering inside a generator with a shared budget.
|
|
236
|
+
* const budget = new TimeSliceBudget();
|
|
237
|
+
* const groups = await timeSlicedMap([[1, 2, 3]], function* (values) {
|
|
238
|
+
* return yield* waitFor(timeSlicedFilter(values, (value) => value > 1, budget));
|
|
239
|
+
* }, budget);
|
|
240
|
+
*/
|
|
241
|
+
export async function timeSlicedFilter(items, predicate, budget = new TimeSliceBudget()) {
|
|
242
|
+
const results = [];
|
|
243
|
+
await timeSlicedForEach(items, (item, index) => {
|
|
244
|
+
if (predicate(item, index)) {
|
|
245
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Append to the local result without quadratic copying.
|
|
246
|
+
results.push(item);
|
|
247
|
+
}
|
|
248
|
+
}, budget);
|
|
249
|
+
return results;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Groups items by a synchronous key, preserving key and group insertion order.
|
|
253
|
+
*
|
|
254
|
+
* Uses `Map` key equality. Measures key selection and group accumulation.
|
|
255
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
256
|
+
*
|
|
257
|
+
* @example Group strings by length.
|
|
258
|
+
* const groups = await timeSlicedGroupBy(["a", "bb", "c"], (text) => text.length);
|
|
259
|
+
*
|
|
260
|
+
* @example Await grouping inside a generator and share its budget.
|
|
261
|
+
* const budget = new TimeSliceBudget();
|
|
262
|
+
* const groupedPages = await timeSlicedMap([[1, 2, 3]], function* (values) {
|
|
263
|
+
* return yield* waitFor(timeSlicedGroupBy(values, (value) => value % 2, budget));
|
|
264
|
+
* }, budget);
|
|
265
|
+
*/
|
|
266
|
+
export async function timeSlicedGroupBy(items, keyOf, budget = new TimeSliceBudget()) {
|
|
267
|
+
const groups = new Map();
|
|
268
|
+
await timeSlicedForEach(items, (item, index) => {
|
|
269
|
+
const key = keyOf(item, index);
|
|
270
|
+
const group = groups.get(key);
|
|
271
|
+
if (group === undefined)
|
|
272
|
+
groups.set(key, [item]);
|
|
273
|
+
// oxlint-disable-next-line zingage/no-restricted-syntax -- Each local group accumulates without quadratic copying.
|
|
274
|
+
else
|
|
275
|
+
group.push(item);
|
|
276
|
+
}, budget);
|
|
277
|
+
return groups;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Builds a Map from entries in input order; the last value for a repeated key wins.
|
|
281
|
+
*
|
|
282
|
+
* Measures entry insertion. Replacing a value preserves the key's first insertion
|
|
283
|
+
* position. The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
284
|
+
*
|
|
285
|
+
* @example Build a Map while preserving last-value-wins behavior.
|
|
286
|
+
* const map = await timeSlicedMapFromEntries([["a", 1], ["a", 2]] as const);
|
|
287
|
+
*
|
|
288
|
+
* @example Await Map construction inside a generator with a shared budget.
|
|
289
|
+
* const budget = new TimeSliceBudget();
|
|
290
|
+
* const maps = await timeSlicedMap([1, 2], function* (value) {
|
|
291
|
+
* return yield* waitFor(timeSlicedMapFromEntries([["key", value]] as const, budget));
|
|
292
|
+
* }, budget);
|
|
293
|
+
*/
|
|
294
|
+
export async function timeSlicedMapFromEntries(entries, budget = new TimeSliceBudget()) {
|
|
295
|
+
const result = new Map();
|
|
296
|
+
await timeSlicedForEach(entries, ([key, value]) => {
|
|
297
|
+
result.set(key, value);
|
|
298
|
+
}, budget);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=time-slicing.js.map
|
|
@@ -0,0 +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;AAEpD,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAChC,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AAIzD;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,eAAe;IACjB,QAAQ,CAAC;IACT,IAAI,CAAC;IACL,iBAAiB,CAAC;IAC3B,YAAY,CAAC;IACb,MAAM,GAAG,CAAC,CAAC;IAEX;;;;;;;;;OASG;IACH,YACE,OAAO,GAAG,qBAAqB,EAC/B,GAAG,GAAG,cAAc,EACpB,gBAAgB,GAAwB,YAAY;QAEpD,MAAM,CAAC,EAAE,CACP,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EACvC,8CAA8C,CAC/C,CAAC;QACF,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAClC,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED;;;;;;;;;;;OAWG;IACH,OAAO,CAAS,IAAwC;QACtD,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC;QACpC,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;QACjB,IAAI,CAAC;YACH,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;YACjB,IAAI,SAAS;gBAAE,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC;QAC9D,CAAC;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACH,aAAa;QACX,IAAI,IAAI,CAAC,YAAY,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC5C,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC;QAClC,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC;IAClC,CAAC;CACF;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,SAAS,CAAC,CAAC,OAAO,CACtB,OAA2B;IAE3B,MAAM,MAAM,GAAoC,EAAE,CAAC;IACnD,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QAC3B,MAAM,CAAC,KAAK,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,gDAAgD,CAAC,CAAC;IAC1E,OAAO,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;AAC/B,CAAC;AAED,SAAS,mBAAmB,CAC1B,KAAyC;IAEzC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACf,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU;QAChC,OAAO,IAAI,KAAK;QAChB,OAAO,KAAK,CAAC,KAAK,KAAK,UAAU;QACjC,MAAM,CAAC,QAAQ,IAAI,KAAK;QACxB,OAAO,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,UAAU,CAC7C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,sBAAsB,CACnC,SAAqC,EACrC,MAAuB;IAEvB,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;IAC1E,6HAA6H;IAC7H,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAC1D,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,WAAoB,EAAE,KAAK,EAAE,CAAC,EACpD,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,UAAmB,EAAE,KAAK,EAAE,CAAC,CAC7D,CAAC;QACF,KAAK,CAAC,IAAI;YACR,OAAO,CAAC,MAAM,KAAK,WAAW;gBAC5B,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBACrD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,SAAoC,EACpC,MAAuB;IAEvB,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;IAC/D,uIAAuI;IACvI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,kFAAkF;QAClF,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CACpD,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,WAAoB,EAAE,KAAK,EAAE,CAAC,EACpD,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,UAAmB,EAAE,KAAK,EAAE,CAAC,CAC7D,CAAC;QACF,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;YACvC,IAAI,OAAO,KAAK,SAAS;gBAAE,MAAM,OAAO,CAAC;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,sBAAsB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAClD,CAAC;YAAC,MAAM,CAAC;gBACP,2DAA2D;YAC7D,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;QAC9B,KAAK,CAAC,IAAI;YACR,OAAO,CAAC,MAAM,KAAK,WAAW;gBAC5B,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBACrD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;AAC1B,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,KAAqB,EACrB,QAG+D,EAC/D,MAAgC,EAChC,MAAuB;IAEvB,MAAM,QAAQ,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAC9B,gJAAgJ;IAChJ,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAqC,GAAG,EAAE,CACpE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAC/B,CAAC;QACF,MAAM,MAAM,GAAG,mBAAmB,CAAS,KAAK,CAAC;YAC/C,CAAC,CAAC,MAAM,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC;YAC3C,CAAC,CAAC,KAAK,CAAC;QACV,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QACrC,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;QACpB,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;QACvC,IAAI,OAAO,KAAK,SAAS;YAAE,MAAM,OAAO,CAAC;IAC3C,CAAC;AACH,CAAC;AAoCD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAqB,EACrB,QAG+D,EAC/D,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,MAAM,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAoCD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAqB,EACrB,QAG+D,EAC/D,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,QAAQ,CACZ,KAAK,EACL,QAAQ,EACR,CAAC,MAAM,EAAE,EAAE;QACT,kHAAkH;QAClH,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC,EACD,MAAM,CACP,CAAC;IACF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAqB,EACrB,QAA0D,EAC1D,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAC9B,gIAAgI;IAChI,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QACpE,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,MAAM,iBAAiB,CACrB,MAAM,EACN,CAAC,KAAK,EAAE,EAAE;YACR,qIAAqI;YACrI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,EACD,MAAM,CACP,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;IACvC,IAAI,OAAO,KAAK,SAAS;QAAE,MAAM,OAAO,CAAC;IACzC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAqB,EACrB,SAAiD,EACjD,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,MAAM,OAAO,GAAW,EAAE,CAAC;IAC3B,MAAM,iBAAiB,CACrB,KAAK,EACL,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACd,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;YAC3B,iHAAiH;YACjH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,EACD,MAAM,CACP,CAAC;IACF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAqB,EACrB,KAA4D,EAC5D,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAe,CAAC;IACtC,MAAM,iBAAiB,CACrB,KAAK,EACL,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACd,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;QACjD,mHAAmH;;YAC9G,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC,EACD,MAAM,CACP,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAwC,EACxC,MAAM,GAAG,IAAI,eAAe,EAAE;IAE9B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAc,CAAC;IACrC,MAAM,iBAAiB,CACrB,OAAO,EACP,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QACf,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACzB,CAAC,EACD,MAAM,CACP,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zingage/scheduling",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/joinava/sensitive-packages.git",
|
|
7
|
+
"directory": "packages/scheduling"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"!dist/**/*.test.*",
|
|
13
|
+
"!src/**/*.test.ts",
|
|
14
|
+
"!dist/.tsbuildinfo"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org/"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^26.1.1",
|
|
30
|
+
"fast-check": "^4.2.0",
|
|
31
|
+
"oxlint": "^1.83.0",
|
|
32
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
33
|
+
"typescript": "npm:@typescript/typescript6@^6.0.0",
|
|
34
|
+
"typescript-7": "npm:typescript@^7.0.2",
|
|
35
|
+
"@zingage/base-configs-oxlint": "1.0.0",
|
|
36
|
+
"@zingage/base-configs-ts": "1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"tsc:watch": "tsc -b --watch",
|
|
40
|
+
"tsc:full-emit": "tsc -b --noCheck",
|
|
41
|
+
"tsc:check": "tsc -b --emitDeclarationOnly",
|
|
42
|
+
"lint:oxlint": "oxlint --type-aware src",
|
|
43
|
+
"test": "node --test \"./dist/**/*.test.js\"",
|
|
44
|
+
"test:watch": "node --test --watch \"./dist/**/*.test.js\"",
|
|
45
|
+
"test:ci": "NODE_OPTIONS=\"--test-reporter=junit --test-reporter-destination=./test-report.xml --test-reporter=spec --test-reporter-destination=stdout\" pnpm run test",
|
|
46
|
+
"dev": "pnpm run tsc:watch"
|
|
47
|
+
}
|
|
6
48
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { TimeSliceBudget, timeSlicedForEach } from "./time-slicing.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a new, stably sorted array using a synchronous comparator.
|
|
6
|
+
*
|
|
7
|
+
* A negative result puts the left value first; a positive result puts the right
|
|
8
|
+
* value first. Zero and NaN preserve input order. The comparator receives every
|
|
9
|
+
* input value, including undefined. The input is not reordered.
|
|
10
|
+
*
|
|
11
|
+
* Measures copying, comparisons, and merge writes. Checks the budget after each
|
|
12
|
+
* merge write, including writes that need no comparison. One comparator call
|
|
13
|
+
* cannot be interrupted. A nested helper cannot interrupt its containing
|
|
14
|
+
* measured segment. Uses O(n log n) work and O(n) additional memory.
|
|
15
|
+
* The optional budget defaults to a new independent `TimeSliceBudget`.
|
|
16
|
+
*
|
|
17
|
+
* @example Sort a readonly input without reordering it.
|
|
18
|
+
* const sorted = await timeSlicedSort([3, 1, 2] as const, (left, right) => left - right);
|
|
19
|
+
*
|
|
20
|
+
* @example Load values in a generator and share its budget with sorting.
|
|
21
|
+
* const budget = new TimeSliceBudget();
|
|
22
|
+
* const sortedPages = await timeSlicedMap([1, 2], function* (page) {
|
|
23
|
+
* const values = yield* waitFor(Promise.resolve([page + 1, page]));
|
|
24
|
+
* return yield* waitFor(timeSlicedSort(values, (left, right) => left - right, budget));
|
|
25
|
+
* }, budget);
|
|
26
|
+
*/
|
|
27
|
+
export async function timeSlicedSort<Item>(
|
|
28
|
+
items: Iterable<Item>,
|
|
29
|
+
compare: (left: Item, right: Item) => number,
|
|
30
|
+
budget = new TimeSliceBudget(),
|
|
31
|
+
): Promise<Item[]> {
|
|
32
|
+
// Boxing distinguishes an absent buffer entry from a valid undefined input value.
|
|
33
|
+
type Entry = { value: Item };
|
|
34
|
+
const buffers: { source: Entry[]; target: Entry[]; width: number } = {
|
|
35
|
+
source: [],
|
|
36
|
+
target: [],
|
|
37
|
+
width: 1,
|
|
38
|
+
};
|
|
39
|
+
/* oxlint-disable zingage/no-restricted-syntax -- Stable merging uses local buffers and sequential index updates to avoid quadratic copying and per-item promises. */
|
|
40
|
+
await timeSlicedForEach(
|
|
41
|
+
items,
|
|
42
|
+
(value) => {
|
|
43
|
+
buffers.source.push({ value });
|
|
44
|
+
},
|
|
45
|
+
budget,
|
|
46
|
+
);
|
|
47
|
+
const length = buffers.source.length;
|
|
48
|
+
while (buffers.width < length) {
|
|
49
|
+
const run = { start: 0 };
|
|
50
|
+
while (run.start < length) {
|
|
51
|
+
const middle = Math.min(run.start + buffers.width, length);
|
|
52
|
+
const end = Math.min(run.start + 2 * buffers.width, length);
|
|
53
|
+
const position = { left: run.start, right: middle, output: run.start };
|
|
54
|
+
while (position.output < end) {
|
|
55
|
+
budget.measure(() => {
|
|
56
|
+
const left =
|
|
57
|
+
position.left < middle ? buffers.source[position.left] : undefined;
|
|
58
|
+
const right =
|
|
59
|
+
position.right < end ? buffers.source[position.right] : undefined;
|
|
60
|
+
assert.ok(left !== undefined || position.left === middle);
|
|
61
|
+
assert.ok(right !== undefined || position.right === end);
|
|
62
|
+
if (
|
|
63
|
+
left !== undefined &&
|
|
64
|
+
(right === undefined || !(compare(left.value, right.value) > 0))
|
|
65
|
+
) {
|
|
66
|
+
buffers.target[position.output] = left;
|
|
67
|
+
position.left += 1;
|
|
68
|
+
} else {
|
|
69
|
+
assert.ok(right);
|
|
70
|
+
buffers.target[position.output] = right;
|
|
71
|
+
position.right += 1;
|
|
72
|
+
}
|
|
73
|
+
position.output += 1;
|
|
74
|
+
});
|
|
75
|
+
const pending = budget.yieldIfNeeded();
|
|
76
|
+
if (pending !== undefined) await pending;
|
|
77
|
+
}
|
|
78
|
+
run.start = end;
|
|
79
|
+
}
|
|
80
|
+
const previous = buffers.source;
|
|
81
|
+
buffers.source = buffers.target;
|
|
82
|
+
buffers.target = previous;
|
|
83
|
+
buffers.width *= 2;
|
|
84
|
+
}
|
|
85
|
+
const result: Item[] = [];
|
|
86
|
+
await timeSlicedForEach(
|
|
87
|
+
buffers.source,
|
|
88
|
+
(entry) => {
|
|
89
|
+
result.push(entry.value);
|
|
90
|
+
},
|
|
91
|
+
budget,
|
|
92
|
+
);
|
|
93
|
+
/* oxlint-enable zingage/no-restricted-syntax */
|
|
94
|
+
return result;
|
|
95
|
+
}
|