@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 +21 -0
- package/README.md +228 -0
- package/package.json +27 -49
- package/src/anthropic.js +122 -0
- package/src/app.js +123 -0
- package/src/dom.js +527 -0
- package/src/index.d.ts +405 -0
- package/src/index.js +10 -0
- package/src/iterable.js +20 -0
- package/src/openai.js +112 -0
- package/src/response.js +312 -0
- package/src/signal.js +328 -0
- package/src/sse.js +264 -0
- package/src/stream.js +582 -0
- package/src/utils.js +39 -0
- package/dist/index.js +0 -1193
- package/dist/index.js.map +0 -1
- package/src/component.ts +0 -26
- package/src/error.ts +0 -144
- package/src/index.ts +0 -95
- package/src/render.ts +0 -286
- package/src/signal.ts +0 -213
- package/src/ssr.ts +0 -341
- package/src/stream.ts +0 -432
- package/src/utils.ts +0 -116
- package/src/virtual-list.ts +0 -405
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.
|
|
4
|
-
"description": "Qore
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Qore is a streaming-response framework where stream becomes signal.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
9
8
|
"exports": {
|
|
10
9
|
".": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"import": "./src/index.js"
|
|
13
12
|
},
|
|
14
|
-
"./
|
|
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
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
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
|
-
"
|
|
43
|
-
"framework",
|
|
44
|
-
"ai-native",
|
|
45
|
-
"reactive",
|
|
46
|
-
"signals",
|
|
31
|
+
"stream",
|
|
47
32
|
"streaming",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
33
|
+
"signal",
|
|
34
|
+
"reactive",
|
|
35
|
+
"sse",
|
|
36
|
+
"ai",
|
|
37
|
+
"openai",
|
|
38
|
+
"anthropic",
|
|
39
|
+
"framework",
|
|
40
|
+
"async-iterable"
|
|
53
41
|
],
|
|
54
|
-
"
|
|
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
|
-
"
|
|
66
|
-
"
|
|
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
|
-
"
|
|
74
|
-
"node": ">=18.0.0"
|
|
75
|
-
}
|
|
53
|
+
"license": "MIT"
|
|
76
54
|
}
|
package/src/anthropic.js
ADDED
|
@@ -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
|
+
}
|