@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
  [![npm version](https://img.shields.io/npm/v/%40qorejs%2Fqore?color=0f766e&label=npm)](https://www.npmjs.com/package/@qorejs/qore)
4
+ [![latest release](https://img.shields.io/github/v/release/qorejs/qore?color=0f766e&label=release)](https://github.com/qorejs/qore/releases/latest)
4
5
  [![ci](https://github.com/qorejs/qore/actions/workflows/ci.yml/badge.svg)](https://github.com/qorejs/qore/actions/workflows/ci.yml)
5
6
  [![browser smoke](https://img.shields.io/badge/browser-smoke-playwright-45ba63)](#browser-regression)
6
7
  [![release checks](https://github.com/qorejs/qore/actions/workflows/release-check.yml/badge.svg)](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.peek().length;
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
- chunks([...chunks.peek(), chunk]);
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: [...chunks.peek()],
204
+ chunks: snapshotResponseChunks(chunks),
205
205
  startedAt: startedAt.peek(),
206
206
  finishedAt: finishedAt.peek(),
207
- chunkCount: chunks.peek().length
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 = signal([]);
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().length);
87
+ const chunkCount = computed(() => chunks.count());
23
88
  return {
24
89
  status,
25
90
  value,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qorejs/qore",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Qore is a streaming-response framework where stream becomes signal.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",