@qorejs/qore 0.7.1 → 0.7.3

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.
Files changed (86) hide show
  1. package/README.md +139 -3
  2. package/dist/src/core/iterable.d.ts +1 -0
  3. package/dist/src/core/iterable.js +17 -0
  4. package/dist/src/core/response-runtime.d.ts +2 -0
  5. package/dist/src/core/response-runtime.js +233 -0
  6. package/dist/src/core/response-state.d.ts +7 -0
  7. package/dist/src/core/response-state.js +102 -0
  8. package/dist/src/core/response-types.d.ts +73 -0
  9. package/dist/src/core/response-types.js +1 -0
  10. package/dist/src/core/response.d.ts +4 -0
  11. package/dist/src/core/response.js +26 -0
  12. package/dist/src/core/signal-context.d.ts +10 -0
  13. package/dist/src/core/signal-context.js +69 -0
  14. package/dist/src/core/signal-nodes.d.ts +45 -0
  15. package/dist/src/core/signal-nodes.js +197 -0
  16. package/dist/src/core/signal-scheduler.d.ts +2 -0
  17. package/dist/src/core/signal-scheduler.js +19 -0
  18. package/dist/src/core/signal-types.d.ts +19 -0
  19. package/dist/src/core/signal-types.js +1 -0
  20. package/dist/src/core/signal.d.ts +19 -0
  21. package/dist/src/core/signal.js +41 -0
  22. package/dist/src/core/stream-backpressure.d.ts +3 -0
  23. package/dist/src/core/stream-backpressure.js +36 -0
  24. package/dist/src/core/stream-buffer.d.ts +17 -0
  25. package/dist/src/core/stream-buffer.js +162 -0
  26. package/dist/src/core/stream-iterator.d.ts +6 -0
  27. package/dist/src/core/stream-iterator.js +14 -0
  28. package/dist/src/core/stream-lifecycle.d.ts +10 -0
  29. package/dist/src/core/stream-lifecycle.js +28 -0
  30. package/dist/src/core/stream-queue.d.ts +17 -0
  31. package/dist/src/core/stream-queue.js +62 -0
  32. package/dist/src/core/stream-runtime.d.ts +2 -0
  33. package/dist/src/core/stream-runtime.js +127 -0
  34. package/dist/src/core/stream-source.d.ts +2 -0
  35. package/dist/src/core/stream-source.js +28 -0
  36. package/dist/src/core/stream-state.d.ts +4 -0
  37. package/dist/src/core/stream-state.js +20 -0
  38. package/dist/src/core/stream-types.d.ts +61 -0
  39. package/dist/src/core/stream-types.js +1 -0
  40. package/dist/src/core/stream.d.ts +11 -0
  41. package/dist/src/core/stream.js +90 -0
  42. package/dist/src/dom/app.d.ts +38 -0
  43. package/dist/src/dom/app.js +102 -0
  44. package/dist/src/dom/dom.d.ts +13 -0
  45. package/dist/src/dom/dom.js +197 -0
  46. package/dist/src/dom/properties.d.ts +3 -0
  47. package/dist/src/dom/properties.js +147 -0
  48. package/dist/src/dom/reactive.d.ts +6 -0
  49. package/dist/src/dom/reactive.js +14 -0
  50. package/dist/src/dom/response-view.d.ts +4 -0
  51. package/dist/src/dom/response-view.js +37 -0
  52. package/dist/src/dom/scope.d.ts +10 -0
  53. package/dist/src/dom/scope.js +49 -0
  54. package/dist/src/dom/types.d.ts +34 -0
  55. package/dist/src/dom/types.js +1 -0
  56. package/dist/src/index.d.ts +16 -0
  57. package/dist/src/index.js +10 -0
  58. package/dist/src/providers/anthropic.d.ts +2 -0
  59. package/dist/src/providers/anthropic.js +102 -0
  60. package/dist/src/providers/openai.d.ts +2 -0
  61. package/dist/src/providers/openai.js +96 -0
  62. package/dist/src/providers/sse-adapter.d.ts +2 -0
  63. package/dist/src/providers/sse-adapter.js +83 -0
  64. package/dist/src/providers/sse-env.d.ts +4 -0
  65. package/dist/src/providers/sse-env.js +33 -0
  66. package/dist/src/providers/sse-parser.d.ts +5 -0
  67. package/dist/src/providers/sse-parser.js +99 -0
  68. package/dist/src/providers/sse.d.ts +4 -0
  69. package/dist/src/providers/sse.js +3 -0
  70. package/dist/src/providers/types.d.ts +94 -0
  71. package/dist/src/providers/types.js +1 -0
  72. package/dist/src/shared/utils.d.ts +2 -0
  73. package/dist/src/shared/utils.js +32 -0
  74. package/package.json +25 -11
  75. package/src/anthropic.js +0 -122
  76. package/src/app.js +0 -123
  77. package/src/dom.js +0 -527
  78. package/src/index.d.ts +0 -405
  79. package/src/index.js +0 -10
  80. package/src/iterable.js +0 -20
  81. package/src/openai.js +0 -112
  82. package/src/response.js +0 -312
  83. package/src/signal.js +0 -328
  84. package/src/sse.js +0 -264
  85. package/src/stream.js +0 -582
  86. package/src/utils.js +0 -39
