@qorejs/qore 0.7.1 → 0.7.3
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 +139 -3
- 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 +7 -0
- package/dist/src/core/response-state.js +102 -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,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';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { dynamic, fragment, h, list, renderResponse, show, text } from './dom.js';
|
|
2
|
+
import { batch, computed, effect, signal, untrack } from '../core/signal.js';
|
|
3
|
+
import { response } from '../core/response.js';
|
|
4
|
+
import { from, mapStream, scanStream, stream } from '../core/stream.js';
|
|
5
|
+
import type { MountTarget, MountView, QoreChild, QoreElement } from './types.js';
|
|
6
|
+
export interface AppContext<Props extends Record<string, unknown>> {
|
|
7
|
+
app: QoreApp<Props>;
|
|
8
|
+
root: QoreElement;
|
|
9
|
+
props: Props;
|
|
10
|
+
signal: typeof signal;
|
|
11
|
+
computed: typeof computed;
|
|
12
|
+
effect: typeof effect;
|
|
13
|
+
batch: typeof batch;
|
|
14
|
+
untrack: typeof untrack;
|
|
15
|
+
stream: typeof stream;
|
|
16
|
+
from: typeof from;
|
|
17
|
+
mapStream: typeof mapStream;
|
|
18
|
+
scanStream: typeof scanStream;
|
|
19
|
+
response: typeof response;
|
|
20
|
+
h: typeof h;
|
|
21
|
+
text: typeof text;
|
|
22
|
+
dynamic: typeof dynamic;
|
|
23
|
+
show: typeof show;
|
|
24
|
+
list: typeof list;
|
|
25
|
+
fragment: typeof fragment;
|
|
26
|
+
renderResponse: typeof renderResponse;
|
|
27
|
+
onCleanup(handler: (() => void) | null | undefined): (() => void) | null | undefined;
|
|
28
|
+
}
|
|
29
|
+
export type AppSetupResult = QoreChild | {
|
|
30
|
+
view?: MountView;
|
|
31
|
+
onMount?: (root: QoreElement) => void;
|
|
32
|
+
};
|
|
33
|
+
export interface QoreApp<Props extends Record<string, unknown>> {
|
|
34
|
+
mount(target: MountTarget, props?: Props): QoreElement;
|
|
35
|
+
unmount(): QoreApp<Props>;
|
|
36
|
+
readonly root: QoreElement | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function createApp<Props extends Record<string, unknown> = Record<string, unknown>>(setup: (context: AppContext<Props>) => AppSetupResult): QoreApp<Props>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { dynamic, fragment, h, list, mount, renderResponse, show, text } from './dom.js';
|
|
2
|
+
import { batch, computed, effect, signal, untrack } from '../core/signal.js';
|
|
3
|
+
import { response } from '../core/response.js';
|
|
4
|
+
import { from, mapStream, scanStream, stream } from '../core/stream.js';
|
|
5
|
+
// Resolve a CSS selector or direct node into the root mount target.
|
|
6
|
+
function resolveTarget(target) {
|
|
7
|
+
if (typeof document === 'undefined') {
|
|
8
|
+
throw new Error('Qore app mounting requires a browser-like environment');
|
|
9
|
+
}
|
|
10
|
+
if (typeof target === 'string') {
|
|
11
|
+
const element = document.querySelector(target);
|
|
12
|
+
if (!element) {
|
|
13
|
+
throw new Error(`Qore could not find a mount target for selector: ${target}`);
|
|
14
|
+
}
|
|
15
|
+
return element;
|
|
16
|
+
}
|
|
17
|
+
return target;
|
|
18
|
+
}
|
|
19
|
+
// Create a tiny application shell around Qore's lower-level primitives.
|
|
20
|
+
export function createApp(setup) {
|
|
21
|
+
let dispose = null;
|
|
22
|
+
let mountedRoot = null;
|
|
23
|
+
let cleanupHandlers = [];
|
|
24
|
+
const app = {
|
|
25
|
+
// Mount the app, provide framework primitives to setup, and render the resulting view.
|
|
26
|
+
mount(target, props = {}) {
|
|
27
|
+
const root = resolveTarget(target);
|
|
28
|
+
app.unmount();
|
|
29
|
+
cleanupHandlers = [];
|
|
30
|
+
mountedRoot = root;
|
|
31
|
+
// Expose the core runtime pieces so an app can stay entirely within Qore primitives.
|
|
32
|
+
const context = {
|
|
33
|
+
app,
|
|
34
|
+
root,
|
|
35
|
+
props,
|
|
36
|
+
signal,
|
|
37
|
+
computed,
|
|
38
|
+
effect,
|
|
39
|
+
batch,
|
|
40
|
+
untrack,
|
|
41
|
+
stream,
|
|
42
|
+
from,
|
|
43
|
+
mapStream,
|
|
44
|
+
scanStream,
|
|
45
|
+
response,
|
|
46
|
+
h,
|
|
47
|
+
text,
|
|
48
|
+
dynamic,
|
|
49
|
+
show,
|
|
50
|
+
list,
|
|
51
|
+
fragment,
|
|
52
|
+
renderResponse,
|
|
53
|
+
onCleanup(handler) {
|
|
54
|
+
if (typeof handler === 'function') {
|
|
55
|
+
cleanupHandlers.push(handler);
|
|
56
|
+
}
|
|
57
|
+
return handler;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Allow setup to return either a raw view or an object with lifecycle hooks.
|
|
61
|
+
const result = setup(context);
|
|
62
|
+
const lifecycleResult = result && typeof result === 'object'
|
|
63
|
+
? result
|
|
64
|
+
: null;
|
|
65
|
+
const view = lifecycleResult && 'view' in lifecycleResult
|
|
66
|
+
? lifecycleResult.view
|
|
67
|
+
: result;
|
|
68
|
+
const onMount = lifecycleResult?.onMount ?? null;
|
|
69
|
+
dispose = mount(root, view);
|
|
70
|
+
if (typeof onMount === 'function') {
|
|
71
|
+
onMount(root);
|
|
72
|
+
}
|
|
73
|
+
return root;
|
|
74
|
+
},
|
|
75
|
+
// Tear down the mounted tree and any user-registered cleanup handlers.
|
|
76
|
+
unmount() {
|
|
77
|
+
if (dispose) {
|
|
78
|
+
const stop = dispose;
|
|
79
|
+
dispose = null;
|
|
80
|
+
stop();
|
|
81
|
+
}
|
|
82
|
+
for (let index = cleanupHandlers.length - 1; index >= 0; index -= 1) {
|
|
83
|
+
try {
|
|
84
|
+
const cleanup = cleanupHandlers[index];
|
|
85
|
+
if (cleanup) {
|
|
86
|
+
cleanup();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignore user cleanup errors so the app can still unmount.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
cleanupHandlers = [];
|
|
94
|
+
mountedRoot = null;
|
|
95
|
+
return app;
|
|
96
|
+
},
|
|
97
|
+
get root() {
|
|
98
|
+
return mountedRoot;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return app;
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResponseState } from '../core/response.js';
|
|
2
|
+
import type { MountTarget, MountView, QoreDocumentFragment, QoreElement, QoreChild, QoreComponent, QoreText, ReactiveValue, ResponseViews } from './types.js';
|
|
3
|
+
export declare function fragment(...children: QoreChild[]): QoreDocumentFragment;
|
|
4
|
+
export declare function dynamic<T>(source: ReactiveValue<T>, render?: (value: T) => QoreChild): QoreDocumentFragment;
|
|
5
|
+
export declare function show<T>(source: ReactiveValue<T>, render?: (value: T) => QoreChild, fallback?: QoreChild | ((value: T) => QoreChild)): QoreDocumentFragment;
|
|
6
|
+
export declare function list<T>(source: ReactiveValue<Iterable<T> | ArrayLike<T> | null | undefined>, render: (item: T, index: number) => QoreChild, options?: {
|
|
7
|
+
fallback?: QoreChild | ((items: T[]) => QoreChild);
|
|
8
|
+
}): QoreDocumentFragment;
|
|
9
|
+
export declare function renderResponse<TChunk, TValue>(responseState: ResponseState<TChunk, TValue>, views?: ResponseViews<TChunk, TValue>): QoreDocumentFragment;
|
|
10
|
+
export declare function h(tag: string, props?: Record<string, unknown> | null, ...children: QoreChild[]): QoreElement;
|
|
11
|
+
export declare function h<TProps extends Record<string, unknown>>(tag: QoreComponent<TProps>, props?: TProps | null, ...children: QoreChild[]): QoreChild;
|
|
12
|
+
export declare function text(valueOrGetter: ReactiveValue<unknown>): QoreText;
|
|
13
|
+
export declare function mount(root: MountTarget, view: MountView): () => QoreElement;
|