@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 +21 -0
- package/README.md +229 -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,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.
|
|
4
|
-
"description": "Qore
|
|
3
|
+
"version": "0.7.1",
|
|
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
|
+
}
|