package/README.md CHANGED
@@ -1,18 +1,57 @@
1
1
  # Qore
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/%40qorejs%2Fqore?color=0f766e&label=npm)](https://www.npmjs.com/package/@qorejs/qore)
4
+ [![latest release](https://img.shields.io/github/v/release/qorejs/qore?color=0f766e&label=release)](https://github.com/qorejs/qore/releases/latest)
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
+ [![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
+ [![node >= 18](https://img.shields.io/badge/node-%3E%3D18-1f6feb)](https://nodejs.org/)
11
+ [![license: MIT](https://img.shields.io/badge/license-MIT-0f766e)](https://github.com/qorejs/qore/blob/main/LICENSE)
12
+
3
13
  Qore is a streaming-response framework where `stream = signal`.
4
14
 
5
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.
6
16
 
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
+ - [landing page source](https://github.com/qorejs/qore/blob/main/index.html)
23
+ - [streaming demo source](https://github.com/qorejs/qore/blob/main/examples/streaming-response.html)
24
+ - [benchmark page](https://github.com/qorejs/qore/blob/main/examples/benchmark.html)
25
+ - [continuous integration](https://github.com/qorejs/qore/actions/workflows/ci.yml)
26
+ - [release workflow](https://github.com/qorejs/qore/actions/workflows/release-check.yml)
27
+ - [contributing guide](https://github.com/qorejs/qore/blob/main/CONTRIBUTING.md)
28
+ - [security policy](https://github.com/qorejs/qore/blob/main/SECURITY.md)
29
+
7
30
  ## Installation
8
31
 
32
+ From npm:
33
+
9
34
  ```bash
10
35
  npm i @qorejs/qore
11
36
  ```
12
37
 
38
+ From GitHub Packages:
39
+
40
+ ```bash
41
+ echo "@qorejs:registry=https://npm.pkg.github.com" >> .npmrc
42
+ npm i @qorejs/qore
43
+ ```
44
+
45
+ GitHub Packages installs require an authenticated session against `https://npm.pkg.github.com`.
46
+
13
47
  - Package name: `@qorejs/qore`
14
48
  - Module format: `ESM`
15
49
  - Supported runtime: `Node >= 18`
50
+ - CI coverage: `Node 18`, `20`, and `22`
51
+ - Browser regression: Playwright desktop + mobile smoke coverage for the homepage, focused demo, and benchmark page
52
+ - Registries:
53
+ - npm: [npmjs.com/package/@qorejs/qore](https://www.npmjs.com/package/@qorejs/qore)
54
+ - GitHub Packages: [github.com/qorejs/qore/packages](https://github.com/qorejs/qore/packages)
16
55
 
17
56
  ## Core Idea
18
57
 
@@ -36,6 +75,17 @@ Here, `answer` is all of the following at once:
36
75
  - An `AsyncIterable`, so you can still use `for await...of`
37
76
  - A lifecycle-aware streaming state, with `status()`, `streaming()`, `error()`, and `chunks()`
38
77
 
78
+ ## Performance Model
79
+
80
+ Qore keeps the streaming hot path narrow:
81
+
82
+ - chunk commits append into an internal log instead of cloning the full history on every token
83
+ - public `chunks()` reads still return defensive copies, so consumers cannot corrupt runtime state
84
+ - `chunkCount()` tracks the internal log version directly, so status UIs can stay cheap during long generations
85
+ - DOM bindings update only the nodes that read the stream signal
86
+
87
+ 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.
88
+
39
89
  ## Why Qore
40
90
 
41
91
  - React treats streaming as a special case that needs extra machinery
@@ -182,22 +232,108 @@ If your goal is to pipe a stream directly into the UI, prefer `stream(...)`.
182
232
  The repository includes a landing page and a focused streaming demo:
183
233
 
184
234
  - [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)
235
+ - [Homepage Logic](https://github.com/qorejs/qore/blob/main/examples/showcase.ts)
186
236
  - [Homepage Styles](https://github.com/qorejs/qore/blob/main/examples/showcase.css)
237
+ - [Benchmark Page](https://github.com/qorejs/qore/blob/main/examples/benchmark.html)
238
+ - [Benchmark Logic](https://github.com/qorejs/qore/blob/main/examples/benchmark-page.ts)
239
+ - [Benchmark Core](https://github.com/qorejs/qore/blob/main/examples/benchmark-core.ts)
187
240
  - [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)
241
+ - [Focused Chat Logic](https://github.com/qorejs/qore/blob/main/examples/qore-chat.ts)
242
+ - [React Compare](https://github.com/qorejs/qore/blob/main/examples/react-chat.ts)
243
+
244
+ ## Project Layout
245
+
246
+ ```text
247
+ src/
248
+ core/ stream, signal, response, iterable
249
+ dom/ app mounting and DOM bindings
250
+ providers/ OpenAI, Anthropic, generic SSE adapters
251
+ shared/ runtime utilities
252
+ index.ts public entrypoint
253
+
254
+ dist/
255
+ src/ compiled package output
256
+ examples/ built showcase scripts for local preview
257
+ test/ compiled test output
258
+ ```
190
259
 
191
260
  For a local preview:
192
261
 
193
262
  ```bash
194
263
  git clone git@github.com:qorejs/qore.git
195
264
  cd qore
265
+ npm install
266
+ npm run build
196
267
  python3 -m http.server 4173
197
268
  ```
198
269
 
199
270
  Then open [http://127.0.0.1:4173/](http://127.0.0.1:4173/).
200
271
 
272
+ ## Browser Regression
273
+
274
+ Install the browser binary once:
275
+
276
+ ```bash
277
+ npm run browsers:install
278
+ ```
279
+
280
+ Then run the browser smoke suite:
281
+
282
+ ```bash
283
+ npm run test:browser
284
+ ```
285
+
286
+ It validates:
287
+
288
+ - the homepage stream demo
289
+ - the focused streaming chat demo
290
+ - the dedicated benchmark page
291
+
292
+ The suite checks desktop and mobile layouts, watches for runtime console errors, exercises the primary interactions, and runs inside `release:check`.
293
+
294
+ CI also uploads the browser regression evidence as workflow artifacts. The bundle includes viewport screenshots, focused page-surface screenshots, the Playwright HTML report, and a `benchmark-suite.json` attachment from the dedicated benchmark page.
295
+
296
+ 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`.
297
+
298
+ Local preview ports can also be pinned when another process is already using the default range:
299
+
300
+ ```bash
301
+ QORE_STATIC_PORT=4300 QORE_STATIC_PORT_END=4400 npm run test:browser
302
+ ```
303
+
304
+ ## Benchmark Methodology
305
+
306
+ Qore now includes a reproducible browser benchmark that compares two rendering paths against the same workload:
307
+
308
+ - `Qore stream = signal`: mount the transcript shell once and advance the same live text node as chunks arrive
309
+ - `Snapshot rerender baseline`: rebuild the transcript shell from a snapshot string on every chunk
310
+
311
+ 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.
312
+
313
+ ## GitHub Packages
314
+
315
+ The repository includes GitHub Actions workflows for both release validation and GitHub Packages publishing.
316
+
317
+ - Release validation: `.github/workflows/release-check.yml`
318
+ - GitHub Packages publish: `.github/workflows/publish-github-packages.yml`
319
+ - Both workflows can be triggered manually from the Actions tab
320
+ - Publishing a GitHub Release triggers both the release check and the GitHub Packages publish flow
321
+ - The publish workflow validates the release tag, changelog, test suite, and tarball before it pushes the package
322
+ - The publish workflow uses the repository `GITHUB_TOKEN`, which GitHub documents as the recommended way to publish packages from the workflow repository
323
+ - GitHub Packages starts new packages as private by default, so you may want to switch the package visibility to public after the first publish
324
+
325
+ 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.
326
+
327
+ ## Project Hygiene
328
+
329
+ The repository also includes:
330
+
331
+ - `CONTRIBUTING.md` for contributor expectations and release flow
332
+ - `SECURITY.md` for responsible disclosure
333
+ - issue templates for bugs and feature requests
334
+ - a pull request template
335
+ - `.github/release.yml` to keep GitHub release notes structured
336
+
201
337
  ## Package Boundary
202
338
 
203
339
  Qore does not ship a built-in catalog of buttons, dialogs, tabs, or other UI primitives.
@@ -0,0 +1 @@
1
+ export declare function toAsyncIterable<T>(source: T | Iterable<T> | AsyncIterable<T> | null | undefined): AsyncIterable<T>;
@@ -0,0 +1,17 @@
1
+ // Normalize values, iterables, and async iterables into one async iterable shape.
2
+ export function toAsyncIterable(source) {
3
+ if (source == null) {
4
+ return (async function* empty() { })();
5
+ }
6
+ if (typeof source[Symbol.asyncIterator] === 'function') {
7
+ return source;
8
+ }
9
+ if (typeof source[Symbol.iterator] === 'function') {
10
+ return (async function* fromIterable() {
11
+ yield* source;
12
+ })();
13
+ }
14
+ return (async function* fromValue() {
15
+ yield source;
16
+ })();
17
+ }
@@ -0,0 +1,2 @@
1
+ import type { CreateResponseOptions, ResponseState } from './response-types.js';
2
+ export declare function createResponse<TChunk, TValue>(options: CreateResponseOptions<TChunk, TValue>): ResponseState<TChunk, TValue>;
@@ -0,0 +1,233 @@
1
+ import { batch } from './signal.js';
2
+ import { toAsyncIterable } from './iterable.js';
3
+ import { appendResponseChunk, createResponseState, getResponseChunkCount, isTerminalStatus, snapshotResponseChunks } from './response-state.js';
4
+ import { normalizeError } from '../shared/utils.js';
5
+ function isResponseSourceFactory(source) {
6
+ const callableSource = source;
7
+ return typeof source === 'function'
8
+ && typeof callableSource[Symbol.asyncIterator] !== 'function'
9
+ && typeof callableSource.peek !== 'function';
10
+ }
11
+ // Build a response state machine that accumulates chunks into a reactive value.
12
+ export function createResponse(options) {
13
+ const { seed, reduce } = options;
14
+ const state = createResponseState(seed);
15
+ const { status, value, error, chunks, startedAt, finishedAt, pending, streaming, completed, failed, aborted, chunkCount } = state;
16
+ let activeController = null;
17
+ let runId = 0;
18
+ let api;
19
+ // Abort the active executor when a new run supersedes it.
20
+ function supersedeActiveRun(reason = 'Response superseded by a new run') {
21
+ if (!activeController) {
22
+ return;
23
+ }
24
+ activeController.abort(reason);
25
+ activeController = null;
26
+ }
27
+ // Reset the response to its initial seed and clear all lifecycle markers.
28
+ function reset(nextSeed = seed) {
29
+ supersedeActiveRun('Response reset');
30
+ batch(() => {
31
+ status('idle');
32
+ value(nextSeed);
33
+ error(null);
34
+ chunks([]);
35
+ startedAt(null);
36
+ finishedAt(null);
37
+ });
38
+ return nextSeed;
39
+ }
40
+ // Push a chunk through the reducer and advance the response into streaming state.
41
+ function push(chunk) {
42
+ const currentStatus = status.peek();
43
+ if (isTerminalStatus(currentStatus)) {
44
+ return value.peek();
45
+ }
46
+ const index = getResponseChunkCount(chunks);
47
+ const nextValue = reduce(value.peek(), chunk, index);
48
+ batch(() => {
49
+ if (status.peek() === 'idle' || status.peek() === 'pending') {
50
+ status('streaming');
51
+ }
52
+ appendResponseChunk(chunks, chunk);
53
+ value(nextValue);
54
+ });
55
+ return nextValue;
56
+ }
57
+ // Mark the response as completed and freeze the current accumulated value.
58
+ function complete() {
59
+ const currentStatus = status.peek();
60
+ if (isTerminalStatus(currentStatus)) {
61
+ return value.peek();
62
+ }
63
+ activeController = null;
64
+ batch(() => {
65
+ status('completed');
66
+ finishedAt(Date.now());
67
+ });
68
+ return value.peek();
69
+ }
70
+ // Capture an error unless the response is already closed.
71
+ function fail(reason) {
72
+ const currentStatus = status.peek();
73
+ const normalizedError = normalizeError(reason);
74
+ if (isTerminalStatus(currentStatus)) {
75
+ return currentStatus === 'error'
76
+ ? error.peek() ?? normalizedError
77
+ : value.peek();
78
+ }
79
+ activeController = null;
80
+ batch(() => {
81
+ status('error');
82
+ error(normalizedError);
83
+ finishedAt(Date.now());
84
+ });
85
+ return normalizedError;
86
+ }
87
+ // Abort an in-flight response while keeping the value accumulated so far.
88
+ function abort(reason = 'Response aborted') {
89
+ const currentStatus = status.peek();
90
+ if (currentStatus !== 'pending' && currentStatus !== 'streaming') {
91
+ return value.peek();
92
+ }
93
+ activeController?.abort(reason);
94
+ activeController = null;
95
+ batch(() => {
96
+ status('aborted');
97
+ finishedAt(Date.now());
98
+ });
99
+ return value.peek();
100
+ }
101
+ // Run an async executor and guard every lifecycle method to the active run only.
102
+ async function run(executor, options = {}) {
103
+ const { resetValue = true, nextSeed = seed } = options;
104
+ supersedeActiveRun();
105
+ runId += 1;
106
+ const currentRunId = runId;
107
+ const controller = new AbortController();
108
+ activeController = controller;
109
+ // A response may outlive older executors, so every write must prove it still owns the run.
110
+ const isCurrentRun = () => (currentRunId === runId
111
+ && activeController === controller
112
+ && !controller.signal.aborted);
113
+ const settleExitedRun = () => {
114
+ if (currentRunId !== runId) {
115
+ return value.peek();
116
+ }
117
+ if (status.peek() === 'error') {
118
+ throw error.peek() ?? new Error('Response failed');
119
+ }
120
+ return value.peek();
121
+ };
122
+ batch(() => {
123
+ if (resetValue) {
124
+ value(nextSeed);
125
+ chunks([]);
126
+ }
127
+ error(null);
128
+ status('pending');
129
+ startedAt(Date.now());
130
+ finishedAt(null);
131
+ });
132
+ // Expose guarded lifecycle helpers so stale executors cannot leak writes into the latest run.
133
+ const context = {
134
+ get signal() {
135
+ return controller.signal;
136
+ },
137
+ response: api,
138
+ push(chunk) {
139
+ if (!isCurrentRun()) {
140
+ return value.peek();
141
+ }
142
+ return push(chunk);
143
+ },
144
+ complete() {
145
+ if (!isCurrentRun()) {
146
+ return value.peek();
147
+ }
148
+ return complete();
149
+ },
150
+ fail(reason) {
151
+ if (!isCurrentRun()) {
152
+ return status.peek() === 'error'
153
+ ? error.peek() ?? normalizeError(reason)
154
+ : value.peek();
155
+ }
156
+ return fail(reason);
157
+ },
158
+ abort(reason) {
159
+ if (!isCurrentRun()) {
160
+ return value.peek();
161
+ }
162
+ return abort(reason);
163
+ }
164
+ };
165
+ try {
166
+ await executor(context);
167
+ if (!isCurrentRun()) {
168
+ return settleExitedRun();
169
+ }
170
+ return complete();
171
+ }
172
+ catch (reason) {
173
+ if (!isCurrentRun()) {
174
+ return settleExitedRun();
175
+ }
176
+ throw fail(reason);
177
+ }
178
+ finally {
179
+ if (currentRunId === runId && activeController === controller) {
180
+ activeController = null;
181
+ }
182
+ }
183
+ }
184
+ // Consume any async iterable-like source and route each chunk through the guarded writer.
185
+ async function consume(source, options = {}) {
186
+ return run(async ({ signal: abortSignal, push: write }) => {
187
+ const resolvedSource = isResponseSourceFactory(source)
188
+ ? await source({ signal: abortSignal, response: api })
189
+ : await source;
190
+ for await (const chunk of toAsyncIterable(resolvedSource)) {
191
+ if (abortSignal.aborted) {
192
+ break;
193
+ }
194
+ write(chunk);
195
+ }
196
+ }, options);
197
+ }
198
+ // Return a plain snapshot suitable for inspection without exposing mutable internals.
199
+ function snapshot() {
200
+ return {
201
+ status: status.peek(),
202
+ value: value.peek(),
203
+ error: error.peek(),
204
+ chunks: snapshotResponseChunks(chunks),
205
+ startedAt: startedAt.peek(),
206
+ finishedAt: finishedAt.peek(),
207
+ chunkCount: getResponseChunkCount(chunks)
208
+ };
209
+ }
210
+ api = {
211
+ status,
212
+ value,
213
+ error,
214
+ chunks,
215
+ startedAt,
216
+ finishedAt,
217
+ pending,
218
+ streaming,
219
+ completed,
220
+ failed,
221
+ aborted,
222
+ chunkCount,
223
+ reset,
224
+ push,
225
+ complete,
226
+ fail,
227
+ abort,
228
+ run,
229
+ consume,
230
+ snapshot
231
+ };
232
+ return api;
233
+ }
@@ -0,0 +1,7 @@
1
+ import { type Signal } from './signal.js';
2
+ import type { ResponseReactiveState, ResponseStatus } from './response-types.js';
3
+ export declare function isTerminalStatus(currentStatus: ResponseStatus): boolean;
4
+ export declare function appendResponseChunk<T>(chunks: Signal<T[]>, chunk: T): number;
5
+ export declare function getResponseChunkCount<T>(chunks: Signal<T[]>): number;
6
+ export declare function snapshotResponseChunks<T>(chunks: Signal<T[]>): T[];
7
+ export declare function createResponseState<TChunk, TValue>(seed: TValue): ResponseReactiveState<TChunk, TValue>;
@@ -0,0 +1,102 @@
1
+ import { READ } from './signal-context.js';
2
+ import { computed, signal } from './signal.js';
3
+ // Treat these states as closed so late writes cannot mutate a finished response.
4
+ export function isTerminalStatus(currentStatus) {
5
+ return currentStatus === 'completed' || currentStatus === 'error' || currentStatus === 'aborted';
6
+ }
7
+ function isChunkSignal(value) {
8
+ return typeof value.append === 'function'
9
+ && typeof value.count === 'function'
10
+ && typeof value.peekCount === 'function'
11
+ && typeof value.snapshot === 'function';
12
+ }
13
+ // Store the live chunk log behind a version signal so token commits do not copy
14
+ // the full history on every push. Public reads still receive defensive copies.
15
+ function createChunkSignal() {
16
+ let items = [];
17
+ const version = signal(0);
18
+ const notify = () => version(version.peek() + 1);
19
+ const chunks = ((nextValue = READ) => {
20
+ if (nextValue === READ) {
21
+ version();
22
+ return [...items];
23
+ }
24
+ items = [...nextValue];
25
+ notify();
26
+ return [...items];
27
+ });
28
+ chunks.set = (nextValue) => {
29
+ items = [...nextValue];
30
+ notify();
31
+ return [...items];
32
+ };
33
+ chunks.update = (updater) => chunks.set(updater([...items]));
34
+ chunks.peek = () => [...items];
35
+ chunks.subscribe = (listener, options = {}) => {
36
+ const { immediate = true } = options;
37
+ if (immediate) {
38
+ listener([...items]);
39
+ }
40
+ return version.subscribe(() => listener([...items]), { immediate: false });
41
+ };
42
+ chunks.append = (chunk) => {
43
+ const index = items.length;
44
+ items.push(chunk);
45
+ notify();
46
+ return index;
47
+ };
48
+ chunks.count = () => {
49
+ version();
50
+ return items.length;
51
+ };
52
+ chunks.peekCount = () => items.length;
53
+ chunks.snapshot = () => [...items];
54
+ return chunks;
55
+ }
56
+ export function appendResponseChunk(chunks, chunk) {
57
+ if (isChunkSignal(chunks)) {
58
+ return chunks.append(chunk);
59
+ }
60
+ const currentChunks = chunks.peek();
61
+ const index = currentChunks.length;
62
+ chunks([...currentChunks, chunk]);
63
+ return index;
64
+ }
65
+ export function getResponseChunkCount(chunks) {
66
+ return isChunkSignal(chunks) ? chunks.peekCount() : chunks.peek().length;
67
+ }
68
+ export function snapshotResponseChunks(chunks) {
69
+ return isChunkSignal(chunks) ? chunks.snapshot() : [...chunks.peek()];
70
+ }
71
+ // Create the reactive state bundle that powers a response lifecycle.
72
+ export function createResponseState(seed) {
73
+ const status = signal('idle');
74
+ const value = signal(seed);
75
+ const error = signal(null);
76
+ const chunks = createChunkSignal();
77
+ const startedAt = signal(null);
78
+ const finishedAt = signal(null);
79
+ const pending = computed(() => {
80
+ const currentStatus = status();
81
+ return currentStatus === 'pending' || currentStatus === 'streaming';
82
+ });
83
+ const streaming = computed(() => status() === 'streaming');
84
+ const completed = computed(() => status() === 'completed');
85
+ const failed = computed(() => status() === 'error');
86
+ const aborted = computed(() => status() === 'aborted');
87
+ const chunkCount = computed(() => chunks.count());
88
+ return {
89
+ status,
90
+ value,
91
+ error,
92
+ chunks,
93
+ startedAt,
94
+ finishedAt,
95
+ pending,
96
+ streaming,
97
+ completed,
98
+ failed,
99
+ aborted,
100
+ chunkCount
101
+ };
102
+ }
@@ -0,0 +1,73 @@
1
+ import type { ComputedSignal, Signal } from './signal.js';
2
+ export type MaybePromise<T> = T | Promise<T>;
3
+ export type ResponseStatus = 'idle' | 'pending' | 'streaming' | 'completed' | 'error' | 'aborted';
4
+ export type GlobalAbortSignal = typeof globalThis extends {
5
+ AbortSignal: infer T;
6
+ } ? T extends {
7
+ prototype: infer P;
8
+ } ? P : unknown : {
9
+ aborted: boolean;
10
+ reason?: unknown;
11
+ };
12
+ export interface ResponseSnapshot<TChunk = unknown, TValue = unknown> {
13
+ status: ResponseStatus;
14
+ value: TValue;
15
+ error: Error | null;
16
+ chunks: TChunk[];
17
+ startedAt: number | null;
18
+ finishedAt: number | null;
19
+ chunkCount: number;
20
+ }
21
+ export interface ResponseRunOptions<TValue> {
22
+ resetValue?: boolean;
23
+ nextSeed?: TValue;
24
+ }
25
+ export type SourceLike<T> = T | Iterable<T> | AsyncIterable<T> | null | undefined;
26
+ export interface ResponseReactiveState<TChunk, TValue> {
27
+ status: Signal<ResponseStatus>;
28
+ value: Signal<TValue>;
29
+ error: Signal<Error | null>;
30
+ chunks: Signal<TChunk[]>;
31
+ startedAt: Signal<number | null>;
32
+ finishedAt: Signal<number | null>;
33
+ pending: ComputedSignal<boolean>;
34
+ streaming: ComputedSignal<boolean>;
35
+ completed: ComputedSignal<boolean>;
36
+ failed: ComputedSignal<boolean>;
37
+ aborted: ComputedSignal<boolean>;
38
+ chunkCount: ComputedSignal<number>;
39
+ }
40
+ export interface ResponseConsumeContext<TChunk, TValue> {
41
+ signal: GlobalAbortSignal;
42
+ response: ResponseState<TChunk, TValue>;
43
+ }
44
+ export interface ResponseExecutorContext<TChunk, TValue> {
45
+ readonly signal: GlobalAbortSignal;
46
+ response: ResponseState<TChunk, TValue>;
47
+ push(chunk: TChunk): TValue;
48
+ complete(): TValue;
49
+ fail(reason?: unknown): Error | TValue;
50
+ abort(reason?: unknown): TValue;
51
+ }
52
+ export type ResponseSourceFactory<TChunk, TValue> = (context: ResponseConsumeContext<TChunk, TValue>) => MaybePromise<SourceLike<TChunk>>;
53
+ export type ResponseSource<TChunk, TValue> = SourceLike<TChunk> | ResponseSourceFactory<TChunk, TValue>;
54
+ export interface CreateResponseOptions<TChunk, TValue> {
55
+ seed: TValue;
56
+ reduce: (currentValue: TValue, chunk: TChunk, index: number) => TValue;
57
+ }
58
+ export interface ResponseState<TChunk = unknown, TValue = unknown> extends ResponseReactiveState<TChunk, TValue> {
59
+ reset(nextSeed?: TValue): TValue;
60
+ push(chunk: TChunk): TValue;
61
+ complete(): TValue;
62
+ fail(reason?: unknown): Error | TValue;
63
+ abort(reason?: unknown): TValue;
64
+ run(executor: (context: ResponseExecutorContext<TChunk, TValue>) => MaybePromise<unknown>, options?: ResponseRunOptions<TValue>): Promise<TValue>;
65
+ consume(source: ResponseSource<TChunk, TValue>, options?: ResponseRunOptions<TValue>): Promise<TValue>;
66
+ snapshot(): ResponseSnapshot<TChunk, TValue>;
67
+ }
68
+ export interface ResponseFactory {
69
+ create<TChunk, TValue>(options: CreateResponseOptions<TChunk, TValue>): ResponseState<TChunk, TValue>;
70
+ text<TChunk = string>(seed?: string): ResponseState<TChunk, string>;
71
+ list<TChunk>(seed?: TChunk[]): ResponseState<TChunk, TChunk[]>;
72
+ latest<TChunk>(seed?: TChunk | null): ResponseState<TChunk, TChunk | null>;
73
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { ResponseFactory } from './response-types.js';
2
+ export { createResponse } from './response-runtime.js';
3
+ export type { CreateResponseOptions, GlobalAbortSignal, MaybePromise, ResponseConsumeContext, ResponseExecutorContext, ResponseFactory, ResponseReactiveState, ResponseRunOptions, ResponseSnapshot, ResponseSource, ResponseSourceFactory, ResponseState, ResponseStatus, SourceLike } from './response-types.js';
4
+ export declare const response: ResponseFactory;
@@ -0,0 +1,26 @@
1
+ import { createResponse } from './response-runtime.js';
2
+ // Ship a few common reducers as convenience constructors on top of createResponse.
3
+ export { createResponse } from './response-runtime.js';
4
+ export const response = {
5
+ create(options) {
6
+ return createResponse(options);
7
+ },
8
+ text(seed = '') {
9
+ return createResponse({
10
+ seed,
11
+ reduce: (currentValue, chunk) => currentValue + String(chunk)
12
+ });
13
+ },
14
+ list(seed = []) {
15
+ return createResponse({
16
+ seed,
17
+ reduce: (currentValue, chunk) => [...currentValue, chunk]
18
+ });
19
+ },
20
+ latest(seed = null) {
21
+ return createResponse({
22
+ seed,
23
+ reduce: (_, chunk) => chunk
24
+ });
25
+ }
26
+ };
@@ -0,0 +1,10 @@
1
+ import type { ReactiveObserver } from './signal-types.js';
2
+ export declare const READ: unique symbol;
3
+ export declare function getActiveObserver(): ReactiveObserver | null;
4
+ export declare function setActiveObserver(observer: ReactiveObserver | null): void;
5
+ export declare function withActiveObserver<T>(observer: ReactiveObserver | null, fn: () => T): T;
6
+ export declare function cleanupObserver(observer: ReactiveObserver): void;
7
+ export declare function scheduleObserver(observer: ReactiveObserver): void;
8
+ export declare function removePendingObserver(observer: ReactiveObserver): void;
9
+ export declare function batch<T>(fn: () => T): T;
10
+ export declare function untrack<T>(fn: () => T): T;