@qorejs/qore 0.6.0 → 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Qore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # Qore
2
+
3
+ Qore is a streaming-response framework where `stream = signal`.
4
+
5
+ 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.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm i @qorejs/qore
11
+ ```
12
+
13
+ - Package name: `@qorejs/qore`
14
+ - Module format: `ESM`
15
+ - Supported runtime: `Node >= 18`
16
+
17
+ ## Core Idea
18
+
19
+ `stream` is how data flows.
20
+ `signal` is how the UI reacts.
21
+
22
+ In Qore, they are two sides of the same primitive:
23
+
24
+ ```js
25
+ import { createOpenAI, h, stream, text } from '@qorejs/qore';
26
+
27
+ const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
28
+ const answer = stream(openai.chat('hello'));
29
+
30
+ return h('div', {}, text(() => answer()));
31
+ ```
32
+
33
+ Here, `answer` is all of the following at once:
34
+
35
+ - A read-only `signal`, so `answer()` returns the current accumulated value
36
+ - An `AsyncIterable`, so you can still use `for await...of`
37
+ - A lifecycle-aware streaming state, with `status()`, `streaming()`, `error()`, and `chunks()`
38
+
39
+ ## Why Qore
40
+
41
+ - React treats streaming as a special case that needs extra machinery
42
+ - SolidJS has excellent signals, but no native stream primitive
43
+ - Vue has ergonomic refs, but stream handling still lives outside the core model
44
+ - Qore makes `stream = signal` the core API from the start
45
+
46
+ ## Quick Start
47
+
48
+ ```js
49
+ import { h, mount, stream, text } from '@qorejs/qore';
50
+
51
+ const answer = stream(async function* () {
52
+ yield 'stream';
53
+ yield ' = ';
54
+ yield 'signal';
55
+ }());
56
+
57
+ mount('#app', () => h('div', { className: 'answer' }, text(() => answer())));
58
+ ```
59
+
60
+ This updates only the text node that depends on the stream. It does not re-render the whole tree.
61
+
62
+ ## Providers
63
+
64
+ ### `createOpenAI(options?)`
65
+
66
+ ```js
67
+ import { createOpenAI, stream } from '@qorejs/qore';
68
+
69
+ const openai = createOpenAI({
70
+ apiKey: process.env.OPENAI_API_KEY,
71
+ model: 'gpt-5'
72
+ });
73
+
74
+ const answer = stream(openai.chat('Why should stream be signal?'));
75
+ ```
76
+
77
+ ### `createAnthropic(options?)`
78
+
79
+ ```js
80
+ import { createAnthropic, stream } from '@qorejs/qore';
81
+
82
+ const anthropic = createAnthropic({
83
+ apiKey: process.env.ANTHROPIC_API_KEY,
84
+ model: 'claude-sonnet-4-20250514'
85
+ });
86
+
87
+ const answer = stream(anthropic.chat('Why should stream be signal?'));
88
+ ```
89
+
90
+ ### `createSSEAdapter(options?)`
91
+
92
+ If your backend already streams `text/event-stream`, Qore can adopt it directly:
93
+
94
+ ```js
95
+ import { createSSEAdapter, stream } from '@qorejs/qore';
96
+
97
+ const provider = createSSEAdapter({
98
+ name: 'Local Chat',
99
+ url: 'http://localhost:3000/api/chat',
100
+ buildRequest(request) {
101
+ return {
102
+ method: 'POST',
103
+ body: JSON.stringify(request)
104
+ };
105
+ },
106
+ buildChatRequest(input) {
107
+ return { prompt: input };
108
+ },
109
+ eventToText(event) {
110
+ return event.data?.type === 'token' ? event.data.text : undefined;
111
+ }
112
+ });
113
+
114
+ const answer = stream(provider.chat('hello'));
115
+ ```
116
+
117
+ That makes `stream(provider.chat(...))` a general entry point instead of something tied to a single SDK.
118
+
119
+ ## API Shape
120
+
121
+ ### `stream(source, options?)`
122
+
123
+ By default, `stream(...)` accumulates chunks into a text signal:
124
+
125
+ ```js
126
+ const answer = stream(openai.chat('hello'));
127
+
128
+ answer(); // current text
129
+ answer.status(); // idle | pending | streaming | completed | error | aborted
130
+ answer.streaming(); // boolean
131
+ answer.chunks(); // raw chunks
132
+ await answer.ready; // wait for completion
133
+ ```
134
+
135
+ If you need structured streams:
136
+
137
+ ```js
138
+ const events = stream.list(eventSource);
139
+ const latest = stream.latest(modelEvents);
140
+ ```
141
+
142
+ ### Backpressure
143
+
144
+ ```js
145
+ const answer = stream.withBackpressure(openai.chat('hello'), {
146
+ interval: 16,
147
+ buffer: 8,
148
+ overflow: 'drop-oldest'
149
+ });
150
+ ```
151
+
152
+ Backpressure is not just a delay wrapper:
153
+
154
+ - `interval`: the minimum spacing between chunk delivery into the signal and UI
155
+ - `buffer`: the maximum number of queued chunks before the UI catches up
156
+ - `overflow`: what to do when the buffer is full: `wait`, `drop-oldest`, `drop-newest`, or `error`
157
+
158
+ You can also observe stream pressure directly:
159
+
160
+ ```js
161
+ answer.buffered(); // how many chunks are queued right now
162
+ answer.dropped(); // how many chunks were dropped by the overflow policy
163
+ ```
164
+
165
+ ### `signal`, `computed`, `effect`
166
+
167
+ ```js
168
+ import { computed, signal, stream } from '@qorejs/qore';
169
+
170
+ const answer = stream(openai.chat('hello'));
171
+ const length = computed(() => answer().length);
172
+ ```
173
+
174
+ ### `response`
175
+
176
+ `response` still exists, but it is closer to a lower-level state machine escape hatch for custom reducers and aggregators.
177
+
178
+ If your goal is to pipe a stream directly into the UI, prefer `stream(...)`.
179
+
180
+ ## Demos
181
+
182
+ The repository includes a landing page and a focused streaming demo:
183
+
184
+ - [Landing Page Source](https://github.com/qorejs/qore/blob/main/index.html)
185
+ - [Homepage Logic](https://github.com/qorejs/qore/blob/main/examples/showcase.js)
186
+ - [Homepage Styles](https://github.com/qorejs/qore/blob/main/examples/showcase.css)
187
+ - [Focused Demo](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
188
+ - [Focused Chat Logic](https://github.com/qorejs/qore/blob/main/examples/qore-chat.js)
189
+ - [React Compare](https://github.com/qorejs/qore/blob/main/examples/react-chat.jsx)
190
+
191
+ For a local preview:
192
+
193
+ ```bash
194
+ git clone git@github.com:qorejs/qore.git
195
+ cd qore
196
+ python3 -m http.server 4173
197
+ ```
198
+
199
+ Then open [http://127.0.0.1:4173/](http://127.0.0.1:4173/).
200
+
201
+ ## Package Boundary
202
+
203
+ Qore does not ship a built-in catalog of buttons, dialogs, tabs, or other UI primitives.
204
+
205
+ The core package does only three things:
206
+
207
+ - Move streams into state
208
+ - Move state into the UI
209
+ - Keep the whole process finely reactive
210
+
211
+ Anything that does not serve `streaming response` belongs in an experimental layer or a separate package.
212
+
213
+ ## Testing
214
+
215
+ ```bash
216
+ npm test
217
+ ```
218
+
219
+ The current test suite covers:
220
+
221
+ - `signal`, `computed`, and `effect`
222
+ - The core `stream = signal` behavior
223
+ - `response` interoperability with async iterables
224
+ - OpenAI, Anthropic, and generic SSE adapters
225
+
226
+ ## Roadmap
227
+
228
+ - Tighten the hydration model around server-streamed rendering
229
+ - Publish repeatable benchmarks that compare Qore with React and the Vercel AI SDK
package/package.json CHANGED
@@ -1,76 +1,54 @@
1
1
  {
2
2
  "name": "@qorejs/qore",
3
- "version": "0.6.0",
4
- "description": "Qore - AI-Native Frontend Framework (<3kb gzip)",
3
+ "version": "0.7.1",
4
+ "description": "Qore is a streaming-response framework where stream becomes signal.",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
9
8
  "exports": {
10
9
  ".": {
11
- "import": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js"
13
12
  },
14
- "./ssr": {
15
- "import": "./dist/ssr.js",
16
- "types": "./dist/ssr.d.ts"
17
- },
18
- "./virtual-list": {
19
- "import": "./dist/virtual-list.js",
20
- "types": "./dist/virtual-list.d.ts"
21
- }
13
+ "./package.json": "./package.json"
22
14
  },
23
15
  "files": [
24
- "dist",
25
16
  "src",
26
17
  "README.md",
27
18
  "LICENSE"
28
19
  ],
29
20
  "sideEffects": false,
30
21
  "scripts": {
31
- "dev": "vite build --watch",
32
- "build": "vite build",
33
- "build:types": "tsc --emitDeclarationOnly",
34
- "test": "vitest run",
35
- "test:watch": "vitest",
36
- "test:coverage": "vitest run --coverage",
37
- "prepublishOnly": "pnpm run build && pnpm run test",
38
- "publish:npm": "npm publish --access public"
22
+ "test": "node --test",
23
+ "release:check": "npm test && npm pack --dry-run",
24
+ "prepublishOnly": "npm run release:check"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
39
28
  },
40
29
  "keywords": [
41
30
  "qore",
42
- "frontend",
43
- "framework",
44
- "ai-native",
45
- "reactive",
46
- "signals",
31
+ "stream",
47
32
  "streaming",
48
- "ssr",
49
- "virtual-list",
50
- "lightweight",
51
- "performance",
52
- "typescript"
33
+ "signal",
34
+ "reactive",
35
+ "sse",
36
+ "ai",
37
+ "openai",
38
+ "anthropic",
39
+ "framework",
40
+ "async-iterable"
53
41
  ],
54
- "author": "Qore Team",
55
- "license": "MIT",
42
+ "homepage": "https://github.com/qorejs/qore#readme",
56
43
  "repository": {
57
44
  "type": "git",
58
- "url": "https://github.com/qorejs/qore.git",
59
- "directory": "packages/core"
45
+ "url": "git+https://github.com/qorejs/qore.git"
60
46
  },
61
- "homepage": "https://github.com/qorejs/qore#readme",
62
47
  "bugs": {
63
48
  "url": "https://github.com/qorejs/qore/issues"
64
49
  },
65
- "devDependencies": {
66
- "@types/node": "^20.11.0",
67
- "jsdom": "^29.0.2",
68
- "typescript": "^5.4.0",
69
- "vite": "^5.2.0",
70
- "vite-plugin-dts": "^3.7.0",
71
- "vitest": "^1.4.0"
50
+ "publishConfig": {
51
+ "access": "public"
72
52
  },
73
- "engines": {
74
- "node": ">=18.0.0"
75
- }
53
+ "license": "MIT"
76
54
  }
@@ -0,0 +1,122 @@
1
+ import { createSSEAdapter, readEnv } from './sse.js';
2
+
3
+ const DEFAULT_BASE_URL = 'https://api.anthropic.com/v1';
4
+ const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
5
+ const DEFAULT_VERSION = '2023-06-01';
6
+ const DEFAULT_MAX_TOKENS = 1024;
7
+
8
+ // Normalize single-string prompts into Anthropic's Messages API shape.
9
+ function normalizeMessages(input) {
10
+ if (typeof input === 'string') {
11
+ return [{ role: 'user', content: input }];
12
+ }
13
+
14
+ if (Array.isArray(input)) {
15
+ return input;
16
+ }
17
+
18
+ if (input && typeof input === 'object' && 'role' in input) {
19
+ return [input];
20
+ }
21
+
22
+ return input;
23
+ }
24
+
25
+ // Keep provider setup explicit because real API keys should stay off the client.
26
+ export function createAnthropic(options = {}) {
27
+ const {
28
+ apiKey,
29
+ baseURL = DEFAULT_BASE_URL,
30
+ model = DEFAULT_MODEL,
31
+ version = DEFAULT_VERSION,
32
+ maxTokens = DEFAULT_MAX_TOKENS,
33
+ headers: defaultHeaders = {},
34
+ fetch: fetchImpl = globalThis.fetch
35
+ } = options;
36
+ const resolvedApiKey = apiKey ?? readEnv('ANTHROPIC_API_KEY');
37
+
38
+ if (!resolvedApiKey) {
39
+ throw new Error('Qore Anthropic adapter requires an API key. Pass apiKey or set ANTHROPIC_API_KEY.');
40
+ }
41
+
42
+ const transport = createSSEAdapter({
43
+ name: 'Anthropic',
44
+ url: `${baseURL}/messages`,
45
+ headers: {
46
+ 'content-type': 'application/json',
47
+ 'anthropic-version': version,
48
+ 'x-api-key': resolvedApiKey,
49
+ ...defaultHeaders
50
+ },
51
+ fetch: fetchImpl,
52
+ buildRequest(request, requestOptions = {}) {
53
+ const { signal, headers = {}, ...overrides } = requestOptions;
54
+
55
+ return {
56
+ method: 'POST',
57
+ signal,
58
+ headers,
59
+ body: JSON.stringify({
60
+ model,
61
+ max_tokens: maxTokens,
62
+ stream: true,
63
+ ...request,
64
+ ...overrides
65
+ })
66
+ };
67
+ },
68
+ parse: JSON.parse,
69
+ isError: (event) => event.data?.type === 'error',
70
+ getError: (event) => event.data?.error?.message ?? 'Anthropic streaming error',
71
+ eventToText: (event) => (
72
+ event.data?.type === 'content_block_delta'
73
+ && event.data.delta?.type === 'text_delta'
74
+ && typeof event.data.delta.text === 'string'
75
+ )
76
+ ? event.data.delta.text
77
+ : undefined
78
+ });
79
+
80
+ async function* streamEvents(request, requestOptions = {}) {
81
+ for await (const event of transport.stream(request, requestOptions)) {
82
+ yield event.data;
83
+ }
84
+ }
85
+
86
+ async function* streamText(messages, requestOptions = {}) {
87
+ const request = messages && typeof messages === 'object' && 'messages' in messages
88
+ ? messages
89
+ : { messages };
90
+
91
+ for await (const chunk of transport.streamText(request, requestOptions)) {
92
+ yield chunk;
93
+ }
94
+ }
95
+
96
+ return {
97
+ // Stream typed semantic events from the Messages API.
98
+ messages: {
99
+ stream: streamEvents
100
+ },
101
+
102
+ // Stream only text delta chunks from assistant content blocks.
103
+ streamText(messages, requestOptions = {}) {
104
+ return streamText(messages, requestOptions);
105
+ },
106
+
107
+ // Match the Qore narrative directly: stream(anthropic.chat(prompt)).
108
+ chat(input, requestOptions = {}) {
109
+ const {
110
+ signal,
111
+ headers,
112
+ ...request
113
+ } = requestOptions;
114
+
115
+ if (!('messages' in request)) {
116
+ request.messages = normalizeMessages(input);
117
+ }
118
+
119
+ return streamText(request, { signal, headers });
120
+ }
121
+ };
122
+ }
package/src/app.js ADDED
@@ -0,0 +1,123 @@
1
+ import {
2
+ dynamic,
3
+ fragment,
4
+ h,
5
+ list,
6
+ mount,
7
+ renderResponse,
8
+ show,
9
+ text
10
+ } from './dom.js';
11
+ import { batch, computed, effect, signal, untrack } from './signal.js';
12
+ import { response } from './response.js';
13
+ import { from, mapStream, scanStream, stream } from './stream.js';
14
+
15
+ // Resolve a CSS selector or direct node into the root mount target.
16
+ function resolveTarget(target) {
17
+ if (typeof document === 'undefined') {
18
+ throw new Error('Qore app mounting requires a browser-like environment');
19
+ }
20
+
21
+ if (typeof target === 'string') {
22
+ const element = document.querySelector(target);
23
+
24
+ if (!element) {
25
+ throw new Error(`Qore could not find a mount target for selector: ${target}`);
26
+ }
27
+
28
+ return element;
29
+ }
30
+
31
+ return target;
32
+ }
33
+
34
+ // Create a tiny application shell around Qore's lower-level primitives.
35
+ export function createApp(setup) {
36
+ let dispose = null;
37
+ let mountedRoot = null;
38
+ let cleanupHandlers = [];
39
+
40
+ const app = {
41
+ // Mount the app, provide framework primitives to setup, and render the resulting view.
42
+ mount(target, props = {}) {
43
+ const root = resolveTarget(target);
44
+ app.unmount();
45
+
46
+ cleanupHandlers = [];
47
+ mountedRoot = root;
48
+
49
+ // Expose the core runtime pieces so an app can stay entirely within Qore primitives.
50
+ const context = {
51
+ app,
52
+ root,
53
+ props,
54
+ signal,
55
+ computed,
56
+ effect,
57
+ batch,
58
+ untrack,
59
+ stream,
60
+ from,
61
+ mapStream,
62
+ scanStream,
63
+ response,
64
+ h,
65
+ text,
66
+ dynamic,
67
+ show,
68
+ list,
69
+ fragment,
70
+ renderResponse,
71
+ onCleanup(handler) {
72
+ if (typeof handler === 'function') {
73
+ cleanupHandlers.push(handler);
74
+ }
75
+
76
+ return handler;
77
+ }
78
+ };
79
+
80
+ // Allow setup to return either a raw view or an object with lifecycle hooks.
81
+ const result = setup(context);
82
+ const view = result && typeof result === 'object' && 'view' in result
83
+ ? result.view
84
+ : result;
85
+ const onMount = result && typeof result === 'object' ? result.onMount : null;
86
+
87
+ dispose = mount(root, view);
88
+
89
+ if (typeof onMount === 'function') {
90
+ onMount(root);
91
+ }
92
+
93
+ return root;
94
+ },
95
+
96
+ // Tear down the mounted tree and any user-registered cleanup handlers.
97
+ unmount() {
98
+ if (dispose) {
99
+ const stop = dispose;
100
+ dispose = null;
101
+ stop();
102
+ }
103
+
104
+ for (let index = cleanupHandlers.length - 1; index >= 0; index -= 1) {
105
+ try {
106
+ cleanupHandlers[index]();
107
+ } catch {
108
+ // Ignore user cleanup errors so the app can still unmount.
109
+ }
110
+ }
111
+
112
+ cleanupHandlers = [];
113
+ mountedRoot = null;
114
+ return app;
115
+ },
116
+
117
+ get root() {
118
+ return mountedRoot;
119
+ }
120
+ };
121
+
122
+ return app;
123
+ }