@qorejs/qore 1.0.0-rc.3 → 1.0.1
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 +63 -570
- package/README.zh-CN.md +29 -0
- package/dist/src/core/stream-types.d.ts +14 -0
- package/dist/src/core/stream-types.d.ts.map +1 -1
- package/dist/src/core/stream.d.ts +1 -1
- package/dist/src/core/stream.d.ts.map +1 -1
- package/dist/src/core/stream.js +44 -0
- package/dist/src/core/stream.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/docs/api.md +71 -0
- package/docs/architecture.md +56 -0
- package/docs/benchmarks.md +27 -0
- package/docs/comparisons.md +35 -0
- package/docs/concepts.md +67 -0
- package/docs/providers.md +58 -0
- package/docs/runtime.md +81 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -3,275 +3,107 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@qorejs/qore)
|
|
4
4
|
[](https://github.com/qorejs/qore/releases/latest)
|
|
5
5
|
[](https://github.com/qorejs/qore/actions/workflows/ci.yml)
|
|
6
|
-
[](#browser-regression)
|
|
7
6
|
[](https://github.com/qorejs/qore/actions/workflows/release-check.yml)
|
|
8
|
-
[](https://github.com/qorejs/qore/actions/workflows/publish-github-packages.yml)
|
|
9
|
-
[](https://github.com/qorejs/qore/packages)
|
|
10
7
|
[](https://nodejs.org/)
|
|
11
8
|
[](https://github.com/qorejs/qore/blob/main/LICENSE)
|
|
12
9
|
|
|
13
|
-
Qore is a
|
|
10
|
+
Qore is a reactive stream runtime for AI-native interfaces.
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
Its core primitive is `stream = signal`: an async stream is also a readonly signal, so tokens, tool events, status updates, markdown fragments, or diffs can flow directly into UI state.
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- [npm package](https://www.npmjs.com/package/@qorejs/qore)
|
|
20
|
-
- [latest release](https://github.com/qorejs/qore/releases/latest)
|
|
21
|
-
- [GitHub Packages](https://github.com/qorejs/qore/packages)
|
|
22
|
-
- [migration notes](./MIGRATION.md)
|
|
23
|
-
- [landing page source](https://github.com/qorejs/qore/blob/main/index.html)
|
|
24
|
-
- [streaming demo source](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
|
|
25
|
-
- [benchmark page](https://github.com/qorejs/qore/blob/main/examples/benchmark.html)
|
|
26
|
-
- [continuous integration](https://github.com/qorejs/qore/actions/workflows/ci.yml)
|
|
27
|
-
- [release workflow](https://github.com/qorejs/qore/actions/workflows/release-check.yml)
|
|
28
|
-
- [contributing guide](https://github.com/qorejs/qore/blob/main/CONTRIBUTING.md)
|
|
29
|
-
- [security policy](https://github.com/qorejs/qore/blob/main/SECURITY.md)
|
|
30
|
-
|
|
31
|
-
## Installation
|
|
32
|
-
|
|
33
|
-
From npm:
|
|
14
|
+
## Install
|
|
34
15
|
|
|
35
16
|
```bash
|
|
36
17
|
npm i @qorejs/qore
|
|
37
18
|
```
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
echo "@qorejs:registry=https://npm.pkg.github.com" >> .npmrc
|
|
43
|
-
npm i @qorejs/qore
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
GitHub Packages installs require an authenticated session against `https://npm.pkg.github.com`.
|
|
47
|
-
|
|
48
|
-
For maintainers, the local npm release path now includes a fast preflight:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
npm run publish:preflight
|
|
52
|
-
npm run publish:npm
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
The preflight checks npm auth, confirms the changelog matches `package.json`, and fails early if that exact version is already published.
|
|
56
|
-
|
|
57
|
-
- Package name: `@qorejs/qore`
|
|
58
|
-
- Module format: `ESM`
|
|
59
|
-
- Supported runtime: `Node >= 18`
|
|
60
|
-
- CI coverage: `Node 18`, `20`, and `22`
|
|
61
|
-
- Browser regression: Playwright desktop + mobile smoke coverage for the homepage, focused demo, and benchmark page
|
|
62
|
-
- Registries:
|
|
63
|
-
- npm: [npmjs.com/package/@qorejs/qore](https://www.npmjs.com/package/@qorejs/qore)
|
|
64
|
-
- GitHub Packages: [github.com/qorejs/qore/packages](https://github.com/qorejs/qore/packages)
|
|
65
|
-
|
|
66
|
-
## Compatibility Matrix
|
|
67
|
-
|
|
68
|
-
| Surface | Status | Notes |
|
|
69
|
-
| --- | --- | --- |
|
|
70
|
-
| Node runtime | Supported | `Node >= 18` |
|
|
71
|
-
| Browser runtime | Supported | browser DOM entrypoints require `document` |
|
|
72
|
-
| Reactive core | Supported | `signal`, `computed`, `effect`, `batch`, `createRoot`, `onCleanup` |
|
|
73
|
-
| Stream runtime | Supported | includes backpressure, retry, orchestration, async iteration, and abort |
|
|
74
|
-
| Provider adapters | Supported | OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, generic SSE, generic line-stream |
|
|
75
|
-
| Browser DOM layer | Supported | `h`, `text`, `dynamic`, `list`, `mount`, `createApp(...).mount(...)` |
|
|
76
|
-
| Published package maps | Supported | JavaScript source maps and declaration maps ship in `dist/src` for debugger-friendly production diagnostics |
|
|
77
|
-
| SSR | Not supported | explicit browser-only DOM boundary in `1.0.x` |
|
|
78
|
-
| Hydration | Not supported | deferred until a fully proven implementation exists |
|
|
79
|
-
|
|
80
|
-
## Core Idea
|
|
81
|
-
|
|
82
|
-
`stream` is how data flows.
|
|
83
|
-
`signal` is how the UI reacts.
|
|
84
|
-
|
|
85
|
-
In Qore, they are two sides of the same primitive:
|
|
20
|
+
## 30-Second Demo
|
|
86
21
|
|
|
87
22
|
```js
|
|
88
|
-
import {
|
|
89
|
-
|
|
90
|
-
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
91
|
-
const answer = stream(openai.chat('hello'));
|
|
23
|
+
import { createSSEResponse, h, mount, stream, text } from '@qorejs/qore';
|
|
92
24
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
## Performance Model
|
|
103
|
-
|
|
104
|
-
Qore keeps the streaming hot path narrow:
|
|
105
|
-
|
|
106
|
-
- chunk commits append into an internal log instead of cloning the full history on every token
|
|
107
|
-
- public `chunks()` reads still return defensive copies, so consumers cannot corrupt runtime state
|
|
108
|
-
- `chunkCount()` tracks the internal log version directly, so status UIs can stay cheap during long generations
|
|
109
|
-
- DOM bindings update only the nodes that read the stream signal
|
|
110
|
-
|
|
111
|
-
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.
|
|
112
|
-
|
|
113
|
-
## Why Qore
|
|
114
|
-
|
|
115
|
-
- React treats streaming as a special case that needs extra machinery
|
|
116
|
-
- SolidJS has excellent signals, but no native stream primitive
|
|
117
|
-
- Vue has ergonomic refs, but stream handling still lives outside the core model
|
|
118
|
-
- Qore makes `stream = signal` the core API from the start
|
|
119
|
-
|
|
120
|
-
## Quick Start
|
|
121
|
-
|
|
122
|
-
```js
|
|
123
|
-
import { h, mount, stream, text } from '@qorejs/qore';
|
|
124
|
-
|
|
125
|
-
const answer = stream(async function* () {
|
|
126
|
-
yield 'stream';
|
|
127
|
-
yield ' = ';
|
|
128
|
-
yield 'signal';
|
|
129
|
-
}());
|
|
130
|
-
|
|
131
|
-
mount('#app', () => h('div', { className: 'answer' }, text(() => answer())));
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
This updates only the text node that depends on the stream. It does not re-render the whole tree.
|
|
135
|
-
|
|
136
|
-
## Providers
|
|
137
|
-
|
|
138
|
-
### `createOpenAI(options?)`
|
|
139
|
-
|
|
140
|
-
```js
|
|
141
|
-
import { createOpenAI, stream } from '@qorejs/qore';
|
|
142
|
-
|
|
143
|
-
const openai = createOpenAI({
|
|
144
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
145
|
-
model: 'gpt-5'
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const answer = stream(openai.chat('Why should stream be signal?'));
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### `createAnthropic(options?)`
|
|
152
|
-
|
|
153
|
-
```js
|
|
154
|
-
import { createAnthropic, stream } from '@qorejs/qore';
|
|
25
|
+
// Server: expose your provider as SSE.
|
|
26
|
+
export function handler() {
|
|
27
|
+
return createSSEResponse(async function* () {
|
|
28
|
+
yield 'stream';
|
|
29
|
+
yield ' = ';
|
|
30
|
+
yield 'signal';
|
|
31
|
+
}());
|
|
32
|
+
}
|
|
155
33
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
model: 'claude-sonnet-4-20250514'
|
|
159
|
-
});
|
|
34
|
+
// Browser: consume the stream as one reactive value.
|
|
35
|
+
const answer = stream(fetch('/api/chat').then((response) => response.body));
|
|
160
36
|
|
|
161
|
-
|
|
37
|
+
mount('#app', () =>
|
|
38
|
+
h('main', {}, text(() => answer()))
|
|
39
|
+
);
|
|
162
40
|
```
|
|
163
41
|
|
|
164
|
-
|
|
42
|
+
`answer` is a readonly signal and an async iterable. The UI reads `answer()`, and Qore updates only the DOM node that depends on it.
|
|
165
43
|
|
|
166
|
-
|
|
167
|
-
import { createOpenRouter, stream } from '@qorejs/qore';
|
|
44
|
+
## Why Qore
|
|
168
45
|
|
|
169
|
-
|
|
170
|
-
apiKey: process.env.OPENROUTER_API_KEY,
|
|
171
|
-
model: 'openai/gpt-4.1-mini'
|
|
172
|
-
});
|
|
46
|
+
React/Vercel AI SDK:
|
|
173
47
|
|
|
174
|
-
|
|
48
|
+
```text
|
|
49
|
+
Token -> Hook state -> Component render -> Reconcile
|
|
175
50
|
```
|
|
176
51
|
|
|
177
|
-
|
|
52
|
+
Qore:
|
|
178
53
|
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const deepseek = createDeepSeek({
|
|
183
|
-
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
184
|
-
model: 'deepseek-chat'
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const answer = stream(deepseek.chat('Why should stream be signal?'));
|
|
54
|
+
```text
|
|
55
|
+
Token -> Stream signal -> Text node
|
|
188
56
|
```
|
|
189
57
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
```js
|
|
193
|
-
const controller = new AbortController();
|
|
194
|
-
const answer = stream(openrouter.chat('Keep streaming', {
|
|
195
|
-
signal: controller.signal
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
|
-
controller.abort('user navigated away');
|
|
199
|
-
```
|
|
58
|
+
That is the core difference. Qore is not trying to be another AI SDK. It is the runtime layer that makes streamed data reactive.
|
|
200
59
|
|
|
201
|
-
|
|
60
|
+
## Event Streams
|
|
202
61
|
|
|
203
|
-
|
|
62
|
+
AI interfaces do not only stream text. They stream status, tool calls, reasoning, diffs, artifacts, retries, and errors.
|
|
204
63
|
|
|
205
64
|
```js
|
|
206
|
-
|
|
65
|
+
const events = stream.events(agent.run(task));
|
|
207
66
|
|
|
208
|
-
const
|
|
209
|
-
|
|
67
|
+
const text = events.select('text', {
|
|
68
|
+
seed: '',
|
|
69
|
+
reduce: (current, event) => current + event.text
|
|
210
70
|
});
|
|
211
|
-
|
|
212
|
-
const
|
|
71
|
+
const tools = events.select('tool_call');
|
|
72
|
+
const status = events.select('status');
|
|
73
|
+
const diff = events.select('diff');
|
|
213
74
|
```
|
|
214
75
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
If your backend already streams `text/event-stream`, Qore can adopt it directly:
|
|
218
|
-
|
|
219
|
-
```js
|
|
220
|
-
import { createSSEAdapter, stream } from '@qorejs/qore';
|
|
221
|
-
|
|
222
|
-
const provider = createSSEAdapter({
|
|
223
|
-
name: 'Local Chat',
|
|
224
|
-
url: 'http://localhost:3000/api/chat',
|
|
225
|
-
buildRequest(request) {
|
|
226
|
-
return {
|
|
227
|
-
method: 'POST',
|
|
228
|
-
body: JSON.stringify(request)
|
|
229
|
-
};
|
|
230
|
-
},
|
|
231
|
-
buildChatRequest(input) {
|
|
232
|
-
return { prompt: input };
|
|
233
|
-
},
|
|
234
|
-
eventToText(event) {
|
|
235
|
-
return event.data?.type === 'token' ? event.data.text : undefined;
|
|
236
|
-
}
|
|
237
|
-
});
|
|
76
|
+
Each selected stream is still a signal and an async iterable. A timeline can render every event, while a markdown pane can bind only to `text()`.
|
|
238
77
|
|
|
239
|
-
|
|
240
|
-
```
|
|
78
|
+
## Provider Safety
|
|
241
79
|
|
|
242
|
-
|
|
80
|
+
Provider adapters are intended for server-side or trusted runtimes. Do not put `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or any vendor secret in browser code. For browsers, expose your own SSE or NDJSON endpoint and stream that endpoint into Qore.
|
|
243
81
|
|
|
244
|
-
|
|
82
|
+
## Documentation
|
|
245
83
|
|
|
246
|
-
|
|
84
|
+
- [Concepts](./docs/concepts.md): `stream = signal` and the mental model
|
|
85
|
+
- [Architecture](./docs/architecture.md): provider, runtime, signal, and DOM layers
|
|
86
|
+
- [API Reference](./docs/api.md): core primitives and composition helpers
|
|
87
|
+
- [Providers](./docs/providers.md): OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, SSE, and NDJSON
|
|
88
|
+
- [Runtime](./docs/runtime.md): backpressure, lifecycle, abort, retry, and orchestration
|
|
89
|
+
- [Comparisons](./docs/comparisons.md): Qore vs React, Vercel AI SDK, Solid, and Vue
|
|
90
|
+
- [Benchmarks](./docs/benchmarks.md): methodology and current evidence
|
|
91
|
+
- [migration notes](./MIGRATION.md)
|
|
92
|
+
- [RELEASE.md](./RELEASE.md)
|
|
247
93
|
|
|
248
|
-
|
|
249
|
-
import { createLineAdapter, stream } from '@qorejs/qore';
|
|
250
|
-
|
|
251
|
-
const provider = createLineAdapter({
|
|
252
|
-
name: 'Local NDJSON Chat',
|
|
253
|
-
url: 'http://localhost:11434/api/chat',
|
|
254
|
-
buildRequest(request) {
|
|
255
|
-
return {
|
|
256
|
-
method: 'POST',
|
|
257
|
-
body: JSON.stringify(request)
|
|
258
|
-
};
|
|
259
|
-
},
|
|
260
|
-
buildChatRequest(input) {
|
|
261
|
-
return {
|
|
262
|
-
model: 'llama3.2',
|
|
263
|
-
messages: [{ role: 'user', content: input }]
|
|
264
|
-
};
|
|
265
|
-
},
|
|
266
|
-
lineToText(event) {
|
|
267
|
-
return typeof event.data?.message?.content === 'string'
|
|
268
|
-
? event.data.message.content
|
|
269
|
-
: undefined;
|
|
270
|
-
}
|
|
271
|
-
});
|
|
94
|
+
## Compatibility Matrix
|
|
272
95
|
|
|
273
|
-
|
|
274
|
-
|
|
96
|
+
| Surface | Status | Notes |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| Node runtime | Supported | `Node >= 18` |
|
|
99
|
+
| Browser runtime | Supported | DOM entrypoints require `document` |
|
|
100
|
+
| Reactive core | Supported | `signal`, `computed`, `effect`, `batch`, `createRoot`, `onCleanup` |
|
|
101
|
+
| Stream runtime | Supported | backpressure, retry, orchestration, async iteration, and abort |
|
|
102
|
+
| Provider adapters | Supported | OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, generic SSE, generic line-stream |
|
|
103
|
+
| Browser DOM layer | Supported | `h`, `text`, `dynamic`, `list`, `mount`, `createApp(...).mount(...)` |
|
|
104
|
+
| Published package maps | Supported | JavaScript source maps and declaration maps ship in `dist/src` |
|
|
105
|
+
| SSR | Not supported | explicit browser-only DOM boundary in `1.0.x` |
|
|
106
|
+
| Hydration | Not supported | deferred until a fully proven implementation exists |
|
|
275
107
|
|
|
276
108
|
## Provider Support Matrix
|
|
277
109
|
|
|
@@ -285,352 +117,13 @@ const answer = stream(provider.chat('hello'));
|
|
|
285
117
|
| `createSSEAdapter(...)` | SSE | Supported | generic hosted or self-managed SSE |
|
|
286
118
|
| `createLineAdapter(...)` | line-stream / NDJSON | Supported | generic line-delimited transport |
|
|
287
119
|
|
|
288
|
-
Common guarantees across supported adapters:
|
|
289
|
-
|
|
290
|
-
- async iterable text streaming
|
|
291
|
-
- request `signal` support for cancellation
|
|
292
|
-
- typed event streaming on provider-specific surfaces
|
|
293
|
-
- package smoke coverage
|
|
294
|
-
|
|
295
|
-
Additional hosted SSE guarantees:
|
|
296
|
-
|
|
297
|
-
- retry contract support
|
|
298
|
-
- `Last-Event-ID` resume support
|
|
299
|
-
- normalized metadata helpers for usage, finish reason, response id, and model identity
|
|
300
|
-
|
|
301
|
-
## API Shape
|
|
302
|
-
|
|
303
|
-
### `stream(source, options?)`
|
|
304
|
-
|
|
305
|
-
By default, `stream(...)` accumulates chunks into a text signal:
|
|
306
|
-
|
|
307
|
-
```js
|
|
308
|
-
const answer = stream(openai.chat('hello'));
|
|
309
|
-
|
|
310
|
-
answer(); // current text
|
|
311
|
-
answer.status(); // idle | pending | streaming | completed | error | aborted
|
|
312
|
-
answer.streaming(); // boolean
|
|
313
|
-
answer.chunks(); // raw chunks
|
|
314
|
-
await answer.ready; // wait for completion
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
If you need structured streams:
|
|
318
|
-
|
|
319
|
-
```js
|
|
320
|
-
const events = stream.list(eventSource);
|
|
321
|
-
const latest = stream.latest(modelEvents);
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
For append-heavy DOM lists such as chat transcripts, pass a stable key:
|
|
325
|
-
|
|
326
|
-
```js
|
|
327
|
-
list(messages, (message) => h('article', {}, message.body), {
|
|
328
|
-
key: (message) => message.id
|
|
329
|
-
});
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
## Server-Side SSE
|
|
333
|
-
|
|
334
|
-
If you want Qore to produce the server stream as well:
|
|
335
|
-
|
|
336
|
-
```js
|
|
337
|
-
import { createSSEResponse } from '@qorejs/qore';
|
|
338
|
-
|
|
339
|
-
export function handler() {
|
|
340
|
-
return createSSEResponse(['hello', ' world']);
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
If you need orchestration:
|
|
345
|
-
|
|
346
|
-
```js
|
|
347
|
-
const merged = stream.merge([openai.chat('a'), anthropic.chat('b')]);
|
|
348
|
-
const scripted = stream.concat([retrieve.chat('a'), summarize.chat('a')]);
|
|
349
|
-
const pipeline = stream.pipe(openai.chat('hello'), [
|
|
350
|
-
(draft) => review.chat(draft),
|
|
351
|
-
(reviewed) => format.chat(reviewed)
|
|
352
|
-
]);
|
|
353
|
-
const fastest = stream.race([openai.chat('hello'), openrouter.chat('hello')]);
|
|
354
|
-
const resilient = stream.retryable(() => openai.chat('retry me'), {
|
|
355
|
-
maxRetries: 2,
|
|
356
|
-
backoff: 'exponential'
|
|
357
|
-
});
|
|
358
|
-
const liveAnswer = stream.switchMap(promptChanges, (prompt) => openai.chat(prompt));
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Provider adapters can also retry dropped SSE connections and resume from the last event id:
|
|
362
|
-
|
|
363
|
-
```js
|
|
364
|
-
const openai = createOpenAI({
|
|
365
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
366
|
-
retry: {
|
|
367
|
-
maxAttempts: 3,
|
|
368
|
-
backoff: 'exponential'
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
Provider metadata can be normalized into one shared shape:
|
|
374
|
-
|
|
375
|
-
```js
|
|
376
|
-
import {
|
|
377
|
-
collectProviderMetadata,
|
|
378
|
-
extractAnthropicMetadata,
|
|
379
|
-
extractOpenAIMetadata
|
|
380
|
-
} from '@qorejs/qore';
|
|
381
|
-
|
|
382
|
-
const openaiMetadata = await collectProviderMetadata(
|
|
383
|
-
'OpenAI',
|
|
384
|
-
openai.responses.stream({ input: 'hello' }),
|
|
385
|
-
extractOpenAIMetadata
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
const anthropicMetadata = await collectProviderMetadata(
|
|
389
|
-
'Anthropic',
|
|
390
|
-
anthropic.messages.stream({ messages: [{ role: 'user', content: 'hello' }] }),
|
|
391
|
-
extractAnthropicMetadata
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
openaiMetadata.usage?.totalTokens;
|
|
395
|
-
anthropicMetadata.finishReason;
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### Backpressure
|
|
399
|
-
|
|
400
|
-
```js
|
|
401
|
-
const answer = stream.withBackpressure(openai.chat('hello'), {
|
|
402
|
-
interval: 16,
|
|
403
|
-
buffer: 8,
|
|
404
|
-
overflow: 'drop-oldest'
|
|
405
|
-
});
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
Backpressure is not just a delay wrapper:
|
|
409
|
-
|
|
410
|
-
- `interval`: the minimum spacing between chunk delivery into the signal and UI
|
|
411
|
-
- `buffer`: the maximum number of queued chunks before the UI catches up
|
|
412
|
-
- `overflow`: what to do when the buffer is full: `wait`, `drop-oldest`, `drop-newest`, or `error`
|
|
413
|
-
|
|
414
|
-
You can also observe stream pressure directly:
|
|
415
|
-
|
|
416
|
-
```js
|
|
417
|
-
answer.buffered(); // how many chunks are queued right now
|
|
418
|
-
answer.dropped(); // how many chunks were dropped by the overflow policy
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### `signal`, `computed`, `effect`, `createRoot`, `onCleanup`
|
|
422
|
-
|
|
423
|
-
```js
|
|
424
|
-
import { computed, createRoot, effect, onCleanup, signal, stream } from '@qorejs/qore';
|
|
425
|
-
|
|
426
|
-
const answer = stream(openai.chat('hello'));
|
|
427
|
-
const length = computed(() => answer().length);
|
|
428
|
-
|
|
429
|
-
const dispose = createRoot((dispose) => {
|
|
430
|
-
effect(() => {
|
|
431
|
-
console.log(length());
|
|
432
|
-
onCleanup(() => console.log('effect disposed'));
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
return dispose;
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
dispose();
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
### `response`
|
|
442
|
-
|
|
443
|
-
`response` still exists, but it is closer to a lower-level state machine escape hatch for custom reducers and aggregators.
|
|
444
|
-
|
|
445
|
-
If your goal is to pipe a stream directly into the UI, prefer `stream(...)`.
|
|
446
|
-
|
|
447
|
-
## Demos
|
|
448
|
-
|
|
449
|
-
The repository includes a landing page and a focused streaming demo:
|
|
450
|
-
|
|
451
|
-
- [Landing Page Source](https://github.com/qorejs/qore/blob/main/index.html)
|
|
452
|
-
- [Homepage Logic](https://github.com/qorejs/qore/blob/main/examples/showcase.ts)
|
|
453
|
-
- [Homepage Styles](https://github.com/qorejs/qore/blob/main/examples/showcase.css)
|
|
454
|
-
- [Benchmark Page](https://github.com/qorejs/qore/blob/main/examples/benchmark.html)
|
|
455
|
-
- [Benchmark Logic](https://github.com/qorejs/qore/blob/main/examples/benchmark-page.ts)
|
|
456
|
-
- [Benchmark Core](https://github.com/qorejs/qore/blob/main/examples/benchmark-core.ts)
|
|
457
|
-
- [Focused Demo](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
|
|
458
|
-
- [Focused Chat Logic](https://github.com/qorejs/qore/blob/main/examples/qore-chat.ts)
|
|
459
|
-
- [React Compare](https://github.com/qorejs/qore/blob/main/examples/react-chat.ts)
|
|
460
|
-
|
|
461
|
-
## Project Layout
|
|
462
|
-
|
|
463
|
-
```text
|
|
464
|
-
src/
|
|
465
|
-
core/ stream, signal, response, iterable
|
|
466
|
-
dom/ app mounting and DOM bindings
|
|
467
|
-
providers/ OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, SSE, and line-stream adapters
|
|
468
|
-
shared/ runtime utilities
|
|
469
|
-
index.ts public entrypoint
|
|
470
|
-
|
|
471
|
-
dist/
|
|
472
|
-
src/ compiled package output
|
|
473
|
-
examples/ built showcase scripts for local preview
|
|
474
|
-
test/ compiled test output
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
For a local preview:
|
|
478
|
-
|
|
479
|
-
```bash
|
|
480
|
-
git clone git@github.com:qorejs/qore.git
|
|
481
|
-
cd qore
|
|
482
|
-
npm install
|
|
483
|
-
npm run build
|
|
484
|
-
python3 -m http.server 4173
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
Then open [http://127.0.0.1:4173/](http://127.0.0.1:4173/).
|
|
488
|
-
|
|
489
|
-
## Server And SSR
|
|
490
|
-
|
|
491
|
-
Qore's reactive core and stream runtime work in Node and browser environments today.
|
|
492
|
-
|
|
493
|
-
The DOM layer is intentionally browser-only right now:
|
|
494
|
-
|
|
495
|
-
- `signal`, `computed`, `effect`, `stream`, and provider adapters work in Node and the browser
|
|
496
|
-
- `h`, `text`, `mount`, and `createApp(...).mount(...)` require a browser-like `document`
|
|
497
|
-
- `canUseDOM()` is exported so integrations can branch cleanly before touching DOM APIs
|
|
498
|
-
- `assertCanUseDOM(name?)` is exported if you want to fail fast with the same browser-boundary error shape Qore uses internally
|
|
499
|
-
- the published entrypoint is checked against a frozen public API snapshot before release so accidental export drift fails CI early
|
|
500
|
-
|
|
501
|
-
If you call DOM helpers without a browser-like runtime, Qore throws an entrypoint-specific error instead of failing later with a generic reference error. For example:
|
|
502
|
-
|
|
503
|
-
- `h() requires a browser-like environment`
|
|
504
|
-
- `mount() requires a browser-like environment`
|
|
505
|
-
- `createApp(...).mount(...) requires a browser-like environment`
|
|
506
|
-
|
|
507
|
-
Example:
|
|
508
|
-
|
|
509
|
-
```js
|
|
510
|
-
import { assertCanUseDOM, canUseDOM } from '@qorejs/qore';
|
|
511
|
-
|
|
512
|
-
if (canUseDOM()) {
|
|
513
|
-
// Safe to call mount(), h(), text(), and other DOM entrypoints.
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
assertCanUseDOM('chat shell hydration');
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
That means the current `1.0.0` path is:
|
|
520
|
-
|
|
521
|
-
- stable reactive runtime
|
|
522
|
-
- stable streaming runtime
|
|
523
|
-
- explicit browser DOM boundary
|
|
524
|
-
- streaming SSR and hydration as a post-`1.0.0` expansion area unless the implementation is fully proven first
|
|
525
|
-
|
|
526
|
-
## Browser Regression
|
|
527
|
-
|
|
528
|
-
Install the browser binary once:
|
|
529
|
-
|
|
530
|
-
```bash
|
|
531
|
-
npm run browsers:install
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Then run the browser smoke suite:
|
|
535
|
-
|
|
536
|
-
```bash
|
|
537
|
-
npm run test:browser
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
It validates:
|
|
541
|
-
|
|
542
|
-
- the homepage stream demo
|
|
543
|
-
- the focused streaming chat demo
|
|
544
|
-
- the dedicated benchmark page
|
|
545
|
-
|
|
546
|
-
The suite checks desktop and mobile layouts, watches for runtime console errors, exercises the primary interactions, and runs inside `release:check`.
|
|
547
|
-
|
|
548
|
-
CI also uploads the browser regression evidence as workflow artifacts. The bundle includes viewport screenshots, focused page-surface screenshots, the Playwright HTML report, a `benchmark-suite.json` attachment from the dedicated benchmark page, and a human-readable benchmark summary markdown file from the benchmark gate.
|
|
549
|
-
|
|
550
|
-
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`.
|
|
551
|
-
|
|
552
|
-
Local preview ports can also be pinned when another process is already using the default range:
|
|
553
|
-
|
|
554
|
-
```bash
|
|
555
|
-
QORE_STATIC_PORT=4300 QORE_STATIC_PORT_END=4400 npm run test:browser
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
## Benchmark Methodology
|
|
559
|
-
|
|
560
|
-
Qore now includes a reproducible browser benchmark that compares two rendering paths against the same workload:
|
|
561
|
-
|
|
562
|
-
- `Qore stream = signal`: mount the transcript shell once and advance the same live text node as chunks arrive
|
|
563
|
-
- `Snapshot rerender baseline`: rebuild the transcript shell from a snapshot string on every chunk
|
|
564
|
-
|
|
565
|
-
Both paths use the same transcript history, the same chunk list, and the same final answer text. The benchmark reports first paint time, mutation records, node churn, and regenerated markup so the difference is visible instead of rhetorical.
|
|
566
|
-
|
|
567
|
-
## GitHub Packages
|
|
568
|
-
|
|
569
|
-
The repository includes GitHub Actions workflows for both release validation and GitHub Packages publishing.
|
|
570
|
-
|
|
571
|
-
- Release validation: `.github/workflows/release-check.yml`
|
|
572
|
-
- GitHub Packages publish: `.github/workflows/publish-github-packages.yml`
|
|
573
|
-
- Both workflows can be triggered manually from the Actions tab
|
|
574
|
-
- Publishing a GitHub Release triggers both the release check and the GitHub Packages publish flow
|
|
575
|
-
- The publish workflow validates the release tag, changelog, test suite, and tarball before it pushes the package
|
|
576
|
-
- The publish workflow uses the repository `GITHUB_TOKEN`, which GitHub documents as the recommended way to publish packages from the workflow repository
|
|
577
|
-
- GitHub Packages starts new packages as private by default, so you may want to switch the package visibility to public after the first publish
|
|
578
|
-
|
|
579
|
-
Because the package already includes the correct `repository` field in `package.json`, GitHub Packages can link the package back to `qorejs/qore` when the workflow publishes it.
|
|
580
|
-
|
|
581
120
|
## Release Checklist
|
|
582
121
|
|
|
583
|
-
The canonical release checklist lives in [
|
|
584
|
-
|
|
585
|
-
For every release candidate or stable release, the minimum local gate is:
|
|
122
|
+
The canonical release checklist lives in [RELEASE.md](./RELEASE.md). The minimum local gate is:
|
|
586
123
|
|
|
587
124
|
```bash
|
|
588
125
|
npm ci
|
|
589
126
|
npm run release:check
|
|
590
127
|
```
|
|
591
128
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
```bash
|
|
595
|
-
npm run publish:preflight
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
## Project Hygiene
|
|
599
|
-
|
|
600
|
-
The repository also includes:
|
|
601
|
-
|
|
602
|
-
- `CONTRIBUTING.md` for contributor expectations and release flow
|
|
603
|
-
- `SECURITY.md` for responsible disclosure
|
|
604
|
-
- issue templates for bugs and feature requests
|
|
605
|
-
- a pull request template
|
|
606
|
-
- `.github/release.yml` to keep GitHub release notes structured
|
|
607
|
-
|
|
608
|
-
## Package Boundary
|
|
609
|
-
|
|
610
|
-
Qore does not ship a built-in catalog of buttons, dialogs, tabs, or other UI primitives.
|
|
611
|
-
|
|
612
|
-
The core package does only three things:
|
|
613
|
-
|
|
614
|
-
- Move streams into state
|
|
615
|
-
- Move state into the UI
|
|
616
|
-
- Keep the whole process finely reactive
|
|
617
|
-
|
|
618
|
-
Anything that does not serve `streaming response` belongs in an experimental layer or a separate package.
|
|
619
|
-
|
|
620
|
-
## Testing
|
|
621
|
-
|
|
622
|
-
```bash
|
|
623
|
-
npm test
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
The current test suite covers:
|
|
627
|
-
|
|
628
|
-
- `signal`, `computed`, and `effect`
|
|
629
|
-
- The core `stream = signal` behavior
|
|
630
|
-
- `response` interoperability with async iterables
|
|
631
|
-
- OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, and generic streaming adapters
|
|
632
|
-
|
|
633
|
-
## Roadmap
|
|
634
|
-
|
|
635
|
-
- Tighten the hydration model around server-streamed rendering
|
|
636
|
-
- Publish repeatable benchmarks that compare Qore with React and the Vercel AI SDK
|
|
129
|
+
Qore publishes through GitHub Actions trusted publishing with npm provenance enabled.
|