@proj-airi/stream-kit 0.0.0-stage → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-PRESENT Neko Ayaka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,153 @@
1
- # Temporary Holding Version
1
+ # @proj-airi/stream-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lifetime, scheduling, and stream helpers for AIRI. The package has no UI, audio, or provider dependency.
4
+
5
+ ## Use it for
6
+
7
+ - One unit of async work with one outcome, a timeout, and cleanups: `createTask` and `runTask`.
8
+ - Work that only the newest input needs, like `switchMap`: `createLatest`.
9
+ - A lifetime for resources with child lifetimes: `createScope`.
10
+ - A readable stream that a push source writes to: `createPushStream`.
11
+ - A queue that runs handlers for each item in order: `createQueue`.
12
+ - Event stream state: batches, a bounded buffer, sequence and lateness checks, demand, leases, cooldowns, and a feed that catches up new subscribers. See [Event streams](#event-streams).
13
+
14
+ ## Do not use it for
15
+
16
+ - Audio windows, captures, or playback. Use `@proj-airi/pipelines-audio`.
17
+ - Cross-process events. Use `@moeru/eventa`.
18
+
19
+ ## Tasks
20
+
21
+ A task settles once with an `Outcome`: `finished`, `cancelled`, or `failed`.
22
+
23
+ ```ts
24
+ const task = runTask(async ({ signal }) => fetchText(url, signal), { signal: parent, timeoutMs: 5000 })
25
+ task.defer(() => connection.close())
26
+ const outcome = await task.done
27
+ ```
28
+
29
+ - `signal` aborts on cancellation, failure, timeout, or parent abort. It does not abort on `finish`, so handed-off work, such as queued playback, continues.
30
+ - Cleanups run once, in reverse order, after the outcome. `closed` resolves after they settle.
31
+ - `worstOutcome(outcomes)` merges outcomes. A failure wins over a cancellation, and a cancellation wins over success.
32
+
33
+ ## Newest input only
34
+
35
+ ```ts
36
+ const runner = createLatest<Document>(async (document, signal) => {
37
+ cache.set(document.text, await rewrite(document.text, signal))
38
+ }, { key: document => document.text, debounceMs: 600 })
39
+
40
+ runner.push(document)
41
+ ```
42
+
43
+ - A new key aborts the active run and replaces a waiting input. An equal key keeps the active run.
44
+ - `debounceMs` waits until the key stays the same for that long.
45
+ - `cancelPending()` drops a waiting input and keeps the active run.
46
+ - Errors of superseded or closed runs are not reported.
47
+
48
+ ## Event streams
49
+
50
+ These primitives keep no hidden clock. Each method that needs the time takes `now`, and only `createBatcher` starts a timer.
51
+
52
+ ### Batches: `createBatcher`
53
+
54
+ Groups items, like `groupedWithin`. A batch flushes at `maxItems` items or `maxWaitMs` after its first item, whichever comes first. Use it to send many small messages as fewer large ones.
55
+
56
+ ```ts
57
+ const batcher = createBatcher<TraceRecord>({ maxWaitMs: 50, maxItems: 500, flush: records => send({ records }) })
58
+ batcher.push(record)
59
+ batcher.close() // flushes the rest
60
+ ```
61
+
62
+ - `schedule` replaces the `setTimeout` timer, for example in tests.
63
+ - With `now`, a push after the deadline flushes at once, also when a background tab delays the timer.
64
+
65
+ ### Bounded buffer: `RetentionBuffer`
66
+
67
+ Keeps the newest items within `maxBytes` and `maxAgeMs`, in insertion order. Use it for a replay buffer or a flight recorder window.
68
+
69
+ ```ts
70
+ const buffer = new RetentionBuffer<Entry>({ maxBytes: 8_000_000, maxAgeMs: 600_000, sizeOf: entry => entry.bytes, ageOf: entry => entry.observedAt, onEvict: reportGap })
71
+ buffer.push(entry) // applies the byte limit
72
+ buffer.evict(now) // applies both limits, from a periodic tick
73
+ ```
74
+
75
+ - Only the oldest items drop. Eviction costs time for the dropped items only.
76
+ - `onEvict` receives each group of dropped items, so the caller can report a gap.
77
+
78
+ ### Sequence numbers: `SequenceTracker`
79
+
80
+ Accepts each sequence number once, in any arrival order. Use it to drop repeats after a reconnect or a retry.
81
+
82
+ ```ts
83
+ const tracker = new SequenceTracker()
84
+ if (tracker.accept(record.seq))
85
+ handle(record) // a repeat does not reach this line
86
+ ```
87
+
88
+ `nextSeq - 1` is the end of the unbroken prefix.
89
+
90
+ ### Late events: `Watermark`
91
+
92
+ Tracks the newest event time and marks an event that is more than `allowedLatenessMs` behind it.
93
+
94
+ ```ts
95
+ const watermark = new Watermark({ allowedLatenessMs: 2000 })
96
+ const { late } = watermark.observe(record.ts)
97
+ ```
98
+
99
+ ### Wanted keys: `DemandUnion`
100
+
101
+ The union of what each member wants. A `'*'` member makes the union `'*'`. Use it so that a producer skips work that no viewer wants.
102
+
103
+ ```ts
104
+ const demand = new DemandUnion<string>({ onChange: union => source.setWanted(union) })
105
+ demand.set(viewerId, ['audio.level'])
106
+ demand.delete(viewerId)
107
+ ```
108
+
109
+ `onChange` runs only when the union changes.
110
+
111
+ ### Leases: `createLeaseTable`
112
+
113
+ Keys that stay alive while their owner renews them. Use it for heartbeats and repeated announcements.
114
+
115
+ ```ts
116
+ const leases = createLeaseTable<string>({ ttlMs: 12_000 })
117
+ leases.touch(tabId, now)
118
+ for (const tab of leases.expired(now))
119
+ forgetTab(tab)
120
+ ```
121
+
122
+ ### Cooldowns: `createCooldown`
123
+
124
+ Allows an action at most once per window for each key.
125
+
126
+ ```ts
127
+ const cooldown = createCooldown({ ms: 30_000 })
128
+ if (cooldown.tryAcquire(source, now))
129
+ dump()
130
+ ```
131
+
132
+ ### Catch-up feed: `createCatchUpFeed`
133
+
134
+ A live feed in which a new subscriber receives the backlog, then an optional `caughtUp` message, then live messages. Use it when a viewer opens after the stream started.
135
+
136
+ ```ts
137
+ const feed = createCatchUpFeed<Message>({ backlog: () => buffer.values(), caughtUp: () => ({ kind: 'caughtUp' }) })
138
+ const { unsubscribe } = feed.subscribe(render)
139
+ buffer.push(record)
140
+ feed.publish({ kind: 'record', record })
141
+ ```
142
+
143
+ - No message is lost or repeated between the backlog and the live messages. The owner must update the backlog state before each `publish`.
144
+ - A `publish` during the backlog delivery waits, and goes to the new subscriber after `caughtUp`.
145
+ - The backlog and `caughtUp` arrive synchronously inside `subscribe`.
146
+ - The `strategy` applies to live messages. `unbounded` calls the listener inside `publish`. `sliding` and `dropping` keep up to `capacity` messages and deliver them in a microtask. A full `sliding` queue drops its oldest message, and a full `dropping` queue drops the new one.
147
+
148
+ ## Checks
149
+
150
+ ```sh
151
+ pnpm -F @proj-airi/stream-kit typecheck
152
+ pnpm -F @proj-airi/stream-kit test:run
153
+ ```
@@ -0,0 +1,429 @@
1
+ //#region src/batcher.d.ts
2
+ /** Runs `callback` after `delayMs`. Returns a function that cancels the run. */
3
+ type BatchScheduler = (callback: () => void, delayMs: number) => () => void;
4
+ interface BatcherOptions<T> {
5
+ /** The longest time that the first pending item waits before a flush. */
6
+ readonly maxWaitMs: number;
7
+ /** Flushes at once when this many items wait. @default Infinity */
8
+ readonly maxItems?: number;
9
+ /** Receives each nonempty batch, in push order. A push inside `flush` starts a new batch. */
10
+ readonly flush: (items: T[]) => void;
11
+ /**
12
+ * The current time. When given, a push after the deadline of the batch flushes at once, also when the timer runs
13
+ * late, for example in a throttled background tab. Omission leaves the deadline to the timer only.
14
+ */
15
+ readonly now?: () => number;
16
+ /** Starts the wait timer. @default a `setTimeout` and `clearTimeout` pair */
17
+ readonly schedule?: BatchScheduler;
18
+ }
19
+ interface Batcher<T> {
20
+ push: (item: T) => void;
21
+ /** Flushes the pending items now. Does nothing when no item waits. */
22
+ flush: () => void;
23
+ /** Flushes the pending items and ignores later pushes. */
24
+ close: () => void;
25
+ }
26
+ /**
27
+ * Groups items into batches, like `groupedWithin`. A batch flushes when it has `maxItems` items, or `maxWaitMs`
28
+ * after its first item, whichever comes first.
29
+ *
30
+ * @example
31
+ * const batcher = createBatcher<TraceRecord>({ maxWaitMs: 50, maxItems: 500, flush: records => send({ records }) })
32
+ * batcher.push(record)
33
+ */
34
+ declare function createBatcher<T>(options: BatcherOptions<T>): Batcher<T>;
35
+ //#endregion
36
+ //#region src/catch-up-feed.d.ts
37
+ /**
38
+ * How a subscriber receives live messages.
39
+ *
40
+ * - `unbounded`: `publish` calls the listener at once. A slow listener slows the publisher.
41
+ * - `sliding`: a queue of `capacity` messages, delivered in a microtask. A full queue drops its oldest message.
42
+ * - `dropping`: a queue of `capacity` messages, delivered in a microtask. A full queue drops the new message.
43
+ */
44
+ type DeliveryStrategy = 'unbounded' | 'sliding' | 'dropping';
45
+ interface SubscribeOptions {
46
+ /** @default 'unbounded' */
47
+ readonly strategy?: DeliveryStrategy;
48
+ /** The queue size of `sliding` and `dropping`. @default 256 */
49
+ readonly capacity?: number;
50
+ }
51
+ interface CatchUpFeedOptions<M> {
52
+ /** The messages that a new subscriber receives first, for example the kept buffer. Read once per subscribe. */
53
+ readonly backlog: () => Iterable<M>;
54
+ /** A message after the backlog, for example a cursor. Created when the backlog is read. */
55
+ readonly caughtUp?: () => M;
56
+ }
57
+ interface CatchUpFeed<M> {
58
+ /** Sends a live message. Update the state behind `backlog` before this call. */
59
+ publish: (message: M) => void;
60
+ subscribe: (listener: (message: M) => void, options?: SubscribeOptions) => {
61
+ unsubscribe: () => void;
62
+ };
63
+ /** The number of active subscribers, including one that is still catching up. */
64
+ readonly subscribers: number;
65
+ }
66
+ /**
67
+ * A live feed in which a new subscriber first receives the backlog, then `caughtUp`, then the live messages.
68
+ *
69
+ * Guarantee: no message is lost or repeated between the backlog and the live messages, if the owner updates the
70
+ * state behind `backlog` before each `publish`. `subscribe` copies the backlog first. A `publish` during the backlog
71
+ * delivery, for example from a listener, waits and goes to the new subscriber after `caughtUp`. A subscriber added
72
+ * during a `publish` does not receive that message live, because its backlog already has it.
73
+ *
74
+ * The backlog and `caughtUp` always arrive synchronously inside `subscribe`. The strategy applies to live messages.
75
+ *
76
+ * @example
77
+ * const feed = createCatchUpFeed<Message>({ backlog: () => buffer.values(), caughtUp: () => ({ kind: 'caughtUp' }) })
78
+ * buffer.push(record)
79
+ * feed.publish({ kind: 'record', record })
80
+ */
81
+ declare function createCatchUpFeed<M>(options: CatchUpFeedOptions<M>): CatchUpFeed<M>;
82
+ //#endregion
83
+ //#region src/cooldown.d.ts
84
+ interface CooldownOptions {
85
+ /** The shortest time between two successful acquires of one key. */
86
+ readonly ms: number;
87
+ }
88
+ interface Cooldown {
89
+ /** Returns true and starts a new window when the window of `key` has passed. Returns false inside the window. */
90
+ tryAcquire: (key: string, now: number) => boolean;
91
+ }
92
+ /**
93
+ * Limits an action to once per window for each key, for example one flight recorder dump per source.
94
+ *
95
+ * The cooldown keeps one time for each key that it has seen.
96
+ *
97
+ * @example
98
+ * const cooldown = createCooldown({ ms: 30_000 })
99
+ * if (cooldown.tryAcquire(source, now))
100
+ * dump()
101
+ */
102
+ declare function createCooldown(options: CooldownOptions): Cooldown;
103
+ //#endregion
104
+ //#region src/demand-union.d.ts
105
+ /** A set of wanted keys, or `'*'` for all keys. */
106
+ type Demand<T> = ReadonlySet<T> | '*';
107
+ interface DemandUnionOptions<T> {
108
+ /** Receives the new union after each change of it. A `set` that keeps the union the same does not call it. */
109
+ readonly onChange?: (union: Demand<T>) => void;
110
+ }
111
+ /**
112
+ * The union of what each member wants, for example the tracks of all viewers.
113
+ *
114
+ * A producer reads the union to skip work that no member wants. One `'*'` member makes the union `'*'`.
115
+ *
116
+ * @example
117
+ * const demand = new DemandUnion<string>({ onChange: union => source.setWanted(union) })
118
+ * demand.set('viewer-1', ['audio.level'])
119
+ * demand.set('viewer-2', '*') // onChange('*')
120
+ * demand.delete('viewer-2') // onChange(Set { 'audio.level' })
121
+ */
122
+ declare class DemandUnion<T> {
123
+ private readonly options;
124
+ private readonly members;
125
+ private current;
126
+ constructor(options?: DemandUnionOptions<T>);
127
+ get union(): Demand<T>;
128
+ set(id: string, demand: Iterable<T> | '*'): void;
129
+ delete(id: string): void;
130
+ private update;
131
+ private compute;
132
+ }
133
+ //#endregion
134
+ //#region src/latest.d.ts
135
+ interface LatestOptions<T> {
136
+ /**
137
+ * Values with an equal key (`Object.is`) are the same input. A repeated key keeps the active run.
138
+ * Omission treats every pushed value as new.
139
+ */
140
+ readonly key?: (value: T) => unknown;
141
+ /** Waits until no new key arrives for this long, then runs. @default 0 */
142
+ readonly debounceMs?: number;
143
+ /** Closes the runner. */
144
+ readonly signal?: AbortSignal;
145
+ /** Receives errors from runs that were not superseded or closed. */
146
+ readonly onError?: (error: unknown) => void;
147
+ }
148
+ /**
149
+ * Runs work for the newest input only, like `switchMap`.
150
+ *
151
+ * Each new key aborts the active run and replaces any waiting input. A run sees one value and one signal. The signal
152
+ * aborts when a newer key arrives or the runner closes. Errors after that abort are expected and not reported.
153
+ */
154
+ interface Latest<T> {
155
+ push: (value: T) => void;
156
+ /** Drops an input that waits for its debounce. The active run continues. */
157
+ cancelPending: () => void;
158
+ /** Resolves when no run is active or waiting. */
159
+ idle: () => Promise<void>;
160
+ close: (reason?: string) => void;
161
+ }
162
+ /**
163
+ * Creates a runner that keeps only the newest input.
164
+ *
165
+ * @example
166
+ * const runner = createLatest<string>(async (text, signal) => cache.set(text, await rephrase(text, signal)), { debounceMs: 600 })
167
+ * runner.push('hello')
168
+ * runner.push('hello world') // aborts the run for 'hello'
169
+ */
170
+ declare function createLatest<T>(run: (value: T, signal: AbortSignal) => Promise<void> | void, options?: LatestOptions<T>): Latest<T>;
171
+ //#endregion
172
+ //#region src/lease-table.d.ts
173
+ interface LeaseTableOptions {
174
+ /** A key expires when no `touch` renews it for longer than this. */
175
+ readonly ttlMs: number;
176
+ }
177
+ interface LeaseTable<K> {
178
+ /** Adds the key, or renews its lease. */
179
+ touch: (key: K, now: number) => void;
180
+ /** Removes and returns the keys whose last `touch` is more than `ttlMs` before `now`. */
181
+ expired: (now: number) => K[];
182
+ delete: (key: K) => void;
183
+ has: (key: K) => boolean;
184
+ keys: () => IterableIterator<K>;
185
+ }
186
+ /**
187
+ * Keys that stay alive while their owner renews them, for example a remote source with a heartbeat, or a tab that
188
+ * repeats its announcement. The caller decides when to call `expired`, usually from a periodic tick.
189
+ *
190
+ * @example
191
+ * const leases = createLeaseTable<string>({ ttlMs: 12_000 })
192
+ * leases.touch('tab-1', now)
193
+ * for (const tab of leases.expired(later))
194
+ * forgetTab(tab)
195
+ */
196
+ declare function createLeaseTable<K>(options: LeaseTableOptions): LeaseTable<K>;
197
+ //#endregion
198
+ //#region src/queue.d.ts
199
+ interface HandlerContext<T> {
200
+ data: T;
201
+ emit: (eventName: string, ...params: unknown[]) => void;
202
+ }
203
+ interface Events<T> {
204
+ enqueue: Array<(payload: T, queueLength: number) => void>;
205
+ dequeue: Array<(payload: T, queueLength: number) => void>;
206
+ process: Array<(payload: T, handler: (param: HandlerContext<T>) => Promise<unknown>) => void>;
207
+ error: Array<(payload: T, error: unknown, handler: (param: HandlerContext<T>) => Promise<unknown>) => void>;
208
+ result: Array<(<R>(payload: T, result: R, handler: (param: HandlerContext<T>) => Promise<unknown>) => void)>;
209
+ drain: Array<() => void>;
210
+ }
211
+ declare function createQueue<T>(options: {
212
+ handlers: Array<(ctx: HandlerContext<T>) => Promise<void>>;
213
+ }): {
214
+ enqueue: (payload: T) => void;
215
+ clear: () => void;
216
+ length: () => number;
217
+ on: <E extends keyof Events<T>>(eventName: E, listener: Events<T>[E][number]) => void;
218
+ onHandlerEvent: (eventName: string, listener: (...params: unknown[]) => void) => void;
219
+ };
220
+ type UseQueueReturn<T> = ReturnType<typeof createQueue<T>>;
221
+ //#endregion
222
+ //#region src/retention-buffer.d.ts
223
+ interface RetentionBufferOptions<T> {
224
+ /** Drops the oldest items while the total size is over this limit. @default Infinity */
225
+ readonly maxBytes?: number;
226
+ /** `evict` drops items that are older than this. @default Infinity */
227
+ readonly maxAgeMs?: number;
228
+ /** The size of an item. The buffer reads it once, at `push`. */
229
+ readonly sizeOf: (item: T) => number;
230
+ /** The time of an item, in the clock of the `now` that `evict` receives. */
231
+ readonly ageOf: (item: T) => number;
232
+ /** Receives each group of dropped items, oldest first. A caller can report the gap here. */
233
+ readonly onEvict?: (items: T[]) => void;
234
+ }
235
+ /**
236
+ * Keeps the newest items within a byte limit and an age limit, in insertion order.
237
+ *
238
+ * Only the head can drop, so the age limit assumes that `ageOf` grows with insertion order. An older item behind a
239
+ * newer one stays until the newer one drops. A `push` applies only the byte limit, because the age limit needs `now`.
240
+ * One item that is larger than `maxBytes` drops at once. Eviction costs time for the dropped items only.
241
+ *
242
+ * @example
243
+ * const buffer = new RetentionBuffer<Entry>({ maxBytes: 8_000_000, maxAgeMs: 600_000, sizeOf: e => e.bytes, ageOf: e => e.observedAt })
244
+ * buffer.push(entry)
245
+ * buffer.evict(now) // call it from a periodic tick
246
+ */
247
+ declare class RetentionBuffer<T> {
248
+ private readonly options;
249
+ private entries;
250
+ private head;
251
+ private totalBytes;
252
+ constructor(options: RetentionBufferOptions<T>);
253
+ get size(): number;
254
+ get bytes(): number;
255
+ push(item: T): void;
256
+ /** Drops the items older than the age limit, then the oldest items over the byte limit. Returns the dropped items. */
257
+ evict(now: number): T[];
258
+ /** The kept items, oldest first. Do not push or evict while you iterate. */
259
+ values(): IterableIterator<T>;
260
+ /** Drops every item without `onEvict`. */
261
+ clear(): void;
262
+ private evictWhile;
263
+ }
264
+ //#endregion
265
+ //#region src/scope.d.ts
266
+ /**
267
+ * One owned lifetime.
268
+ *
269
+ * `signal` aborts when the scope closes, either through `close` or through its parent signal.
270
+ * Deferred cleanups run once, in reverse registration order, after the signal aborts.
271
+ */
272
+ interface Scope {
273
+ readonly signal: AbortSignal;
274
+ /** Resolves after every deferred cleanup has settled. Cleanup errors are reported, not thrown. */
275
+ readonly closed: Promise<void>;
276
+ /** Registers cleanup. A cleanup registered after closing runs immediately. */
277
+ defer: (cleanup: () => unknown) => void;
278
+ /**
279
+ * Creates a scope that closes with this one, and can also close earlier by itself.
280
+ * Children start closing before the parent's own cleanups. The parent does not await child cleanup.
281
+ */
282
+ child: () => Scope;
283
+ close: (reason?: unknown) => Promise<void>;
284
+ }
285
+ /**
286
+ * Creates a scope that closes when `parent` aborts.
287
+ *
288
+ * @example
289
+ * const scope = createScope(options.signal)
290
+ * scope.defer(() => track.stop())
291
+ * await scope.close('Recording finished')
292
+ */
293
+ declare function createScope(parent?: AbortSignal, onCleanupError?: (error: unknown) => void): Scope;
294
+ //#endregion
295
+ //#region src/sequence-tracker.d.ts
296
+ /**
297
+ * Accepts each sequence number once, in any arrival order.
298
+ *
299
+ * Numbers below `nextSeq` and repeats are not new. A number above `nextSeq` waits in a set until the numbers before
300
+ * it arrive. `nextSeq - 1` is the end of the unbroken prefix, so a reader can resume from it.
301
+ *
302
+ * @example
303
+ * const tracker = new SequenceTracker()
304
+ * tracker.accept(1) // => true, waits for 0
305
+ * tracker.accept(0) // => true, nextSeq is now 2
306
+ * tracker.accept(1) // => false
307
+ */
308
+ declare class SequenceTracker {
309
+ private next;
310
+ private readonly ahead;
311
+ constructor(firstSeq?: number);
312
+ /** The lowest number that did not arrive yet. */
313
+ get nextSeq(): number;
314
+ /** Returns true when `seq` is new. */
315
+ accept(seq: number): boolean;
316
+ }
317
+ //#endregion
318
+ //#region src/stream.d.ts
319
+ interface StreamController<T> {
320
+ stream: ReadableStream<T>;
321
+ write: (value: T) => void;
322
+ close: () => void;
323
+ error: (err: unknown) => void;
324
+ isClosed: () => boolean;
325
+ /** Remaining queue capacity under `strategy`. Negative when the reader is behind. Null after an error. */
326
+ desiredSize: () => number | null;
327
+ }
328
+ /**
329
+ * The optional cancellation handler owns resources that feed this readable output.
330
+ * `strategy` measures unread values. Writers check `desiredSize` because a push source cannot wait.
331
+ */
332
+ declare function createPushStream<T>(onCancel?: (reason: unknown) => void, strategy?: QueuingStrategy<T>): StreamController<T>;
333
+ declare function readStream<T>(stream: ReadableStream<T>, handler: (value: T) => Promise<void> | void): Promise<void>;
334
+ //#endregion
335
+ //#region src/task.d.ts
336
+ /** The result of one task. Cancellation and failure carry their cause. */
337
+ type Outcome<T = void> = {
338
+ readonly status: 'finished';
339
+ readonly value: T;
340
+ } | {
341
+ readonly status: 'cancelled';
342
+ readonly reason: string;
343
+ } | {
344
+ readonly status: 'failed';
345
+ readonly error: Error;
346
+ };
347
+ interface TaskOptions {
348
+ /** The task cancels when this signal aborts. */
349
+ readonly signal?: AbortSignal;
350
+ /** The task fails with a timeout error after this many milliseconds. Omission adds no timeout. */
351
+ readonly timeoutMs?: number;
352
+ /** Receives cleanup errors. Cleanup errors never change the outcome. */
353
+ readonly onCleanupError?: (error: unknown) => void;
354
+ }
355
+ /**
356
+ * One unit of work with one outcome.
357
+ *
358
+ * `signal` aborts on cancellation, failure, or timeout. It does not abort on `finish`, so work that the task
359
+ * handed off, for example queued playback, continues. Deferred cleanups run once, in reverse order, after the
360
+ * outcome is set.
361
+ */
362
+ interface Task<T = void> {
363
+ readonly signal: AbortSignal;
364
+ readonly done: Promise<Outcome<T>>;
365
+ /** Resolves after every deferred cleanup has settled. */
366
+ readonly closed: Promise<void>;
367
+ /** True after the first of `finish`, `cancel`, or `fail`. */
368
+ readonly settled: boolean;
369
+ /** Registers cleanup. A cleanup registered after settlement runs at once. */
370
+ defer: (cleanup: () => unknown) => void;
371
+ finish: (value: T) => void;
372
+ cancel: (reason: string) => void;
373
+ fail: (error: Error) => void;
374
+ }
375
+ /**
376
+ * Creates a task that settles by an explicit call.
377
+ *
378
+ * @example
379
+ * const task = createTask<string>({ signal: parent.signal, timeoutMs: 5000 })
380
+ * task.defer(() => socket.close())
381
+ * task.finish('ok')
382
+ * await task.done // => { status: 'finished', value: 'ok' }
383
+ */
384
+ declare function createTask<T = void>(options?: TaskOptions): Task<T>;
385
+ /**
386
+ * Runs `work` as one task. The returned value finishes the task, and a thrown error fails it.
387
+ * A call to `cancel` or `fail` inside `work` wins over its later return.
388
+ *
389
+ * @example
390
+ * const task = runTask(async ({ signal }) => fetch(url, { signal }).then(response => response.text()))
391
+ * const outcome = await task.done
392
+ */
393
+ declare function runTask<T>(work: (task: Task<T>) => Promise<T> | T, options?: TaskOptions): Task<T>;
394
+ /**
395
+ * Merges outcomes. A failure wins over a cancellation, and a cancellation wins over success.
396
+ *
397
+ * @example
398
+ * worstOutcome([{ status: 'finished', value: undefined }, { status: 'cancelled', reason: 'Closed' }])
399
+ * // => { status: 'cancelled', reason: 'Closed' }
400
+ */
401
+ declare function worstOutcome(outcomes: readonly Outcome<unknown>[]): Outcome;
402
+ //#endregion
403
+ //#region src/watermark.d.ts
404
+ interface WatermarkOptions {
405
+ /** How far behind the newest time an event can be before it is late. */
406
+ readonly allowedLatenessMs: number;
407
+ }
408
+ /**
409
+ * Tracks the newest event time of a stream and marks events that arrive too late.
410
+ *
411
+ * A late event is still an event. The caller decides what to do with it, for example count it or flag it in a view.
412
+ *
413
+ * @example
414
+ * const watermark = new Watermark({ allowedLatenessMs: 2000 })
415
+ * watermark.observe(10_000) // => { late: false }
416
+ * watermark.observe(7_000) // => { late: true }
417
+ */
418
+ declare class Watermark {
419
+ private readonly options;
420
+ private newestTime;
421
+ constructor(options: WatermarkOptions);
422
+ /** The newest time observed. `-Infinity` before the first event. */
423
+ get newest(): number;
424
+ observe(time: number): {
425
+ late: boolean;
426
+ };
427
+ }
428
+ //#endregion
429
+ export { BatchScheduler, Batcher, BatcherOptions, CatchUpFeed, CatchUpFeedOptions, Cooldown, CooldownOptions, DeliveryStrategy, Demand, DemandUnion, DemandUnionOptions, Events, HandlerContext, Latest, LatestOptions, LeaseTable, LeaseTableOptions, Outcome, RetentionBuffer, RetentionBufferOptions, Scope, SequenceTracker, StreamController, SubscribeOptions, Task, TaskOptions, UseQueueReturn, Watermark, WatermarkOptions, createBatcher, createCatchUpFeed, createCooldown, createLatest, createLeaseTable, createPushStream, createQueue, createScope, createTask, readStream, runTask, worstOutcome };