@qorejs/qore 1.0.0 → 1.0.2

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
@@ -3,275 +3,131 @@
3
3
  [![npm version](https://img.shields.io/npm/v/%40qorejs%2Fqore?color=0f766e&label=npm)](https://www.npmjs.com/package/@qorejs/qore)
4
4
  [![latest release](https://img.shields.io/github/v/release/qorejs/qore?color=0f766e&label=release)](https://github.com/qorejs/qore/releases/latest)
5
5
  [![ci](https://github.com/qorejs/qore/actions/workflows/ci.yml/badge.svg)](https://github.com/qorejs/qore/actions/workflows/ci.yml)
6
- [![browser smoke](https://img.shields.io/badge/browser-smoke-playwright-45ba63)](#browser-regression)
7
6
  [![release checks](https://github.com/qorejs/qore/actions/workflows/release-check.yml/badge.svg)](https://github.com/qorejs/qore/actions/workflows/release-check.yml)
8
- [![publish github packages](https://github.com/qorejs/qore/actions/workflows/publish-github-packages.yml/badge.svg)](https://github.com/qorejs/qore/actions/workflows/publish-github-packages.yml)
9
- [![GitHub Packages](https://img.shields.io/badge/GitHub-Packages-181717?logo=github)](https://github.com/qorejs/qore/packages)
10
7
  [![node >= 18](https://img.shields.io/badge/node-%3E%3D18-1f6feb)](https://nodejs.org/)
11
8
  [![license: MIT](https://img.shields.io/badge/license-MIT-0f766e)](https://github.com/qorejs/qore/blob/main/LICENSE)
12
9
 
13
- Qore is a streaming-response framework where `stream = signal`.
10
+ Qore is a reactive stream runtime for AI-native interfaces.
14
11
 
15
- Instead of treating data as a snapshot, Qore treats it like a river. Tokens arrive piece by piece, and the UI should respond piece by piece too. No manual string accumulation. No scattered loading state. No partial rendering workaround layered on top of a snapshot-first mental model.
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
- Quick links:
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
- From GitHub Packages:
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 { createOpenAI, h, stream, text } from '@qorejs/qore';
23
+ import { createSSEResponse, h, mount, stream, text } from '@qorejs/qore';
89
24
 
90
- const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
91
- const answer = stream(openai.chat('hello'));
92
-
93
- return h('div', {}, text(() => answer()));
94
- ```
95
-
96
- Here, `answer` is all of the following at once:
97
-
98
- - A read-only `signal`, so `answer()` returns the current accumulated value
99
- - An `AsyncIterable`, so you can still use `for await...of`
100
- - A lifecycle-aware streaming state, with `status()`, `streaming()`, `error()`, and `chunks()`
101
-
102
- ## Performance Model
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
+ }
103
33
 
104
- Qore keeps the streaming hot path narrow:
34
+ // Browser: consume the stream as one reactive value.
35
+ const answer = stream(fetch('/api/chat').then((response) => response.body));
105
36
 
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
37
+ mount('#app', () =>
38
+ h('main', {}, text(() => answer()))
39
+ );
40
+ ```
110
41
 
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.
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.
112
43
 
113
44
  ## Why Qore
114
45
 
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';
46
+ React/Vercel AI SDK:
124
47
 
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())));
48
+ ```text
49
+ Token -> Hook state -> Component render -> Reconcile
132
50
  ```
133
51
 
134
- This updates only the text node that depends on the stream. It does not re-render the whole tree.
135
-
136
- ## Providers
52
+ Qore:
137
53
 
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?'));
54
+ ```text
55
+ Token -> Stream signal -> Text node
149
56
  ```
150
57
 
151
- ### `createAnthropic(options?)`
152
-
153
- ```js
154
- import { createAnthropic, stream } from '@qorejs/qore';
155
-
156
- const anthropic = createAnthropic({
157
- apiKey: process.env.ANTHROPIC_API_KEY,
158
- model: 'claude-sonnet-4-20250514'
159
- });
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.
160
59
 
161
- const answer = stream(anthropic.chat('Why should stream be signal?'));
162
- ```
60
+ ## Event Streams
163
61
 
164
- ### `createOpenRouter(options?)`
62
+ AI interfaces do not only stream text. They stream status, tool calls, reasoning, diffs, artifacts, retries, and errors.
165
63
 
166
64
  ```js
167
- import { createOpenRouter, stream } from '@qorejs/qore';
65
+ const events = stream.events(agent.run(task));
168
66
 
169
- const openrouter = createOpenRouter({
170
- apiKey: process.env.OPENROUTER_API_KEY,
171
- model: 'openai/gpt-4.1-mini'
67
+ const text = events.select('text', {
68
+ seed: '',
69
+ reduce: (current, event) => current + event.text
172
70
  });
173
-
174
- const answer = stream(openrouter.chat('Why should stream be signal?'));
71
+ const tools = events.select('tool_call');
72
+ const status = events.select('status');
73
+ const diff = events.select('diff');
175
74
  ```
176
75
 
177
- ### `createDeepSeek(options?)`
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()`. The runnable shape is captured in [`examples/agent-event-stream.ts`](./examples/agent-event-stream.ts).
178
77
 
179
- ```js
180
- import { createDeepSeek, stream } from '@qorejs/qore';
181
78
 
182
- const deepseek = createDeepSeek({
183
- apiKey: process.env.DEEPSEEK_API_KEY,
184
- model: 'deepseek-chat'
185
- });
79
+ ## React Adapter
186
80
 
187
- const answer = stream(deepseek.chat('Why should stream be signal?'));
188
- ```
189
-
190
- Provider adapters also accept a request `signal` so you can cancel in-flight streams explicitly:
191
-
192
- ```js
193
- const controller = new AbortController();
194
- const answer = stream(openrouter.chat('Keep streaming', {
195
- signal: controller.signal
196
- }));
81
+ Qore can be used inside React without replacing your UI stack. The release-ready adapter lives in [`packages/react`](./packages/react) and subscribes to Qore streams through React's external store contract, so the stream remains the source of truth while React renders the view.
197
82
 
198
- controller.abort('user navigated away');
199
- ```
200
-
201
- ### `createOllama(options?)`
83
+ ```tsx
84
+ import { stream } from '@qorejs/qore';
85
+ import { useQoreStream } from '@qorejs/react';
202
86
 
203
- If you want a local-first provider path, Qore can stream directly from Ollama:
204
-
205
- ```js
206
- import { createOllama, stream } from '@qorejs/qore';
87
+ function Answer({ prompt }) {
88
+ const answer = useQoreStream(
89
+ () => stream(fetch(`/api/chat?prompt=${encodeURIComponent(prompt)}`).then((response) => response.body)),
90
+ [prompt],
91
+ { initialValue: '' }
92
+ );
207
93
 
208
- const ollama = createOllama({
209
- model: 'llama3.2'
210
- });
211
-
212
- const answer = stream(ollama.chat('Why should stream be signal?'));
94
+ return <p>{answer.value}</p>;
95
+ }
213
96
  ```
214
97
 
215
- ### `createSSEAdapter(options?)`
98
+ See [React Adapter](./docs/react.md) for lifecycle, status, abort, and signal subscription details.
216
99
 
217
- If your backend already streams `text/event-stream`, Qore can adopt it directly:
100
+ ## Provider Safety
218
101
 
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
- });
238
-
239
- const answer = stream(provider.chat('hello'));
240
- ```
102
+ 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.
241
103
 
242
- That makes `stream(provider.chat(...))` a general entry point instead of something tied to a single SDK.
104
+ ## Documentation
243
105
 
244
- ### `createLineAdapter(options?)`
245
-
246
- If your backend streams newline-delimited JSON instead of `text/event-stream`, Qore can adopt that too:
106
+ - [Concepts](./docs/concepts.md): `stream = signal` and the mental model
107
+ - [Architecture](./docs/architecture.md): provider, runtime, signal, and DOM layers
108
+ - [API Reference](./docs/api.md): core primitives and composition helpers
109
+ - [Providers](./docs/providers.md): OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, SSE, and NDJSON
110
+ - [Runtime](./docs/runtime.md): backpressure, lifecycle, abort, retry, and orchestration
111
+ - [React Adapter](./docs/react.md): use Qore streams inside existing React apps
112
+ - [Comparisons](./docs/comparisons.md): Qore vs React, Vercel AI SDK, Solid, and Vue
113
+ - [Benchmarks](./docs/benchmarks.md): methodology and current evidence
114
+ - [migration notes](./MIGRATION.md)
115
+ - [RELEASE.md](./RELEASE.md)
247
116
 
248
- ```js
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
- });
117
+ ## Compatibility Matrix
272
118
 
273
- const answer = stream(provider.chat('hello'));
274
- ```
119
+ | Surface | Status | Notes |
120
+ | --- | --- | --- |
121
+ | Node runtime | Supported | `Node >= 18` |
122
+ | Browser runtime | Supported | DOM entrypoints require `document` |
123
+ | Reactive core | Supported | `signal`, `computed`, `effect`, `batch`, `createRoot`, `onCleanup` |
124
+ | Stream runtime | Supported | backpressure, retry, orchestration, async iteration, and abort |
125
+ | Provider adapters | Supported | OpenAI, Anthropic, OpenRouter, DeepSeek, Ollama, generic SSE, generic line-stream |
126
+ | Browser DOM layer | Supported | `h`, `text`, `dynamic`, `list`, `mount`, `createApp(...).mount(...)` |
127
+ | React adapter | Release-ready | `packages/react` bridges Qore streams through `useSyncExternalStore`; npm publishing is waiting on scope access |
128
+ | Published package maps | Supported | JavaScript source maps and declaration maps ship in `dist/src` |
129
+ | SSR | Not supported | explicit browser-only DOM boundary in `1.0.x` |
130
+ | Hydration | Not supported | deferred until a fully proven implementation exists |
275
131
 
276
132
  ## Provider Support Matrix
277
133
 
@@ -285,352 +141,13 @@ const answer = stream(provider.chat('hello'));
285
141
  | `createSSEAdapter(...)` | SSE | Supported | generic hosted or self-managed SSE |
286
142
  | `createLineAdapter(...)` | line-stream / NDJSON | Supported | generic line-delimited transport |
287
143
 
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
144
  ## Release Checklist
582
145
 
583
- The canonical release checklist lives in [`RELEASE.md`](./RELEASE.md). You can generate the current version's GitHub release body with `npm run release:notes`, and GitHub Actions publishes with npm provenance enabled.
584
-
585
- For every release candidate or stable release, the minimum local gate is:
146
+ The canonical release checklist lives in [RELEASE.md](./RELEASE.md). The minimum local gate is:
586
147
 
587
148
  ```bash
588
149
  npm ci
589
150
  npm run release:check
590
151
  ```
591
152
 
592
- For npm publishing from a local shell, run:
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
153
+ Qore publishes through GitHub Actions trusted publishing with npm provenance enabled.