@qorejs/qore 0.7.2 → 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
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Qore
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@qorejs/qore)
|
|
4
|
+
[](https://github.com/qorejs/qore/releases/latest)
|
|
4
5
|
[](https://github.com/qorejs/qore/actions/workflows/ci.yml)
|
|
5
6
|
[](#browser-regression)
|
|
6
7
|
[](https://github.com/qorejs/qore/actions/workflows/release-check.yml)
|
|
@@ -16,6 +17,7 @@ Instead of treating data as a snapshot, Qore treats it like a river. Tokens arri
|
|
|
16
17
|
Quick links:
|
|
17
18
|
|
|
18
19
|
- [npm package](https://www.npmjs.com/package/@qorejs/qore)
|
|
20
|
+
- [latest release](https://github.com/qorejs/qore/releases/latest)
|
|
19
21
|
- [GitHub Packages](https://github.com/qorejs/qore/packages)
|
|
20
22
|
- [landing page source](https://github.com/qorejs/qore/blob/main/index.html)
|
|
21
23
|
- [streaming demo source](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
|
|
@@ -73,6 +75,17 @@ Here, `answer` is all of the following at once:
|
|
|
73
75
|
- An `AsyncIterable`, so you can still use `for await...of`
|
|
74
76
|
- A lifecycle-aware streaming state, with `status()`, `streaming()`, `error()`, and `chunks()`
|
|
75
77
|
|
|
78
|
+
## Performance Model
|
|
79
|
+
|
|
80
|
+
Qore keeps the streaming hot path narrow:
|
|
81
|
+
|
|
82
|
+
- chunk commits append into an internal log instead of cloning the full history on every token
|
|
83
|
+
- public `chunks()` reads still return defensive copies, so consumers cannot corrupt runtime state
|
|
84
|
+
- `chunkCount()` tracks the internal log version directly, so status UIs can stay cheap during long generations
|
|
85
|
+
- DOM bindings update only the nodes that read the stream signal
|
|
86
|
+
|
|
87
|
+
That means a long AI answer can keep flowing through one signal and one text node without turning every token into a full transcript rewrite.
|
|
88
|
+
|
|
76
89
|
## Why Qore
|
|
77
90
|
|
|
78
91
|
- React treats streaming as a special case that needs extra machinery
|
|
@@ -282,6 +295,12 @@ CI also uploads the browser regression evidence as workflow artifacts. The bundl
|
|
|
282
295
|
|
|
283
296
|
If a locked-down local shell cannot launch a supported headless browser, the script will defer to CI unless you force a hard local failure with `QORE_BROWSER_SMOKE_REQUIRED=1`.
|
|
284
297
|
|
|
298
|
+
Local preview ports can also be pinned when another process is already using the default range:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
QORE_STATIC_PORT=4300 QORE_STATIC_PORT_END=4400 npm run test:browser
|
|
302
|
+
```
|
|
303
|
+
|
|
285
304
|
## Benchmark Methodology
|
|
286
305
|
|
|
287
306
|
Qore now includes a reproducible browser benchmark that compares two rendering paths against the same workload:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { batch } from './signal.js';
|
|
2
2
|
import { toAsyncIterable } from './iterable.js';
|
|
3
|
-
import { createResponseState, isTerminalStatus } from './response-state.js';
|
|
3
|
+
import { appendResponseChunk, createResponseState, getResponseChunkCount, isTerminalStatus, snapshotResponseChunks } from './response-state.js';
|
|
4
4
|
import { normalizeError } from '../shared/utils.js';
|
|
5
5
|
function isResponseSourceFactory(source) {
|
|
6
6
|
const callableSource = source;
|
|
@@ -43,13 +43,13 @@ export function createResponse(options) {
|
|
|
43
43
|
if (isTerminalStatus(currentStatus)) {
|
|
44
44
|
return value.peek();
|
|
45
45
|
}
|
|
46
|
-
const index = chunks
|
|
46
|
+
const index = getResponseChunkCount(chunks);
|
|
47
47
|
const nextValue = reduce(value.peek(), chunk, index);
|
|
48
48
|
batch(() => {
|
|
49
49
|
if (status.peek() === 'idle' || status.peek() === 'pending') {
|
|
50
50
|
status('streaming');
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
appendResponseChunk(chunks, chunk);
|
|
53
53
|
value(nextValue);
|
|
54
54
|
});
|
|
55
55
|
return nextValue;
|
|
@@ -201,10 +201,10 @@ export function createResponse(options) {
|
|
|
201
201
|
status: status.peek(),
|
|
202
202
|
value: value.peek(),
|
|
203
203
|
error: error.peek(),
|
|
204
|
-
chunks:
|
|
204
|
+
chunks: snapshotResponseChunks(chunks),
|
|
205
205
|
startedAt: startedAt.peek(),
|
|
206
206
|
finishedAt: finishedAt.peek(),
|
|
207
|
-
chunkCount: chunks
|
|
207
|
+
chunkCount: getResponseChunkCount(chunks)
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
api = {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { type Signal } from './signal.js';
|
|
1
2
|
import type { ResponseReactiveState, ResponseStatus } from './response-types.js';
|
|
2
3
|
export declare function isTerminalStatus(currentStatus: ResponseStatus): boolean;
|
|
4
|
+
export declare function appendResponseChunk<T>(chunks: Signal<T[]>, chunk: T): number;
|
|
5
|
+
export declare function getResponseChunkCount<T>(chunks: Signal<T[]>): number;
|
|
6
|
+
export declare function snapshotResponseChunks<T>(chunks: Signal<T[]>): T[];
|
|
3
7
|
export declare function createResponseState<TChunk, TValue>(seed: TValue): ResponseReactiveState<TChunk, TValue>;
|
|
@@ -1,14 +1,79 @@
|
|
|
1
|
+
import { READ } from './signal-context.js';
|
|
1
2
|
import { computed, signal } from './signal.js';
|
|
2
3
|
// Treat these states as closed so late writes cannot mutate a finished response.
|
|
3
4
|
export function isTerminalStatus(currentStatus) {
|
|
4
5
|
return currentStatus === 'completed' || currentStatus === 'error' || currentStatus === 'aborted';
|
|
5
6
|
}
|
|
7
|
+
function isChunkSignal(value) {
|
|
8
|
+
return typeof value.append === 'function'
|
|
9
|
+
&& typeof value.count === 'function'
|
|
10
|
+
&& typeof value.peekCount === 'function'
|
|
11
|
+
&& typeof value.snapshot === 'function';
|
|
12
|
+
}
|
|
13
|
+
// Store the live chunk log behind a version signal so token commits do not copy
|
|
14
|
+
// the full history on every push. Public reads still receive defensive copies.
|
|
15
|
+
function createChunkSignal() {
|
|
16
|
+
let items = [];
|
|
17
|
+
const version = signal(0);
|
|
18
|
+
const notify = () => version(version.peek() + 1);
|
|
19
|
+
const chunks = ((nextValue = READ) => {
|
|
20
|
+
if (nextValue === READ) {
|
|
21
|
+
version();
|
|
22
|
+
return [...items];
|
|
23
|
+
}
|
|
24
|
+
items = [...nextValue];
|
|
25
|
+
notify();
|
|
26
|
+
return [...items];
|
|
27
|
+
});
|
|
28
|
+
chunks.set = (nextValue) => {
|
|
29
|
+
items = [...nextValue];
|
|
30
|
+
notify();
|
|
31
|
+
return [...items];
|
|
32
|
+
};
|
|
33
|
+
chunks.update = (updater) => chunks.set(updater([...items]));
|
|
34
|
+
chunks.peek = () => [...items];
|
|
35
|
+
chunks.subscribe = (listener, options = {}) => {
|
|
36
|
+
const { immediate = true } = options;
|
|
37
|
+
if (immediate) {
|
|
38
|
+
listener([...items]);
|
|
39
|
+
}
|
|
40
|
+
return version.subscribe(() => listener([...items]), { immediate: false });
|
|
41
|
+
};
|
|
42
|
+
chunks.append = (chunk) => {
|
|
43
|
+
const index = items.length;
|
|
44
|
+
items.push(chunk);
|
|
45
|
+
notify();
|
|
46
|
+
return index;
|
|
47
|
+
};
|
|
48
|
+
chunks.count = () => {
|
|
49
|
+
version();
|
|
50
|
+
return items.length;
|
|
51
|
+
};
|
|
52
|
+
chunks.peekCount = () => items.length;
|
|
53
|
+
chunks.snapshot = () => [...items];
|
|
54
|
+
return chunks;
|
|
55
|
+
}
|
|
56
|
+
export function appendResponseChunk(chunks, chunk) {
|
|
57
|
+
if (isChunkSignal(chunks)) {
|
|
58
|
+
return chunks.append(chunk);
|
|
59
|
+
}
|
|
60
|
+
const currentChunks = chunks.peek();
|
|
61
|
+
const index = currentChunks.length;
|
|
62
|
+
chunks([...currentChunks, chunk]);
|
|
63
|
+
return index;
|
|
64
|
+
}
|
|
65
|
+
export function getResponseChunkCount(chunks) {
|
|
66
|
+
return isChunkSignal(chunks) ? chunks.peekCount() : chunks.peek().length;
|
|
67
|
+
}
|
|
68
|
+
export function snapshotResponseChunks(chunks) {
|
|
69
|
+
return isChunkSignal(chunks) ? chunks.snapshot() : [...chunks.peek()];
|
|
70
|
+
}
|
|
6
71
|
// Create the reactive state bundle that powers a response lifecycle.
|
|
7
72
|
export function createResponseState(seed) {
|
|
8
73
|
const status = signal('idle');
|
|
9
74
|
const value = signal(seed);
|
|
10
75
|
const error = signal(null);
|
|
11
|
-
const chunks =
|
|
76
|
+
const chunks = createChunkSignal();
|
|
12
77
|
const startedAt = signal(null);
|
|
13
78
|
const finishedAt = signal(null);
|
|
14
79
|
const pending = computed(() => {
|
|
@@ -19,7 +84,7 @@ export function createResponseState(seed) {
|
|
|
19
84
|
const completed = computed(() => status() === 'completed');
|
|
20
85
|
const failed = computed(() => status() === 'error');
|
|
21
86
|
const aborted = computed(() => status() === 'aborted');
|
|
22
|
-
const chunkCount = computed(() => chunks()
|
|
87
|
+
const chunkCount = computed(() => chunks.count());
|
|
23
88
|
return {
|
|
24
89
|
status,
|
|
25
90
|
value,
|