@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 +21 -0
- package/README.md +152 -2
- package/dist/index.d.mts +429 -0
- package/dist/index.mjs +778 -0
- package/package.json +34 -3
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
|
-
#
|
|
1
|
+
# @proj-airi/stream-kit
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|