@qorejs/qore 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +294 -5
- package/dist/src/core/owner.d.ts +14 -0
- package/dist/src/core/owner.js +66 -0
- package/dist/src/core/response-runtime.js +5 -5
- package/dist/src/core/response-state.d.ts +4 -0
- package/dist/src/core/response-state.js +67 -2
- package/dist/src/core/signal-context.js +19 -9
- package/dist/src/core/signal-nodes.d.ts +5 -0
- package/dist/src/core/signal-nodes.js +51 -21
- package/dist/src/core/signal-types.d.ts +2 -0
- package/dist/src/core/signal.d.ts +1 -0
- package/dist/src/core/signal.js +2 -0
- package/dist/src/core/stream-types.d.ts +12 -0
- package/dist/src/core/stream.d.ts +1 -1
- package/dist/src/core/stream.js +144 -0
- package/dist/src/dom/app.js +2 -3
- package/dist/src/dom/dom.d.ts +1 -0
- package/dist/src/dom/dom.js +96 -9
- package/dist/src/dom/scope.d.ts +3 -1
- package/dist/src/dom/scope.js +10 -3
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.js +7 -2
- package/dist/src/providers/anthropic.js +6 -4
- package/dist/src/providers/deepseek.d.ts +2 -0
- package/dist/src/providers/deepseek.js +90 -0
- package/dist/src/providers/line-adapter.d.ts +2 -0
- package/dist/src/providers/line-adapter.js +83 -0
- package/dist/src/providers/line-parser.d.ts +5 -0
- package/dist/src/providers/line-parser.js +103 -0
- package/dist/src/providers/metadata.d.ts +8 -0
- package/dist/src/providers/metadata.js +193 -0
- package/dist/src/providers/ollama.d.ts +2 -0
- package/dist/src/providers/ollama.js +82 -0
- package/dist/src/providers/openai.js +6 -4
- package/dist/src/providers/openrouter.d.ts +2 -0
- package/dist/src/providers/openrouter.js +90 -0
- package/dist/src/providers/sse-adapter.js +129 -44
- package/dist/src/providers/sse-parser.d.ts +1 -1
- package/dist/src/providers/sse-parser.js +74 -38
- package/dist/src/providers/sse.d.ts +7 -1
- package/dist/src/providers/sse.js +6 -0
- package/dist/src/providers/types.d.ts +145 -0
- package/dist/src/server/sse-response.d.ts +16 -0
- package/dist/src/server/sse-response.js +102 -0
- package/dist/src/shared/utils.d.ts +1 -0
- package/dist/src/shared/utils.js +8 -1
- package/package.json +9 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Cleanup, EffectCallback, EffectOptions, ObserverDependency, ReactiveObserver, SignalListener, SubscribeOptions } from './signal-types.js';
|
|
2
2
|
export declare class SignalNode<T> implements ObserverDependency {
|
|
3
3
|
value: T;
|
|
4
|
+
level: number;
|
|
4
5
|
subscribers: Set<ReactiveObserver>;
|
|
5
6
|
listeners: Set<SignalListener<T>>;
|
|
6
7
|
constructor(initialValue: T);
|
|
@@ -13,6 +14,8 @@ export declare class SignalNode<T> implements ObserverDependency {
|
|
|
13
14
|
}
|
|
14
15
|
export declare class ComputedNode<T> implements ObserverDependency, ReactiveObserver {
|
|
15
16
|
getter: () => T;
|
|
17
|
+
private owner;
|
|
18
|
+
level: number;
|
|
16
19
|
subscribers: Set<ReactiveObserver>;
|
|
17
20
|
listeners: Set<SignalListener<T>>;
|
|
18
21
|
deps: Set<ObserverDependency>;
|
|
@@ -31,6 +34,8 @@ export declare class ComputedNode<T> implements ObserverDependency, ReactiveObse
|
|
|
31
34
|
export declare class EffectNode implements ReactiveObserver {
|
|
32
35
|
fn: EffectCallback;
|
|
33
36
|
scheduler: EffectOptions['scheduler'];
|
|
37
|
+
private owner;
|
|
38
|
+
level: number;
|
|
34
39
|
deps: Set<ObserverDependency>;
|
|
35
40
|
active: boolean;
|
|
36
41
|
scheduled: boolean;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { cleanupObserver, getActiveObserver, removePendingObserver, scheduleObserver, withActiveObserver } from './signal-context.js';
|
|
1
|
+
import { batch, cleanupObserver, getActiveObserver, removePendingObserver, scheduleObserver, withActiveObserver } from './signal-context.js';
|
|
2
|
+
import { createOwnedScope, disposeOwner, onCleanup, resetOwner, withOwner } from './owner.js';
|
|
2
3
|
import { scheduleEffectRun } from './signal-scheduler.js';
|
|
3
4
|
// A mutable signal node stores a value and fan-outs updates to listeners and observers.
|
|
4
5
|
export class SignalNode {
|
|
5
6
|
value;
|
|
7
|
+
level = 0;
|
|
6
8
|
subscribers = new Set();
|
|
7
9
|
listeners = new Set();
|
|
8
10
|
constructor(initialValue) {
|
|
@@ -45,14 +47,18 @@ export class SignalNode {
|
|
|
45
47
|
for (const listener of Array.from(this.listeners)) {
|
|
46
48
|
listener(this.value);
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
batch(() => {
|
|
51
|
+
for (const observer of Array.from(this.subscribers)) {
|
|
52
|
+
scheduleObserver(observer);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
// A computed node re-runs its getter whenever one of its dependencies changes.
|
|
54
58
|
export class ComputedNode {
|
|
55
59
|
getter;
|
|
60
|
+
owner = createOwnedScope();
|
|
61
|
+
level = 1;
|
|
56
62
|
subscribers = new Set();
|
|
57
63
|
listeners = new Set();
|
|
58
64
|
deps = new Set();
|
|
@@ -61,6 +67,7 @@ export class ComputedNode {
|
|
|
61
67
|
value;
|
|
62
68
|
constructor(getter) {
|
|
63
69
|
this.getter = getter;
|
|
70
|
+
onCleanup(() => this.stop());
|
|
64
71
|
this.recompute();
|
|
65
72
|
}
|
|
66
73
|
get() {
|
|
@@ -96,11 +103,16 @@ export class ComputedNode {
|
|
|
96
103
|
return;
|
|
97
104
|
}
|
|
98
105
|
const previousValue = this.value;
|
|
106
|
+
resetOwner(this.owner);
|
|
99
107
|
cleanupObserver(this);
|
|
100
|
-
withActiveObserver(this, () => {
|
|
108
|
+
withOwner(this.owner, () => withActiveObserver(this, () => {
|
|
101
109
|
const nextValue = this.getter();
|
|
102
110
|
const changed = !this.initialized || !Object.is(previousValue, nextValue);
|
|
111
|
+
const nextLevel = this.deps.size > 0
|
|
112
|
+
? Math.max(...Array.from(this.deps, (dependency) => dependency.level)) + 1
|
|
113
|
+
: 1;
|
|
103
114
|
this.value = nextValue;
|
|
115
|
+
this.level = nextLevel;
|
|
104
116
|
this.initialized = true;
|
|
105
117
|
if (!changed) {
|
|
106
118
|
return;
|
|
@@ -108,10 +120,12 @@ export class ComputedNode {
|
|
|
108
120
|
for (const listener of Array.from(this.listeners)) {
|
|
109
121
|
listener(this.value);
|
|
110
122
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
batch(() => {
|
|
124
|
+
for (const observer of Array.from(this.subscribers)) {
|
|
125
|
+
scheduleObserver(observer);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}));
|
|
115
129
|
}
|
|
116
130
|
stop() {
|
|
117
131
|
if (!this.active) {
|
|
@@ -120,6 +134,7 @@ export class ComputedNode {
|
|
|
120
134
|
this.active = false;
|
|
121
135
|
cleanupObserver(this);
|
|
122
136
|
removePendingObserver(this);
|
|
137
|
+
disposeOwner(this.owner);
|
|
123
138
|
this.subscribers.clear();
|
|
124
139
|
this.listeners.clear();
|
|
125
140
|
}
|
|
@@ -128,6 +143,8 @@ export class ComputedNode {
|
|
|
128
143
|
export class EffectNode {
|
|
129
144
|
fn;
|
|
130
145
|
scheduler;
|
|
146
|
+
owner = createOwnedScope();
|
|
147
|
+
level = 1;
|
|
131
148
|
deps = new Set();
|
|
132
149
|
active = true;
|
|
133
150
|
scheduled = false;
|
|
@@ -137,6 +154,7 @@ export class EffectNode {
|
|
|
137
154
|
constructor(fn, options = {}) {
|
|
138
155
|
this.fn = fn;
|
|
139
156
|
this.scheduler = options.scheduler ?? 'sync';
|
|
157
|
+
onCleanup(() => this.stop());
|
|
140
158
|
this.run();
|
|
141
159
|
}
|
|
142
160
|
schedule() {
|
|
@@ -166,18 +184,29 @@ export class EffectNode {
|
|
|
166
184
|
}
|
|
167
185
|
this.running = true;
|
|
168
186
|
this.needsRun = false;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
const previousCleanup = this.cleanup;
|
|
188
|
+
this.cleanup = null;
|
|
189
|
+
try {
|
|
190
|
+
// Run the previous cleanup before dropping old dependencies so a thrown cleanup
|
|
191
|
+
// does not silently unsubscribe the effect from future source updates.
|
|
192
|
+
if (typeof previousCleanup === 'function') {
|
|
193
|
+
previousCleanup();
|
|
194
|
+
}
|
|
195
|
+
resetOwner(this.owner);
|
|
196
|
+
cleanupObserver(this);
|
|
197
|
+
withOwner(this.owner, () => withActiveObserver(this, () => {
|
|
198
|
+
const maybeCleanup = this.fn();
|
|
199
|
+
this.cleanup = typeof maybeCleanup === 'function' ? maybeCleanup : null;
|
|
200
|
+
}));
|
|
201
|
+
this.level = this.deps.size > 0
|
|
202
|
+
? Math.max(...Array.from(this.deps, (dependency) => dependency.level)) + 1
|
|
203
|
+
: 1;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
this.running = false;
|
|
207
|
+
if (this.needsRun && this.active) {
|
|
208
|
+
this.schedule();
|
|
209
|
+
}
|
|
181
210
|
}
|
|
182
211
|
}
|
|
183
212
|
stop() {
|
|
@@ -189,6 +218,7 @@ export class EffectNode {
|
|
|
189
218
|
this.needsRun = false;
|
|
190
219
|
cleanupObserver(this);
|
|
191
220
|
removePendingObserver(this);
|
|
221
|
+
disposeOwner(this.owner);
|
|
192
222
|
if (typeof this.cleanup === 'function') {
|
|
193
223
|
this.cleanup();
|
|
194
224
|
this.cleanup = null;
|
|
@@ -9,11 +9,13 @@ export interface EffectOptions {
|
|
|
9
9
|
scheduler?: EffectScheduler;
|
|
10
10
|
}
|
|
11
11
|
export interface ObserverDependency {
|
|
12
|
+
level: number;
|
|
12
13
|
subscribers: Set<ReactiveObserver>;
|
|
13
14
|
}
|
|
14
15
|
export interface ReactiveObserver {
|
|
15
16
|
deps: Set<ObserverDependency>;
|
|
16
17
|
active: boolean;
|
|
18
|
+
level: number;
|
|
17
19
|
schedule(): void;
|
|
18
20
|
notify(): void;
|
|
19
21
|
}
|
|
@@ -16,4 +16,5 @@ export declare function signal<T>(initialValue: T): Signal<T>;
|
|
|
16
16
|
export declare function computed<T>(getter: () => T): ComputedSignal<T>;
|
|
17
17
|
export declare function effect(fn: EffectCallback, options?: EffectOptions): Cleanup;
|
|
18
18
|
export { batch, untrack } from './signal-context.js';
|
|
19
|
+
export { createRoot, onCleanup } from './owner.js';
|
|
19
20
|
export declare function isSignal<T = unknown>(value: unknown): value is ReadonlySignal<T>;
|
package/dist/src/core/signal.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRoot, onCleanup } from './owner.js';
|
|
1
2
|
import { batch, READ, untrack } from './signal-context.js';
|
|
2
3
|
import { ComputedNode, EffectNode, SignalNode } from './signal-nodes.js';
|
|
3
4
|
// Create a mutable signal function with helper methods attached to it.
|
|
@@ -35,6 +36,7 @@ export function effect(fn, options = {}) {
|
|
|
35
36
|
return () => node.stop();
|
|
36
37
|
}
|
|
37
38
|
export { batch, untrack } from './signal-context.js';
|
|
39
|
+
export { createRoot, onCleanup } from './owner.js';
|
|
38
40
|
// Detect Qore signal-like values by their callable shape plus peek helper.
|
|
39
41
|
export function isSignal(value) {
|
|
40
42
|
return typeof value === 'function' && typeof value.peek === 'function';
|
|
@@ -6,6 +6,7 @@ export interface BackpressureOptions {
|
|
|
6
6
|
buffer?: number;
|
|
7
7
|
overflow?: OverflowStrategy;
|
|
8
8
|
}
|
|
9
|
+
export type RetryBackoff = number | number[] | 'exponential' | ((retry: number, error: unknown) => MaybePromise<number>);
|
|
9
10
|
export interface NormalizedBackpressure {
|
|
10
11
|
interval: number;
|
|
11
12
|
buffer: number;
|
|
@@ -21,6 +22,11 @@ export interface StreamOptions<TChunk = unknown, TValue = string> {
|
|
|
21
22
|
reduce?: (currentValue: TValue, chunk: TChunk, index: number) => TValue;
|
|
22
23
|
backpressure?: number | BackpressureOptions | null;
|
|
23
24
|
}
|
|
25
|
+
export interface RetryableStreamOptions<TChunk = unknown, TValue = string> extends StreamOptions<TChunk, TValue> {
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
backoff?: RetryBackoff;
|
|
28
|
+
}
|
|
29
|
+
export type StreamPipeStage<TChunk = unknown, TValue = string> = (value: TValue, index: number) => MaybePromise<StreamInput<TChunk, TValue>>;
|
|
24
30
|
export interface StreamController<TChunk = unknown, TValue = string> {
|
|
25
31
|
readonly signal: GlobalAbortSignal;
|
|
26
32
|
push(chunk: TChunk): Promise<TValue>;
|
|
@@ -57,5 +63,11 @@ export interface StreamFactory {
|
|
|
57
63
|
latest<TChunk>(sourceOrSetup: StreamInput<TChunk, TChunk | null>, options?: StreamOptions<TChunk, TChunk | null>): QoreStream<TChunk, TChunk | null>;
|
|
58
64
|
withBackpressure<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, backpressure: number | BackpressureOptions, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
59
65
|
paced<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, interval: number, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
66
|
+
merge<TChunk = unknown, TValue = string>(sources: Array<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
67
|
+
concat<TChunk = unknown, TValue = string>(sources: Array<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
68
|
+
pipe<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, stages: Array<StreamPipeStage<TChunk, TValue>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
69
|
+
race<TChunk = unknown, TValue = string>(sources: Array<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
70
|
+
retryable<TChunk = unknown, TValue = string>(sourceFactory: (attempt: number) => StreamInput<TChunk, TValue>, options?: RetryableStreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
71
|
+
switchMap<TInput, TChunk = unknown, TValue = string>(source: SourceLike<TInput>, mapper: (value: TInput, index: number) => MaybePromise<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
|
|
60
72
|
}
|
|
61
73
|
export type StreamResponseState<TChunk, TValue> = ResponseState<TChunk, TValue>;
|
|
@@ -7,5 +7,5 @@ export declare function from<TChunk, TValue = string>(source: SourceLike<TChunk>
|
|
|
7
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
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
9
|
export { createStream } from './stream-runtime.js';
|
|
10
|
-
export type { BackpressureOptions, QoreStream, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './stream-types.js';
|
|
10
|
+
export type { BackpressureOptions, QoreStream, RetryBackoff, RetryableStreamOptions, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './stream-types.js';
|
|
11
11
|
export { toAsyncIterable } from './iterable.js';
|
package/dist/src/core/stream.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toAsyncIterable } from './iterable.js';
|
|
2
2
|
import { createStream } from './stream-runtime.js';
|
|
3
|
+
import { startSource } from './stream-source.js';
|
|
3
4
|
import { reduceText } from './stream-state.js';
|
|
4
5
|
import { sleep } from '../shared/utils.js';
|
|
5
6
|
const streamFactory = ((sourceOrSetup, options = {}) => createStream(sourceOrSetup, {
|
|
@@ -36,8 +37,151 @@ streamFactory.withBackpressure = (sourceOrSetup, backpressure, options = {}) =>
|
|
|
36
37
|
});
|
|
37
38
|
// Shorthand for the first backpressure primitive: minimum interval between chunks.
|
|
38
39
|
streamFactory.paced = (sourceOrSetup, interval, options = {}) => streamFactory.withBackpressure(sourceOrSetup, { interval }, options);
|
|
40
|
+
streamFactory.merge = (sources, options = {}) => createStream(async (controller) => {
|
|
41
|
+
await Promise.all(sources.map(async (source) => {
|
|
42
|
+
await startSource(source, controller);
|
|
43
|
+
}));
|
|
44
|
+
}, options);
|
|
45
|
+
streamFactory.concat = (sources, options = {}) => createStream(async (controller) => {
|
|
46
|
+
for (const source of sources) {
|
|
47
|
+
if (controller.signal.aborted) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
await startSource(source, controller);
|
|
51
|
+
}
|
|
52
|
+
}, options);
|
|
53
|
+
streamFactory.pipe = (sourceOrSetup, stages, options = {}) => createStream(async (controller) => {
|
|
54
|
+
const consumeSource = async (currentSource) => {
|
|
55
|
+
const currentStream = createStream(currentSource, options);
|
|
56
|
+
for await (const chunk of currentStream) {
|
|
57
|
+
if (controller.signal.aborted) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
await controller.push(chunk);
|
|
61
|
+
}
|
|
62
|
+
return currentStream.ready;
|
|
63
|
+
};
|
|
64
|
+
let currentValue = await consumeSource(sourceOrSetup);
|
|
65
|
+
for (const [index, stage] of stages.entries()) {
|
|
66
|
+
if (controller.signal.aborted) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const nextSource = await stage(currentValue, index);
|
|
70
|
+
currentValue = await consumeSource(nextSource);
|
|
71
|
+
}
|
|
72
|
+
}, options);
|
|
73
|
+
streamFactory.race = (sources, options = {}) => createStream(async (controller) => {
|
|
74
|
+
const activeIterators = sources.map((source, index) => ({
|
|
75
|
+
index,
|
|
76
|
+
iterator: toAsyncIterable(source)[Symbol.asyncIterator]()
|
|
77
|
+
}));
|
|
78
|
+
let winner = null;
|
|
79
|
+
try {
|
|
80
|
+
while (!controller.signal.aborted && winner === null && activeIterators.length > 0) {
|
|
81
|
+
const nextResult = await Promise.race(activeIterators.map(async ({ index, iterator }) => ({
|
|
82
|
+
index,
|
|
83
|
+
result: await iterator.next()
|
|
84
|
+
})));
|
|
85
|
+
if (nextResult.result.done) {
|
|
86
|
+
const exhaustedIndex = activeIterators.findIndex(({ index }) => index === nextResult.index);
|
|
87
|
+
if (exhaustedIndex >= 0) {
|
|
88
|
+
activeIterators.splice(exhaustedIndex, 1);
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
winner = activeIterators.find(({ index }) => index === nextResult.index)?.iterator ?? null;
|
|
93
|
+
await controller.push(nextResult.result.value);
|
|
94
|
+
}
|
|
95
|
+
if (!winner || controller.signal.aborted) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const candidate of activeIterators) {
|
|
99
|
+
if (candidate.iterator !== winner) {
|
|
100
|
+
await candidate.iterator.return?.();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
while (!controller.signal.aborted) {
|
|
104
|
+
const nextChunk = await winner.next();
|
|
105
|
+
if (nextChunk.done) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
await controller.push(nextChunk.value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
for (const { iterator } of activeIterators) {
|
|
113
|
+
if (iterator !== winner) {
|
|
114
|
+
await iterator.return?.();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}, options);
|
|
119
|
+
streamFactory.retryable = (sourceFactory, options = {}) => {
|
|
120
|
+
const { maxRetries = 0, backoff = 'exponential', ...streamOptions } = options;
|
|
121
|
+
return createStream(async (controller) => {
|
|
122
|
+
let retries = 0;
|
|
123
|
+
while (!controller.signal.aborted) {
|
|
124
|
+
try {
|
|
125
|
+
await startSource(sourceFactory(retries), controller);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (retries >= maxRetries || controller.signal.aborted) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
retries += 1;
|
|
133
|
+
const delay = await resolveRetryDelay(backoff, retries, error);
|
|
134
|
+
if (delay > 0) {
|
|
135
|
+
await sleep(delay, controller.signal);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}, streamOptions);
|
|
140
|
+
};
|
|
141
|
+
streamFactory.switchMap = (source, mapper, options = {}) => createStream(async (controller) => {
|
|
142
|
+
let index = 0;
|
|
143
|
+
let activeToken = 0;
|
|
144
|
+
let activeTask = null;
|
|
145
|
+
const startInner = (token, innerSource) => (async () => {
|
|
146
|
+
for await (const chunk of toAsyncIterable(innerSource)) {
|
|
147
|
+
if (controller.signal.aborted || token !== activeToken) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
await controller.push(chunk);
|
|
151
|
+
}
|
|
152
|
+
})().catch((error) => {
|
|
153
|
+
if (token !== activeToken || controller.signal.aborted) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
});
|
|
158
|
+
for await (const value of toAsyncIterable(source)) {
|
|
159
|
+
if (controller.signal.aborted) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
activeToken += 1;
|
|
163
|
+
const token = activeToken;
|
|
164
|
+
const innerSource = await mapper(value, index);
|
|
165
|
+
activeTask = startInner(token, innerSource);
|
|
166
|
+
index += 1;
|
|
167
|
+
}
|
|
168
|
+
await activeTask;
|
|
169
|
+
}, options);
|
|
39
170
|
// Create a text-accumulating stream by default.
|
|
40
171
|
export const stream = streamFactory;
|
|
172
|
+
async function resolveRetryDelay(backoff, retry, error) {
|
|
173
|
+
if (typeof backoff === 'function') {
|
|
174
|
+
return Math.max(0, await backoff(retry, error));
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(backoff)) {
|
|
177
|
+
const nextDelay = backoff[Math.min(retry - 1, backoff.length - 1)] ?? 0;
|
|
178
|
+
return Math.max(0, nextDelay);
|
|
179
|
+
}
|
|
180
|
+
if (backoff === 'exponential') {
|
|
181
|
+
return 250 * 2 ** (retry - 1);
|
|
182
|
+
}
|
|
183
|
+
return Math.max(0, backoff);
|
|
184
|
+
}
|
|
41
185
|
// Turn any iterable or async iterable into a stream, optionally adding source delay.
|
|
42
186
|
export function from(source, options = {}) {
|
|
43
187
|
const { delay = 0, ...streamOptions } = options;
|
package/dist/src/dom/app.js
CHANGED
|
@@ -2,11 +2,10 @@ import { dynamic, fragment, h, list, mount, renderResponse, show, text } from '.
|
|
|
2
2
|
import { batch, computed, effect, signal, untrack } from '../core/signal.js';
|
|
3
3
|
import { response } from '../core/response.js';
|
|
4
4
|
import { from, mapStream, scanStream, stream } from '../core/stream.js';
|
|
5
|
+
import { assertDocument } from './scope.js';
|
|
5
6
|
// Resolve a CSS selector or direct node into the root mount target.
|
|
6
7
|
function resolveTarget(target) {
|
|
7
|
-
|
|
8
|
-
throw new Error('Qore app mounting requires a browser-like environment');
|
|
9
|
-
}
|
|
8
|
+
assertDocument('createApp(...).mount(...)');
|
|
10
9
|
if (typeof target === 'string') {
|
|
11
10
|
const element = document.querySelector(target);
|
|
12
11
|
if (!element) {
|
package/dist/src/dom/dom.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare function dynamic<T>(source: ReactiveValue<T>, render?: (value: T)
|
|
|
5
5
|
export declare function show<T>(source: ReactiveValue<T>, render?: (value: T) => QoreChild, fallback?: QoreChild | ((value: T) => QoreChild)): QoreDocumentFragment;
|
|
6
6
|
export declare function list<T>(source: ReactiveValue<Iterable<T> | ArrayLike<T> | null | undefined>, render: (item: T, index: number) => QoreChild, options?: {
|
|
7
7
|
fallback?: QoreChild | ((items: T[]) => QoreChild);
|
|
8
|
+
key?: (item: T, index: number) => unknown;
|
|
8
9
|
}): QoreDocumentFragment;
|
|
9
10
|
export declare function renderResponse<TChunk, TValue>(responseState: ResponseState<TChunk, TValue>, views?: ResponseViews<TChunk, TValue>): QoreDocumentFragment;
|
|
10
11
|
export declare function h(tag: string, props?: Record<string, unknown> | null, ...children: QoreChild[]): QoreElement;
|
package/dist/src/dom/dom.js
CHANGED
|
@@ -63,6 +63,27 @@ function replaceRange(start, end, nextValue) {
|
|
|
63
63
|
parent.insertBefore(node, end);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
function insertBefore(parent, reference, value) {
|
|
67
|
+
for (const node of materialize(value)) {
|
|
68
|
+
parent.insertBefore(node, reference);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function destroyKeyedEntry(entry) {
|
|
72
|
+
disposeScope(entry.scope);
|
|
73
|
+
clearRange(entry.start, entry.end);
|
|
74
|
+
entry.start.remove();
|
|
75
|
+
entry.end.remove();
|
|
76
|
+
}
|
|
77
|
+
function renderKeyedEntry(parent, reference, item, index, render) {
|
|
78
|
+
const start = document.createComment('qore-list-item-start');
|
|
79
|
+
const end = document.createComment('qore-list-item-end');
|
|
80
|
+
const scope = createScope();
|
|
81
|
+
parent.insertBefore(start, reference);
|
|
82
|
+
const content = withScope(scope, () => render(item, index));
|
|
83
|
+
insertBefore(parent, reference, content);
|
|
84
|
+
parent.insertBefore(end, reference);
|
|
85
|
+
return { key: null, value: item, index, start, end, scope };
|
|
86
|
+
}
|
|
66
87
|
// Allow mount targets to be passed as selectors or direct nodes.
|
|
67
88
|
function resolveRoot(root) {
|
|
68
89
|
if (typeof root === 'string') {
|
|
@@ -76,7 +97,7 @@ function resolveRoot(root) {
|
|
|
76
97
|
}
|
|
77
98
|
// Build a fragment from a variadic list of children.
|
|
78
99
|
export function fragment(...children) {
|
|
79
|
-
assertDocument();
|
|
100
|
+
assertDocument('fragment()');
|
|
80
101
|
const node = document.createDocumentFragment();
|
|
81
102
|
for (const child of children) {
|
|
82
103
|
appendChild(node, child);
|
|
@@ -85,7 +106,7 @@ export function fragment(...children) {
|
|
|
85
106
|
}
|
|
86
107
|
// Render a live region between comment markers and refresh it when the source changes.
|
|
87
108
|
export function dynamic(source, render = (value) => value) {
|
|
88
|
-
assertDocument();
|
|
109
|
+
assertDocument('dynamic()');
|
|
89
110
|
const start = document.createComment('qore-dynamic-start');
|
|
90
111
|
const end = document.createComment('qore-dynamic-end');
|
|
91
112
|
const node = document.createDocumentFragment();
|
|
@@ -115,18 +136,84 @@ export function show(source, render, fallback = null) {
|
|
|
115
136
|
}
|
|
116
137
|
// Render a list reactively, or a fallback when the collection is empty.
|
|
117
138
|
export function list(source, render, options = {}) {
|
|
118
|
-
const { fallback = null } = options;
|
|
119
|
-
|
|
139
|
+
const { fallback = null, key } = options;
|
|
140
|
+
if (!key) {
|
|
141
|
+
return dynamic(source, (value) => {
|
|
142
|
+
const items = value == null
|
|
143
|
+
? []
|
|
144
|
+
: Array.isArray(value)
|
|
145
|
+
? value
|
|
146
|
+
: Array.from(value);
|
|
147
|
+
if (items.length === 0) {
|
|
148
|
+
return resolveTemplate(fallback, items);
|
|
149
|
+
}
|
|
150
|
+
return items.map((item, index) => render(item, index));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
assertDocument('list()');
|
|
154
|
+
const start = document.createComment('qore-list-start');
|
|
155
|
+
const end = document.createComment('qore-list-end');
|
|
156
|
+
const node = document.createDocumentFragment();
|
|
157
|
+
node.append(start, end);
|
|
158
|
+
let entries = [];
|
|
159
|
+
let fallbackScope = null;
|
|
160
|
+
const stop = effect(() => {
|
|
161
|
+
const value = resolveAccessor(source);
|
|
120
162
|
const items = value == null
|
|
121
163
|
? []
|
|
122
164
|
: Array.isArray(value)
|
|
123
165
|
? value
|
|
124
166
|
: Array.from(value);
|
|
167
|
+
const parent = end.parentNode;
|
|
168
|
+
if (!parent) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
125
171
|
if (items.length === 0) {
|
|
126
|
-
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
destroyKeyedEntry(entry);
|
|
174
|
+
}
|
|
175
|
+
entries = [];
|
|
176
|
+
disposeScope(fallbackScope);
|
|
177
|
+
fallbackScope = createScope();
|
|
178
|
+
const renderedFallback = withScope(fallbackScope, () => resolveTemplate(fallback, items));
|
|
179
|
+
replaceRange(start, end, renderedFallback);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
disposeScope(fallbackScope);
|
|
183
|
+
fallbackScope = null;
|
|
184
|
+
const nextKeys = items.map((item, index) => key(item, index));
|
|
185
|
+
const canAppend = entries.length <= items.length
|
|
186
|
+
&& entries.every((entry, index) => Object.is(entry.key, nextKeys[index]));
|
|
187
|
+
if (!canAppend) {
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
destroyKeyedEntry(entry);
|
|
190
|
+
}
|
|
191
|
+
clearRange(start, end);
|
|
192
|
+
entries = [];
|
|
193
|
+
}
|
|
194
|
+
if (!canAppend) {
|
|
195
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
196
|
+
const entry = renderKeyedEntry(parent, end, items[index], index, render);
|
|
197
|
+
entry.key = nextKeys[index];
|
|
198
|
+
entries.push(entry);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (let index = entries.length; index < items.length; index += 1) {
|
|
203
|
+
const entry = renderKeyedEntry(parent, end, items[index], index, render);
|
|
204
|
+
entry.key = nextKeys[index];
|
|
205
|
+
entries.push(entry);
|
|
127
206
|
}
|
|
128
|
-
return items.map((item, index) => render(item, index));
|
|
129
207
|
});
|
|
208
|
+
registerCleanup(() => {
|
|
209
|
+
stop();
|
|
210
|
+
disposeScope(fallbackScope);
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
destroyKeyedEntry(entry);
|
|
213
|
+
}
|
|
214
|
+
entries = [];
|
|
215
|
+
});
|
|
216
|
+
return node;
|
|
130
217
|
}
|
|
131
218
|
// Render response state through status-aware template overrides.
|
|
132
219
|
export function renderResponse(responseState, views = {}) {
|
|
@@ -142,7 +229,7 @@ export function renderResponse(responseState, views = {}) {
|
|
|
142
229
|
});
|
|
143
230
|
}
|
|
144
231
|
export function h(tag, props = null, ...children) {
|
|
145
|
-
assertDocument();
|
|
232
|
+
assertDocument('h()');
|
|
146
233
|
if (typeof tag === 'function') {
|
|
147
234
|
return tag({
|
|
148
235
|
...(props ?? {}),
|
|
@@ -162,7 +249,7 @@ export function h(tag, props = null, ...children) {
|
|
|
162
249
|
}
|
|
163
250
|
// Create a text node and keep it in sync with a reactive getter when necessary.
|
|
164
251
|
export function text(valueOrGetter) {
|
|
165
|
-
assertDocument();
|
|
252
|
+
assertDocument('text()');
|
|
166
253
|
const node = document.createTextNode('');
|
|
167
254
|
if (isReactiveValue(valueOrGetter)) {
|
|
168
255
|
const stop = effect(() => {
|
|
@@ -177,7 +264,7 @@ export function text(valueOrGetter) {
|
|
|
177
264
|
}
|
|
178
265
|
// Mount a view into a root element and return a disposer for its reactive scope.
|
|
179
266
|
export function mount(root, view) {
|
|
180
|
-
assertDocument();
|
|
267
|
+
assertDocument('mount()');
|
|
181
268
|
const target = resolveRoot(root);
|
|
182
269
|
target[ROOT_CLEANUP]?.();
|
|
183
270
|
const scope = createScope();
|
package/dist/src/dom/scope.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ export declare const ROOT_CLEANUP: unique symbol;
|
|
|
3
3
|
export interface Scope {
|
|
4
4
|
cleanups: Cleanup[];
|
|
5
5
|
}
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function canUseDOM(): boolean;
|
|
7
|
+
export declare function assertCanUseDOM(apiName?: string): void;
|
|
8
|
+
export declare function assertDocument(apiName?: string): void;
|
|
7
9
|
export declare function createScope(): Scope;
|
|
8
10
|
export declare function withScope<T>(scope: Scope, fn: () => T): T;
|
|
9
11
|
export declare function registerCleanup<T extends Cleanup | null | undefined>(cleanup: T): T;
|
package/dist/src/dom/scope.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// Store mount cleanup directly on the root node so remounts can tear down old scopes.
|
|
2
2
|
export const ROOT_CLEANUP = Symbol('qore.dom.cleanup');
|
|
3
3
|
let activeScope = null;
|
|
4
|
+
// Detect whether the current runtime can create and mutate real DOM nodes.
|
|
5
|
+
export function canUseDOM() {
|
|
6
|
+
return typeof document !== 'undefined';
|
|
7
|
+
}
|
|
4
8
|
// Guard DOM helpers so they only run in browser-like environments.
|
|
5
|
-
export function
|
|
6
|
-
if (
|
|
7
|
-
throw new Error(
|
|
9
|
+
export function assertCanUseDOM(apiName = 'Qore DOM APIs') {
|
|
10
|
+
if (!canUseDOM()) {
|
|
11
|
+
throw new Error(`${apiName} requires a browser-like environment`);
|
|
8
12
|
}
|
|
9
13
|
}
|
|
14
|
+
export function assertDocument(apiName = 'Qore DOM APIs') {
|
|
15
|
+
assertCanUseDOM(apiName);
|
|
16
|
+
}
|
|
10
17
|
// A scope collects effect disposers created while rendering a subtree.
|
|
11
18
|
export function createScope() {
|
|
12
19
|
return { cleanups: [] };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
export { signal, computed, effect, batch, untrack, isSignal } from './core/signal.js';
|
|
1
|
+
export { signal, computed, createRoot, effect, onCleanup, batch, untrack, isSignal } from './core/signal.js';
|
|
2
2
|
export type { ComputedSignal, ReadonlySignal, Signal } from './core/signal.js';
|
|
3
3
|
export type { EffectOptions, EffectScheduler, SubscribeOptions } from './core/signal-types.js';
|
|
4
4
|
export { stream, createStream, from, mapStream, scanStream, toAsyncIterable } from './core/stream.js';
|
|
5
|
-
export type { BackpressureOptions, QoreStream, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './core/stream.js';
|
|
5
|
+
export type { BackpressureOptions, QoreStream, RetryBackoff, RetryableStreamOptions, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './core/stream.js';
|
|
6
6
|
export { createResponse, response } from './core/response.js';
|
|
7
7
|
export type { CreateResponseOptions, GlobalAbortSignal, MaybePromise, ResponseConsumeContext, ResponseExecutorContext, ResponseFactory, ResponseReactiveState, ResponseRunOptions, ResponseSnapshot, ResponseSource, ResponseSourceFactory, ResponseState, ResponseStatus, SourceLike } from './core/response.js';
|
|
8
8
|
export { createApp } from './dom/app.js';
|
|
9
9
|
export type { AppContext, AppSetupResult, QoreApp } from './dom/app.js';
|
|
10
10
|
export { dynamic, fragment, h, list, mount, renderResponse, show, text } from './dom/dom.js';
|
|
11
|
+
export { assertCanUseDOM, canUseDOM } from './dom/scope.js';
|
|
12
|
+
export { createSSEResponse } from './server/sse-response.js';
|
|
13
|
+
export type { CreateSSEResponseOptions, SSEFrame } from './server/sse-response.js';
|
|
11
14
|
export type { GlobalDocumentFragment, GlobalElement, GlobalNode, GlobalText, MountTarget, MountView, QoreChild, QoreComponent, QoreDocumentFragment, QoreElement, QoreNode, QoreTemplate, QoreText, ReactiveValue, ResponseRenderState, ResponseViews } from './dom/types.js';
|
|
12
15
|
export { createAnthropic } from './providers/anthropic.js';
|
|
16
|
+
export { createDeepSeek } from './providers/deepseek.js';
|
|
17
|
+
export { createOllama } from './providers/ollama.js';
|
|
13
18
|
export { createOpenAI } from './providers/openai.js';
|
|
14
|
-
export {
|
|
15
|
-
export
|
|
19
|
+
export { createOpenRouter } from './providers/openrouter.js';
|
|
20
|
+
export { collectProviderMetadata, createLineAdapter, createSSEAdapter, extractAnthropicMetadata, extractDeepSeekMetadata, extractOllamaMetadata, extractOpenAIMetadata, extractOpenRouterMetadata, mergeProviderMetadata } from './providers/sse.js';
|
|
21
|
+
export type { AnthropicAdapter, AnthropicChatInput, AnthropicEvent, AnthropicMessage, AnthropicOptions, AnthropicRequest, DeepSeekAdapter, DeepSeekChatInput, DeepSeekEvent, DeepSeekMessage, DeepSeekOptions, DeepSeekRequest, FetchLike, LineAdapter, LineAdapterOptions, LineEvent, LineRequestConfig, OpenAIAdapter, OpenAIChatInput, OpenAIEvent, OpenAIMessage, OpenAIOptions, OpenAIRequest, OllamaAdapter, OllamaChatInput, OllamaEvent, OllamaMessage, OllamaOptions, OllamaRequest, OpenRouterAdapter, OpenRouterChatInput, OpenRouterEvent, OpenRouterMessage, OpenRouterOptions, OpenRouterRequest, ProviderHeaders, ProviderMetadataUpdate, ProviderRetryBackoff, ProviderRetryOptions, ProviderRequestOptions, ProviderStreamMetadata, ProviderUsage, SSEAdapter, SSEAdapterOptions, SSEEvent, SSERequestConfig } from './providers/sse.js';
|
|
16
22
|
export { normalizeError, sleep } from './shared/utils.js';
|