@qorejs/qore 0.6.0 → 0.7.0

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,228 @@
1
+ # Qore
2
+
3
+ Qore 的灵魂只有四个字:`流式响应`。
4
+
5
+ 它不是把数据当快照,而是把数据当河流。token 一段一段地到来,UI 就应该一段一段地响应,不需要手动拼字符串,不需要到处补 `loading`,也不需要把 partial render 当成特例处理。
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` 是数据流动的方式,`signal` 是 UI 响应变化的方式。
20
+
21
+ 在 Qore 里,这两者是同一个 primitive 的两面:
22
+
23
+ ```js
24
+ import { createOpenAI, h, stream, text } from '@qorejs/qore';
25
+
26
+ const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
27
+ const answer = stream(openai.chat('hello'));
28
+
29
+ return h('div', {}, text(() => answer()));
30
+ ```
31
+
32
+ 这里的 `answer` 同时是:
33
+
34
+ - 一个只读 `signal`,`answer()` 拿到当前累积值
35
+ - 一个 `AsyncIterable`,可以继续 `for await...of`
36
+ - 一个带生命周期的流状态,支持 `status()`、`streaming()`、`error()`、`chunks()`
37
+
38
+ ## Why Qore
39
+
40
+ - React 把 stream 当成特殊情况,需要额外心智去补
41
+ - SolidJS 的 signal 很强,但没有原生 stream primitive
42
+ - Vue 的 ref 很顺手,但 stream 依旧是外置概念
43
+ - Qore 直接把 `stream = signal` 做成核心 API
44
+
45
+ ## Quick Start
46
+
47
+ ```js
48
+ import { h, mount, stream, text } from '@qorejs/qore';
49
+
50
+ const answer = stream(async function* () {
51
+ yield '流';
52
+ yield '式';
53
+ yield '响应';
54
+ }());
55
+
56
+ mount('#app', () => h('div', { className: 'answer' }, text(() => answer())));
57
+ ```
58
+
59
+ 上面这个例子只会更新那一个 text node,不会 whole tree 重绘。
60
+
61
+ ## Providers
62
+
63
+ ### `createOpenAI(options?)`
64
+
65
+ ```js
66
+ import { createOpenAI, stream } from '@qorejs/qore';
67
+
68
+ const openai = createOpenAI({
69
+ apiKey: process.env.OPENAI_API_KEY,
70
+ model: 'gpt-5'
71
+ });
72
+
73
+ const answer = stream(openai.chat('Why should stream be signal?'));
74
+ ```
75
+
76
+ ### `createAnthropic(options?)`
77
+
78
+ ```js
79
+ import { createAnthropic, stream } from '@qorejs/qore';
80
+
81
+ const anthropic = createAnthropic({
82
+ apiKey: process.env.ANTHROPIC_API_KEY,
83
+ model: 'claude-sonnet-4-20250514'
84
+ });
85
+
86
+ const answer = stream(anthropic.chat('Why should stream be signal?'));
87
+ ```
88
+
89
+ ### `createSSEAdapter(options?)`
90
+
91
+ 如果你的后端本来就已经在吐 SSE,Qore 也可以直接把它收编进同一个 story:
92
+
93
+ ```js
94
+ import { createSSEAdapter, stream } from '@qorejs/qore';
95
+
96
+ const provider = createSSEAdapter({
97
+ name: 'Local Chat',
98
+ url: 'http://localhost:3000/api/chat',
99
+ buildRequest(request) {
100
+ return {
101
+ method: 'POST',
102
+ body: JSON.stringify(request)
103
+ };
104
+ },
105
+ buildChatRequest(input) {
106
+ return { prompt: input };
107
+ },
108
+ eventToText(event) {
109
+ return event.data?.type === 'token' ? event.data.text : undefined;
110
+ }
111
+ });
112
+
113
+ const answer = stream(provider.chat('hello'));
114
+ ```
115
+
116
+ 这让 `stream(provider.chat(...))` 不再绑定某一家 SDK,而是成为一个通用入口。
117
+
118
+ ## API Shape
119
+
120
+ ### `stream(source, options?)`
121
+
122
+ 默认把 chunk 累积成文本 signal:
123
+
124
+ ```js
125
+ const answer = stream(openai.chat('hello'));
126
+
127
+ answer(); // 当前文本
128
+ answer.status(); // idle | pending | streaming | completed | error | aborted
129
+ answer.streaming(); // boolean
130
+ answer.chunks(); // 原始 chunk 列表
131
+ await answer.ready; // 等待结束
132
+ ```
133
+
134
+ 如果你需要结构化流:
135
+
136
+ ```js
137
+ const events = stream.list(eventSource);
138
+ const latest = stream.latest(modelEvents);
139
+ ```
140
+
141
+ ### Backpressure
142
+
143
+ ```js
144
+ const answer = stream.withBackpressure(openai.chat('hello'), {
145
+ interval: 16,
146
+ buffer: 8,
147
+ overflow: 'drop-oldest'
148
+ });
149
+ ```
150
+
151
+ backpressure 现在不只是“睡一下”:
152
+
153
+ - `interval`:chunk 进入 signal / UI 之间的最小间隔
154
+ - `buffer`:在 UI 前面最多允许排队多少个 chunk
155
+ - `overflow`:缓冲区满了以后怎么办,可选 `wait` / `drop-oldest` / `drop-newest` / `error`
156
+
157
+ 你还可以直接观察压力状态:
158
+
159
+ ```js
160
+ answer.buffered(); // 当前还有多少 chunk 在排队
161
+ answer.dropped(); // 因 overflow 策略被丢掉了多少 chunk
162
+ ```
163
+
164
+ ### `signal`, `computed`, `effect`
165
+
166
+ ```js
167
+ import { computed, signal, stream } from '@qorejs/qore';
168
+
169
+ const answer = stream(openai.chat('hello'));
170
+ const length = computed(() => answer().length);
171
+ ```
172
+
173
+ ### `response`
174
+
175
+ `response` 仍然保留,但它更像底层状态机 escape hatch,适合复杂 reducer 或自定义聚合。
176
+
177
+ 如果你的目标是“把流直接接进 UI”,优先使用 `stream(...)`。
178
+
179
+ ## Demos
180
+
181
+ 仓库里带了完整 landing page 和 focused demo:
182
+
183
+ - [Landing Page Source](https://github.com/qorejs/qore/blob/main/index.html)
184
+ - [Homepage Logic](https://github.com/qorejs/qore/blob/main/examples/showcase.js)
185
+ - [Homepage Styles](https://github.com/qorejs/qore/blob/main/examples/showcase.css)
186
+ - [Focused Demo](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
187
+ - [Focused Chat Logic](https://github.com/qorejs/qore/blob/main/examples/qore-chat.js)
188
+ - [React Compare](https://github.com/qorejs/qore/blob/main/examples/react-chat.jsx)
189
+
190
+ 本地预览:
191
+
192
+ ```bash
193
+ git clone git@github.com:qorejs/qore.git
194
+ cd qore
195
+ python3 -m http.server 4173
196
+ ```
197
+
198
+ 然后打开 [http://127.0.0.1:4173/](http://127.0.0.1:4173/)。
199
+
200
+ ## Package Boundary
201
+
202
+ Qore 核心包不内置 Button、Dialog、Tabs 这类 UI primitives。
203
+
204
+ 核心包只做三件事:
205
+
206
+ - 让流进入状态
207
+ - 让状态进入 UI
208
+ - 让整个过程保持细粒度响应
209
+
210
+ 一切不服务于 `流式响应` 的东西,都应该放到实验层或者外围仓库。
211
+
212
+ ## Testing
213
+
214
+ ```bash
215
+ npm test
216
+ ```
217
+
218
+ 当前测试覆盖了:
219
+
220
+ - signal / computed / effect
221
+ - stream = signal 的核心行为
222
+ - response 与 async iterable 的兼容
223
+ - OpenAI / Anthropic / generic SSE adapters
224
+
225
+ ## Roadmap
226
+
227
+ - 围绕服务端流式渲染收敛 hydration 模型
228
+ - 做公开 benchmark,把 Qore 和 React/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.0",
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
+ }