@qorejs/qore 0.7.0 → 0.7.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/README.md +170 -52
- package/dist/src/core/iterable.d.ts +1 -0
- package/dist/src/core/iterable.js +17 -0
- package/dist/src/core/response-runtime.d.ts +2 -0
- package/dist/src/core/response-runtime.js +233 -0
- package/dist/src/core/response-state.d.ts +3 -0
- package/dist/src/core/response-state.js +37 -0
- package/dist/src/core/response-types.d.ts +73 -0
- package/dist/src/core/response-types.js +1 -0
- package/dist/src/core/response.d.ts +4 -0
- package/dist/src/core/response.js +26 -0
- package/dist/src/core/signal-context.d.ts +10 -0
- package/dist/src/core/signal-context.js +69 -0
- package/dist/src/core/signal-nodes.d.ts +45 -0
- package/dist/src/core/signal-nodes.js +197 -0
- package/dist/src/core/signal-scheduler.d.ts +2 -0
- package/dist/src/core/signal-scheduler.js +19 -0
- package/dist/src/core/signal-types.d.ts +19 -0
- package/dist/src/core/signal-types.js +1 -0
- package/dist/src/core/signal.d.ts +19 -0
- package/dist/src/core/signal.js +41 -0
- package/dist/src/core/stream-backpressure.d.ts +3 -0
- package/dist/src/core/stream-backpressure.js +36 -0
- package/dist/src/core/stream-buffer.d.ts +17 -0
- package/dist/src/core/stream-buffer.js +162 -0
- package/dist/src/core/stream-iterator.d.ts +6 -0
- package/dist/src/core/stream-iterator.js +14 -0
- package/dist/src/core/stream-lifecycle.d.ts +10 -0
- package/dist/src/core/stream-lifecycle.js +28 -0
- package/dist/src/core/stream-queue.d.ts +17 -0
- package/dist/src/core/stream-queue.js +62 -0
- package/dist/src/core/stream-runtime.d.ts +2 -0
- package/dist/src/core/stream-runtime.js +127 -0
- package/dist/src/core/stream-source.d.ts +2 -0
- package/dist/src/core/stream-source.js +28 -0
- package/dist/src/core/stream-state.d.ts +4 -0
- package/dist/src/core/stream-state.js +20 -0
- package/dist/src/core/stream-types.d.ts +61 -0
- package/dist/src/core/stream-types.js +1 -0
- package/dist/src/core/stream.d.ts +11 -0
- package/dist/src/core/stream.js +90 -0
- package/dist/src/dom/app.d.ts +38 -0
- package/dist/src/dom/app.js +102 -0
- package/dist/src/dom/dom.d.ts +13 -0
- package/dist/src/dom/dom.js +197 -0
- package/dist/src/dom/properties.d.ts +3 -0
- package/dist/src/dom/properties.js +147 -0
- package/dist/src/dom/reactive.d.ts +6 -0
- package/dist/src/dom/reactive.js +14 -0
- package/dist/src/dom/response-view.d.ts +4 -0
- package/dist/src/dom/response-view.js +37 -0
- package/dist/src/dom/scope.d.ts +10 -0
- package/dist/src/dom/scope.js +49 -0
- package/dist/src/dom/types.d.ts +34 -0
- package/dist/src/dom/types.js +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +10 -0
- package/dist/src/providers/anthropic.d.ts +2 -0
- package/dist/src/providers/anthropic.js +102 -0
- package/dist/src/providers/openai.d.ts +2 -0
- package/dist/src/providers/openai.js +96 -0
- package/dist/src/providers/sse-adapter.d.ts +2 -0
- package/dist/src/providers/sse-adapter.js +83 -0
- package/dist/src/providers/sse-env.d.ts +4 -0
- package/dist/src/providers/sse-env.js +33 -0
- package/dist/src/providers/sse-parser.d.ts +5 -0
- package/dist/src/providers/sse-parser.js +99 -0
- package/dist/src/providers/sse.d.ts +4 -0
- package/dist/src/providers/sse.js +3 -0
- package/dist/src/providers/types.d.ts +94 -0
- package/dist/src/providers/types.js +1 -0
- package/dist/src/shared/utils.d.ts +2 -0
- package/dist/src/shared/utils.js +32 -0
- package/package.json +25 -11
- package/src/anthropic.js +0 -122
- package/src/app.js +0 -123
- package/src/dom.js +0 -527
- package/src/index.d.ts +0 -405
- package/src/index.js +0 -10
- package/src/iterable.js +0 -20
- package/src/openai.js +0 -112
- package/src/response.js +0 -312
- package/src/signal.js +0 -328
- package/src/sse.js +0 -264
- package/src/stream.js +0 -582
- package/src/utils.js +0 -39
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createDeferred } from './stream-backpressure.js';
|
|
2
|
+
import { sleep } from '../shared/utils.js';
|
|
3
|
+
export function createStreamBuffer({ enqueueChunk, fail, isTerminated, pressure, readCurrent, recordDrop, signal, updateBuffered, writeChunk }) {
|
|
4
|
+
const pendingWrites = [];
|
|
5
|
+
const spaceWaiters = [];
|
|
6
|
+
const scheduledPushes = new Set();
|
|
7
|
+
let scheduledPushError = null;
|
|
8
|
+
let drainPromise = null;
|
|
9
|
+
let lastPushAt = 0;
|
|
10
|
+
function releaseSpaceWaiters() {
|
|
11
|
+
if (pressure.buffer === Infinity) {
|
|
12
|
+
while (spaceWaiters.length > 0) {
|
|
13
|
+
spaceWaiters.shift()?.resolve(undefined);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
while (spaceWaiters.length > 0 && pendingWrites.length < pressure.buffer) {
|
|
18
|
+
spaceWaiters.shift()?.resolve(undefined);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function flush(result = readCurrent()) {
|
|
22
|
+
while (pendingWrites.length > 0) {
|
|
23
|
+
pendingWrites.shift()?.resolve(result);
|
|
24
|
+
}
|
|
25
|
+
updateBuffered(0);
|
|
26
|
+
releaseSpaceWaiters();
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
async function flushScheduled() {
|
|
30
|
+
while (scheduledPushes.size > 0) {
|
|
31
|
+
await Promise.allSettled(Array.from(scheduledPushes));
|
|
32
|
+
}
|
|
33
|
+
if (scheduledPushError !== null) {
|
|
34
|
+
throw scheduledPushError;
|
|
35
|
+
}
|
|
36
|
+
while (drainPromise) {
|
|
37
|
+
await drainPromise;
|
|
38
|
+
}
|
|
39
|
+
if (scheduledPushError !== null) {
|
|
40
|
+
throw scheduledPushError;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function waitForNextWindow() {
|
|
44
|
+
if (pressure.interval <= 0 || lastPushAt === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const remaining = pressure.interval - (Date.now() - lastPushAt);
|
|
48
|
+
if (remaining > 0) {
|
|
49
|
+
try {
|
|
50
|
+
await sleep(remaining, signal);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!signal.aborted) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function scheduleDrain() {
|
|
60
|
+
if (drainPromise || isTerminated()) {
|
|
61
|
+
return drainPromise;
|
|
62
|
+
}
|
|
63
|
+
drainPromise = (async () => {
|
|
64
|
+
try {
|
|
65
|
+
while (pendingWrites.length > 0 && !isTerminated() && !signal.aborted) {
|
|
66
|
+
const entry = pendingWrites.shift();
|
|
67
|
+
updateBuffered(pendingWrites.length);
|
|
68
|
+
releaseSpaceWaiters();
|
|
69
|
+
await waitForNextWindow();
|
|
70
|
+
if (isTerminated() || signal.aborted) {
|
|
71
|
+
entry.resolve(readCurrent());
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
enqueueChunk(entry.chunk);
|
|
75
|
+
const nextValue = writeChunk(entry.chunk);
|
|
76
|
+
lastPushAt = Date.now();
|
|
77
|
+
entry.resolve(nextValue);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
drainPromise = null;
|
|
82
|
+
if (pendingWrites.length > 0 && !isTerminated() && !signal.aborted) {
|
|
83
|
+
scheduleDrain();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
return drainPromise;
|
|
88
|
+
}
|
|
89
|
+
async function waitForBufferSpace() {
|
|
90
|
+
if (pressure.buffer === Infinity) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
while (!isTerminated() && !signal.aborted && pendingWrites.length >= pressure.buffer) {
|
|
94
|
+
const gate = createDeferred();
|
|
95
|
+
spaceWaiters.push(gate);
|
|
96
|
+
await gate.promise;
|
|
97
|
+
}
|
|
98
|
+
return !isTerminated() && !signal.aborted;
|
|
99
|
+
}
|
|
100
|
+
async function push(chunk) {
|
|
101
|
+
if (isTerminated() || signal.aborted) {
|
|
102
|
+
return readCurrent();
|
|
103
|
+
}
|
|
104
|
+
const writeTask = (async () => {
|
|
105
|
+
if (pressure.overflow === 'wait') {
|
|
106
|
+
const hasSpace = await waitForBufferSpace();
|
|
107
|
+
if (!hasSpace) {
|
|
108
|
+
return readCurrent();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (pressure.buffer !== Infinity && pendingWrites.length >= pressure.buffer) {
|
|
112
|
+
if (pressure.overflow === 'drop-oldest' && pendingWrites.length > 0) {
|
|
113
|
+
const droppedWrite = pendingWrites.shift();
|
|
114
|
+
recordDrop();
|
|
115
|
+
droppedWrite.resolve(readCurrent());
|
|
116
|
+
}
|
|
117
|
+
else if (pressure.overflow === 'drop-newest' || pressure.overflow === 'drop-oldest') {
|
|
118
|
+
recordDrop();
|
|
119
|
+
return readCurrent();
|
|
120
|
+
}
|
|
121
|
+
else if (pressure.overflow === 'error') {
|
|
122
|
+
const overflowError = new Error('Qore stream backpressure buffer overflow');
|
|
123
|
+
if (scheduledPushError === null) {
|
|
124
|
+
scheduledPushError = overflowError;
|
|
125
|
+
}
|
|
126
|
+
fail(overflowError);
|
|
127
|
+
return readCurrent();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (isTerminated() || signal.aborted) {
|
|
131
|
+
return readCurrent();
|
|
132
|
+
}
|
|
133
|
+
const entry = createDeferred();
|
|
134
|
+
entry.chunk = chunk;
|
|
135
|
+
pendingWrites.push(entry);
|
|
136
|
+
updateBuffered(pendingWrites.length);
|
|
137
|
+
scheduleDrain();
|
|
138
|
+
return entry.promise;
|
|
139
|
+
})();
|
|
140
|
+
scheduledPushes.add(writeTask);
|
|
141
|
+
try {
|
|
142
|
+
return await writeTask;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (scheduledPushError === null) {
|
|
146
|
+
scheduledPushError = error;
|
|
147
|
+
}
|
|
148
|
+
if (!isTerminated() && !signal.aborted) {
|
|
149
|
+
fail(error);
|
|
150
|
+
}
|
|
151
|
+
return readCurrent();
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
scheduledPushes.delete(writeTask);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
flush,
|
|
159
|
+
flushScheduled,
|
|
160
|
+
push
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AsyncQueue } from './stream-queue.js';
|
|
2
|
+
import type { QoreStream } from './stream-types.js';
|
|
3
|
+
export type IterableStream<TChunk, TValue> = QoreStream<TChunk, TValue> & {
|
|
4
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<TChunk>;
|
|
5
|
+
};
|
|
6
|
+
export declare function attachStreamIterator<TChunk, TValue>(readable: IterableStream<TChunk, TValue>, queue: AsyncQueue<TChunk>, shouldAbortOnReturn: () => boolean, abort: (reason?: unknown) => TValue): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function attachStreamIterator(readable, queue, shouldAbortOnReturn, abort) {
|
|
2
|
+
readable[Symbol.asyncIterator] = async function* () {
|
|
3
|
+
try {
|
|
4
|
+
for await (const chunk of queue) {
|
|
5
|
+
yield chunk;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
if (shouldAbortOnReturn()) {
|
|
10
|
+
abort('Stream consumer disposed');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface StreamLifecycle<TValue> {
|
|
2
|
+
isTerminated(): boolean;
|
|
3
|
+
markTerminated(): boolean;
|
|
4
|
+
setCleanup(cleanup: () => TValue | void): void;
|
|
5
|
+
stop(finalizer: () => TValue, cleanup?: () => TValue | void): TValue;
|
|
6
|
+
}
|
|
7
|
+
export declare function createStreamLifecycle<TValue>({ closeQueue, readCurrent }: {
|
|
8
|
+
closeQueue(): void;
|
|
9
|
+
readCurrent(): TValue;
|
|
10
|
+
}): StreamLifecycle<TValue>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function createStreamLifecycle({ closeQueue, readCurrent }) {
|
|
2
|
+
let terminated = false;
|
|
3
|
+
let activeCleanup = readCurrent;
|
|
4
|
+
return {
|
|
5
|
+
isTerminated() {
|
|
6
|
+
return terminated;
|
|
7
|
+
},
|
|
8
|
+
markTerminated() {
|
|
9
|
+
if (terminated) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
terminated = true;
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
setCleanup(cleanup) {
|
|
16
|
+
activeCleanup = cleanup;
|
|
17
|
+
},
|
|
18
|
+
stop(finalizer, cleanup = activeCleanup) {
|
|
19
|
+
if (terminated) {
|
|
20
|
+
return readCurrent();
|
|
21
|
+
}
|
|
22
|
+
terminated = true;
|
|
23
|
+
cleanup();
|
|
24
|
+
closeQueue();
|
|
25
|
+
return finalizer();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface QueueWaiter<T> {
|
|
2
|
+
resolve(result: IteratorResult<T>): void;
|
|
3
|
+
reject(reason?: unknown): void;
|
|
4
|
+
}
|
|
5
|
+
export declare class AsyncQueue<T> implements AsyncIterableIterator<T> {
|
|
6
|
+
values: T[];
|
|
7
|
+
waiters: QueueWaiter<T>[];
|
|
8
|
+
closed: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
push(value: T): void;
|
|
11
|
+
close(): void;
|
|
12
|
+
fail(error: unknown): void;
|
|
13
|
+
next(): Promise<IteratorResult<T>>;
|
|
14
|
+
return(): Promise<IteratorResult<T>>;
|
|
15
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<T>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { normalizeError } from '../shared/utils.js';
|
|
2
|
+
// Bridge producer pushes and async iteration with a minimal internal queue.
|
|
3
|
+
export class AsyncQueue {
|
|
4
|
+
values = [];
|
|
5
|
+
waiters = [];
|
|
6
|
+
closed = false;
|
|
7
|
+
error = null;
|
|
8
|
+
push(value) {
|
|
9
|
+
if (this.closed) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const waiter = this.waiters.shift();
|
|
13
|
+
if (waiter) {
|
|
14
|
+
waiter.resolve({ value, done: false });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.values.push(value);
|
|
18
|
+
}
|
|
19
|
+
close() {
|
|
20
|
+
if (this.closed) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.closed = true;
|
|
24
|
+
while (this.waiters.length > 0) {
|
|
25
|
+
const waiter = this.waiters.shift();
|
|
26
|
+
waiter?.resolve({ value: undefined, done: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
fail(error) {
|
|
30
|
+
if (this.closed) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.closed = true;
|
|
34
|
+
this.error = normalizeError(error);
|
|
35
|
+
while (this.waiters.length > 0) {
|
|
36
|
+
const waiter = this.waiters.shift();
|
|
37
|
+
waiter?.reject(this.error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
next() {
|
|
41
|
+
if (this.values.length > 0) {
|
|
42
|
+
const value = this.values.shift();
|
|
43
|
+
return Promise.resolve({ value, done: false });
|
|
44
|
+
}
|
|
45
|
+
if (this.closed) {
|
|
46
|
+
if (this.error) {
|
|
47
|
+
return Promise.reject(this.error);
|
|
48
|
+
}
|
|
49
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
50
|
+
}
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
this.waiters.push({ resolve, reject });
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return() {
|
|
56
|
+
this.close();
|
|
57
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
58
|
+
}
|
|
59
|
+
[Symbol.asyncIterator]() {
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { signal as createSignal } from './signal.js';
|
|
2
|
+
import { createResponse } from './response.js';
|
|
3
|
+
import { normalizeBackpressure } from './stream-backpressure.js';
|
|
4
|
+
import { createStreamBuffer } from './stream-buffer.js';
|
|
5
|
+
import { attachStreamIterator } from './stream-iterator.js';
|
|
6
|
+
import { createStreamLifecycle } from './stream-lifecycle.js';
|
|
7
|
+
import { AsyncQueue } from './stream-queue.js';
|
|
8
|
+
import { startSource } from './stream-source.js';
|
|
9
|
+
import { createReadableArraySignal, createReadableSignal, reduceText } from './stream-state.js';
|
|
10
|
+
import { normalizeError } from '../shared/utils.js';
|
|
11
|
+
// Create the core stream primitive: a read-only signal plus async iterable plus lifecycle state.
|
|
12
|
+
export function createStream(sourceOrSetup, options = {}) {
|
|
13
|
+
const { seed = '', reduce = reduceText, backpressure = null } = options;
|
|
14
|
+
const pressure = normalizeBackpressure(backpressure);
|
|
15
|
+
const state = createResponse({ seed, reduce });
|
|
16
|
+
const queue = new AsyncQueue();
|
|
17
|
+
const readable = createReadableSignal(state.value);
|
|
18
|
+
const buffered = createSignal(0);
|
|
19
|
+
const dropped = createSignal(0);
|
|
20
|
+
const lifecycle = createStreamLifecycle({
|
|
21
|
+
closeQueue: () => queue.close(),
|
|
22
|
+
readCurrent: () => readable.peek()
|
|
23
|
+
});
|
|
24
|
+
let activeSignal = null;
|
|
25
|
+
const run = state.run(async ({ signal: runtimeSignal, push, complete, fail, abort }) => {
|
|
26
|
+
activeSignal = runtimeSignal;
|
|
27
|
+
function failStream(error) {
|
|
28
|
+
if (lifecycle.isTerminated()) {
|
|
29
|
+
return state.error.peek() ?? normalizeError(error);
|
|
30
|
+
}
|
|
31
|
+
lifecycle.markTerminated();
|
|
32
|
+
buffer.flush();
|
|
33
|
+
queue.fail(error);
|
|
34
|
+
return fail(error);
|
|
35
|
+
}
|
|
36
|
+
const buffer = createStreamBuffer({
|
|
37
|
+
enqueueChunk: (chunk) => queue.push(chunk),
|
|
38
|
+
fail: failStream,
|
|
39
|
+
isTerminated: lifecycle.isTerminated,
|
|
40
|
+
pressure,
|
|
41
|
+
readCurrent: () => readable.peek(),
|
|
42
|
+
recordDrop: () => dropped(dropped.peek() + 1),
|
|
43
|
+
signal: runtimeSignal,
|
|
44
|
+
updateBuffered: (count) => buffered(count),
|
|
45
|
+
writeChunk: push
|
|
46
|
+
});
|
|
47
|
+
lifecycle.setCleanup(buffer.flush);
|
|
48
|
+
// Wrap the response lifecycle so streams can push, fail, or abort through one surface.
|
|
49
|
+
const controller = {
|
|
50
|
+
get signal() {
|
|
51
|
+
return runtimeSignal;
|
|
52
|
+
},
|
|
53
|
+
// Queue chunks first, then let the serialized drain loop move them into state/UI.
|
|
54
|
+
async push(chunk) {
|
|
55
|
+
return buffer.push(chunk);
|
|
56
|
+
},
|
|
57
|
+
// Mark the stream as gracefully finished.
|
|
58
|
+
done() {
|
|
59
|
+
return lifecycle.stop(() => complete());
|
|
60
|
+
},
|
|
61
|
+
// Surface producer failures to both async consumers and signal readers.
|
|
62
|
+
fail(error) {
|
|
63
|
+
return failStream(error);
|
|
64
|
+
},
|
|
65
|
+
// Abort the stream and preserve the partial value already accumulated.
|
|
66
|
+
abort(reason = 'Stream aborted') {
|
|
67
|
+
return lifecycle.stop(() => abort(reason));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
await startSource(sourceOrSetup, controller);
|
|
72
|
+
await buffer.flushScheduled();
|
|
73
|
+
if (!lifecycle.isTerminated() && runtimeSignal.aborted) {
|
|
74
|
+
controller.abort(runtimeSignal.reason ?? 'Stream aborted');
|
|
75
|
+
return readable.peek();
|
|
76
|
+
}
|
|
77
|
+
if (!lifecycle.isTerminated()) {
|
|
78
|
+
controller.done();
|
|
79
|
+
}
|
|
80
|
+
return readable.peek();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const terminalStatus = state.status.peek();
|
|
84
|
+
if (runtimeSignal.aborted || terminalStatus === 'aborted') {
|
|
85
|
+
return readable.peek();
|
|
86
|
+
}
|
|
87
|
+
if (lifecycle.isTerminated() && terminalStatus !== 'error') {
|
|
88
|
+
return readable.peek();
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Keep async consumers in sync with failures that surface after startup.
|
|
94
|
+
run.catch((error) => {
|
|
95
|
+
if (!queue.closed) {
|
|
96
|
+
queue.fail(error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
attachStreamState(readable, state, buffered, dropped, run, lifecycle.stop);
|
|
100
|
+
// Expose the current AbortSignal so advanced integrations can observe cancellation.
|
|
101
|
+
Object.defineProperty(readable, 'signal', {
|
|
102
|
+
enumerable: true,
|
|
103
|
+
get() {
|
|
104
|
+
return activeSignal;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
attachStreamIterator(readable, queue, () => !lifecycle.isTerminated() && !state.aborted(), (reason) => readable.abort(reason));
|
|
108
|
+
return readable;
|
|
109
|
+
}
|
|
110
|
+
function attachStreamState(readable, state, buffered, dropped, run, stopStream) {
|
|
111
|
+
readable.status = createReadableSignal(state.status);
|
|
112
|
+
readable.error = createReadableSignal(state.error);
|
|
113
|
+
readable.chunks = createReadableArraySignal(state.chunks);
|
|
114
|
+
readable.startedAt = createReadableSignal(state.startedAt);
|
|
115
|
+
readable.finishedAt = createReadableSignal(state.finishedAt);
|
|
116
|
+
readable.pending = state.pending;
|
|
117
|
+
readable.streaming = state.streaming;
|
|
118
|
+
readable.completed = state.completed;
|
|
119
|
+
readable.failed = state.failed;
|
|
120
|
+
readable.aborted = state.aborted;
|
|
121
|
+
readable.chunkCount = state.chunkCount;
|
|
122
|
+
readable.snapshot = state.snapshot;
|
|
123
|
+
readable.buffered = createReadableSignal(buffered);
|
|
124
|
+
readable.dropped = createReadableSignal(dropped);
|
|
125
|
+
readable.ready = run;
|
|
126
|
+
readable.abort = (reason) => stopStream(() => state.abort(reason));
|
|
127
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { toAsyncIterable } from './iterable.js';
|
|
2
|
+
// Distinguish a setup callback from callable stream or signal-like values.
|
|
3
|
+
function isSetupFunction(sourceOrSetup) {
|
|
4
|
+
const callableSource = sourceOrSetup;
|
|
5
|
+
return typeof sourceOrSetup === 'function'
|
|
6
|
+
&& typeof callableSource[Symbol.asyncIterator] !== 'function'
|
|
7
|
+
&& typeof callableSource.peek !== 'function';
|
|
8
|
+
}
|
|
9
|
+
// Pipe any async iterable-like source into the controller, honoring aborts and pacing.
|
|
10
|
+
async function pipeSource(source, controller) {
|
|
11
|
+
for await (const chunk of toAsyncIterable(source)) {
|
|
12
|
+
if (controller.signal.aborted) {
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
await controller.push(chunk);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Accept either a setup callback or a pre-existing iterable source.
|
|
19
|
+
export async function startSource(sourceOrSetup, controller) {
|
|
20
|
+
if (isSetupFunction(sourceOrSetup)) {
|
|
21
|
+
const maybeSource = await sourceOrSetup(controller);
|
|
22
|
+
if (maybeSource !== undefined) {
|
|
23
|
+
await pipeSource(maybeSource, controller);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await pipeSource(sourceOrSetup, controller);
|
|
28
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReadonlySignal } from './signal.js';
|
|
2
|
+
export declare function createReadableSignal<T>(sourceSignal: ReadonlySignal<T>): ReadonlySignal<T>;
|
|
3
|
+
export declare function createReadableArraySignal<T>(sourceSignal: ReadonlySignal<T[]>): ReadonlySignal<T[]>;
|
|
4
|
+
export declare function reduceText(currentValue: string, chunk: unknown): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Expose a response value as a read-only signal surface.
|
|
2
|
+
export function createReadableSignal(sourceSignal) {
|
|
3
|
+
const read = (() => sourceSignal());
|
|
4
|
+
read.peek = () => sourceSignal.peek();
|
|
5
|
+
read.subscribe = (listener, options) => sourceSignal.subscribe(listener, options);
|
|
6
|
+
return read;
|
|
7
|
+
}
|
|
8
|
+
// Arrays are copied at the public stream boundary so readers cannot mutate internal chunk state.
|
|
9
|
+
export function createReadableArraySignal(sourceSignal) {
|
|
10
|
+
const read = (() => [...sourceSignal()]);
|
|
11
|
+
read.peek = () => [...sourceSignal.peek()];
|
|
12
|
+
read.subscribe = (listener, options) => sourceSignal.subscribe((value) => {
|
|
13
|
+
listener([...value]);
|
|
14
|
+
}, options);
|
|
15
|
+
return read;
|
|
16
|
+
}
|
|
17
|
+
// Text streams concatenate chunks by default so they can drive text nodes directly.
|
|
18
|
+
export function reduceText(currentValue, chunk) {
|
|
19
|
+
return currentValue + String(chunk ?? '');
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReadonlySignal } from './signal.js';
|
|
2
|
+
import type { GlobalAbortSignal, MaybePromise, ResponseSnapshot, ResponseState, ResponseStatus, SourceLike } from './response.js';
|
|
3
|
+
export type OverflowStrategy = 'wait' | 'drop-oldest' | 'drop-newest' | 'error';
|
|
4
|
+
export interface BackpressureOptions {
|
|
5
|
+
interval?: number;
|
|
6
|
+
buffer?: number;
|
|
7
|
+
overflow?: OverflowStrategy;
|
|
8
|
+
}
|
|
9
|
+
export interface NormalizedBackpressure {
|
|
10
|
+
interval: number;
|
|
11
|
+
buffer: number;
|
|
12
|
+
overflow: OverflowStrategy;
|
|
13
|
+
}
|
|
14
|
+
export interface Deferred<T> {
|
|
15
|
+
promise: Promise<T>;
|
|
16
|
+
resolve(value: T | PromiseLike<T>): void;
|
|
17
|
+
reject(reason?: unknown): void;
|
|
18
|
+
}
|
|
19
|
+
export interface StreamOptions<TChunk = unknown, TValue = string> {
|
|
20
|
+
seed?: TValue;
|
|
21
|
+
reduce?: (currentValue: TValue, chunk: TChunk, index: number) => TValue;
|
|
22
|
+
backpressure?: number | BackpressureOptions | null;
|
|
23
|
+
}
|
|
24
|
+
export interface StreamController<TChunk = unknown, TValue = string> {
|
|
25
|
+
readonly signal: GlobalAbortSignal;
|
|
26
|
+
push(chunk: TChunk): Promise<TValue>;
|
|
27
|
+
done(): TValue;
|
|
28
|
+
fail(error?: unknown): Error | TValue;
|
|
29
|
+
abort(reason?: unknown): TValue;
|
|
30
|
+
}
|
|
31
|
+
export type StreamSetup<TChunk, TValue> = (controller: StreamController<TChunk, TValue>) => MaybePromise<void | SourceLike<TChunk>>;
|
|
32
|
+
export type StreamInput<TChunk, TValue> = SourceLike<TChunk> | StreamSetup<TChunk, TValue>;
|
|
33
|
+
export interface QoreStream<TChunk = unknown, TValue = string> extends ReadonlySignal<TValue>, AsyncIterable<TChunk> {
|
|
34
|
+
status: ReadonlySignal<ResponseStatus>;
|
|
35
|
+
error: ReadonlySignal<Error | null>;
|
|
36
|
+
chunks: ReadonlySignal<TChunk[]>;
|
|
37
|
+
startedAt: ReadonlySignal<number | null>;
|
|
38
|
+
finishedAt: ReadonlySignal<number | null>;
|
|
39
|
+
pending: ReadonlySignal<boolean>;
|
|
40
|
+
streaming: ReadonlySignal<boolean>;
|
|
41
|
+
completed: ReadonlySignal<boolean>;
|
|
42
|
+
failed: ReadonlySignal<boolean>;
|
|
43
|
+
aborted: ReadonlySignal<boolean>;
|
|
44
|
+
chunkCount: ReadonlySignal<number>;
|
|
45
|
+
buffered: ReadonlySignal<number>;
|
|
46
|
+
dropped: ReadonlySignal<number>;
|
|
47
|
+
snapshot(): ResponseSnapshot<TChunk, TValue>;
|
|
48
|
+
ready: Promise<TValue>;
|
|
49
|
+
abort(reason?: unknown): TValue;
|
|
50
|
+
readonly signal?: GlobalAbortSignal | null;
|
|
51
|
+
}
|
|
52
|
+
export interface StreamFactory {
|
|
53
|
+
<TChunk = unknown>(sourceOrSetup: StreamInput<TChunk, string>, options?: StreamOptions<TChunk, string>): QoreStream<TChunk, string>;
|
|
54
|
+
create<TChunk, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
55
|
+
text<TChunk = unknown>(sourceOrSetup: StreamInput<TChunk, string>, options?: StreamOptions<TChunk, string>): QoreStream<TChunk, string>;
|
|
56
|
+
list<TChunk>(sourceOrSetup: StreamInput<TChunk, TChunk[]>, options?: StreamOptions<TChunk, TChunk[]>): QoreStream<TChunk, TChunk[]>;
|
|
57
|
+
latest<TChunk>(sourceOrSetup: StreamInput<TChunk, TChunk | null>, options?: StreamOptions<TChunk, TChunk | null>): QoreStream<TChunk, TChunk | null>;
|
|
58
|
+
withBackpressure<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, backpressure: number | BackpressureOptions, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
59
|
+
paced<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, interval: number, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
60
|
+
}
|
|
61
|
+
export type StreamResponseState<TChunk, TValue> = ResponseState<TChunk, TValue>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MaybePromise, SourceLike } from './response.js';
|
|
2
|
+
import type { QoreStream, StreamFactory, StreamOptions } from './stream-types.js';
|
|
3
|
+
export declare const stream: StreamFactory;
|
|
4
|
+
export declare function from<TChunk, TValue = string>(source: SourceLike<TChunk>, options?: StreamOptions<TChunk, TValue> & {
|
|
5
|
+
delay?: number;
|
|
6
|
+
}): QoreStream<TChunk, TValue>;
|
|
7
|
+
export declare function mapStream<TInput, TOutput, TValue = string>(source: SourceLike<TInput>, mapper: (chunk: TInput, index: number) => MaybePromise<TOutput>, options?: StreamOptions<TOutput, TValue>): QoreStream<TOutput, TValue>;
|
|
8
|
+
export declare function scanStream<TInput, TOutput>(source: SourceLike<TInput>, reducer: (currentValue: TOutput, chunk: TInput, index: number) => MaybePromise<TOutput>, seed: TOutput, options?: Omit<StreamOptions<TOutput, TOutput>, 'seed' | 'reduce'>): QoreStream<TOutput, TOutput>;
|
|
9
|
+
export { createStream } from './stream-runtime.js';
|
|
10
|
+
export type { BackpressureOptions, QoreStream, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './stream-types.js';
|
|
11
|
+
export { toAsyncIterable } from './iterable.js';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { toAsyncIterable } from './iterable.js';
|
|
2
|
+
import { createStream } from './stream-runtime.js';
|
|
3
|
+
import { reduceText } from './stream-state.js';
|
|
4
|
+
import { sleep } from '../shared/utils.js';
|
|
5
|
+
const streamFactory = ((sourceOrSetup, options = {}) => createStream(sourceOrSetup, {
|
|
6
|
+
seed: '',
|
|
7
|
+
reduce: reduceText,
|
|
8
|
+
...options
|
|
9
|
+
}));
|
|
10
|
+
// Alias the generic constructor for advanced callers that provide custom reducers.
|
|
11
|
+
streamFactory.create = createStream;
|
|
12
|
+
// Convenience constructor for explicit text streams.
|
|
13
|
+
streamFactory.text = (sourceOrSetup, options = {}) => createStream(sourceOrSetup, {
|
|
14
|
+
seed: '',
|
|
15
|
+
reduce: reduceText,
|
|
16
|
+
...options
|
|
17
|
+
});
|
|
18
|
+
// Convenience constructor for streams that accumulate every chunk into an array.
|
|
19
|
+
streamFactory.list = (sourceOrSetup, options = {}) => createStream(sourceOrSetup, {
|
|
20
|
+
seed: [],
|
|
21
|
+
reduce: (currentValue, chunk) => [...currentValue, chunk],
|
|
22
|
+
...options
|
|
23
|
+
});
|
|
24
|
+
// Convenience constructor for streams that only expose the latest chunk as current value.
|
|
25
|
+
streamFactory.latest = (sourceOrSetup, options = {}) => createStream(sourceOrSetup, {
|
|
26
|
+
seed: null,
|
|
27
|
+
reduce: (_, chunk) => chunk,
|
|
28
|
+
...options
|
|
29
|
+
});
|
|
30
|
+
// Attach backpressure behavior without changing the reducer semantics.
|
|
31
|
+
streamFactory.withBackpressure = (sourceOrSetup, backpressure, options = {}) => createStream(sourceOrSetup, {
|
|
32
|
+
...options,
|
|
33
|
+
backpressure: typeof options.backpressure === 'object' && options.backpressure !== null && typeof backpressure === 'object'
|
|
34
|
+
? { ...options.backpressure, ...backpressure }
|
|
35
|
+
: backpressure
|
|
36
|
+
});
|
|
37
|
+
// Shorthand for the first backpressure primitive: minimum interval between chunks.
|
|
38
|
+
streamFactory.paced = (sourceOrSetup, interval, options = {}) => streamFactory.withBackpressure(sourceOrSetup, { interval }, options);
|
|
39
|
+
// Create a text-accumulating stream by default.
|
|
40
|
+
export const stream = streamFactory;
|
|
41
|
+
// Turn any iterable or async iterable into a stream, optionally adding source delay.
|
|
42
|
+
export function from(source, options = {}) {
|
|
43
|
+
const { delay = 0, ...streamOptions } = options;
|
|
44
|
+
return createStream(async (controller) => {
|
|
45
|
+
for await (const chunk of toAsyncIterable(source)) {
|
|
46
|
+
if (controller.signal.aborted) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (delay > 0) {
|
|
50
|
+
await sleep(delay, controller.signal);
|
|
51
|
+
}
|
|
52
|
+
await controller.push(chunk);
|
|
53
|
+
}
|
|
54
|
+
}, streamOptions);
|
|
55
|
+
}
|
|
56
|
+
// Map source chunks into a new stream while preserving stream semantics.
|
|
57
|
+
export function mapStream(source, mapper, options = {}) {
|
|
58
|
+
return createStream(async (controller) => {
|
|
59
|
+
let index = 0;
|
|
60
|
+
for await (const chunk of toAsyncIterable(source)) {
|
|
61
|
+
if (controller.signal.aborted) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
await controller.push(await mapper(chunk, index));
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
}, options);
|
|
68
|
+
}
|
|
69
|
+
// Emit a running reduction so each chunk sees the accumulated state so far.
|
|
70
|
+
export function scanStream(source, reducer, seed, options = {}) {
|
|
71
|
+
return createStream(async (controller) => {
|
|
72
|
+
let index = 0;
|
|
73
|
+
let current = seed;
|
|
74
|
+
for await (const chunk of toAsyncIterable(source)) {
|
|
75
|
+
if (controller.signal.aborted) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
current = await reducer(current, chunk, index);
|
|
79
|
+
await controller.push(current);
|
|
80
|
+
index += 1;
|
|
81
|
+
}
|
|
82
|
+
}, {
|
|
83
|
+
seed,
|
|
84
|
+
reduce: (_, chunk) => chunk,
|
|
85
|
+
...options
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Re-export the iterable helper so integrations can normalize external sources.
|
|
89
|
+
export { createStream } from './stream-runtime.js';
|
|
90
|
+
export { toAsyncIterable } from './iterable.js';
|