@qorejs/qore 0.7.2 → 0.9.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.
Files changed (47) hide show
  1. package/README.md +294 -5
  2. package/dist/src/core/owner.d.ts +14 -0
  3. package/dist/src/core/owner.js +66 -0
  4. package/dist/src/core/response-runtime.js +5 -5
  5. package/dist/src/core/response-state.d.ts +4 -0
  6. package/dist/src/core/response-state.js +67 -2
  7. package/dist/src/core/signal-context.js +19 -9
  8. package/dist/src/core/signal-nodes.d.ts +5 -0
  9. package/dist/src/core/signal-nodes.js +51 -21
  10. package/dist/src/core/signal-types.d.ts +2 -0
  11. package/dist/src/core/signal.d.ts +1 -0
  12. package/dist/src/core/signal.js +2 -0
  13. package/dist/src/core/stream-types.d.ts +12 -0
  14. package/dist/src/core/stream.d.ts +1 -1
  15. package/dist/src/core/stream.js +144 -0
  16. package/dist/src/dom/app.js +2 -3
  17. package/dist/src/dom/dom.d.ts +1 -0
  18. package/dist/src/dom/dom.js +96 -9
  19. package/dist/src/dom/scope.d.ts +3 -1
  20. package/dist/src/dom/scope.js +10 -3
  21. package/dist/src/index.d.ts +10 -4
  22. package/dist/src/index.js +7 -2
  23. package/dist/src/providers/anthropic.js +6 -4
  24. package/dist/src/providers/deepseek.d.ts +2 -0
  25. package/dist/src/providers/deepseek.js +90 -0
  26. package/dist/src/providers/line-adapter.d.ts +2 -0
  27. package/dist/src/providers/line-adapter.js +83 -0
  28. package/dist/src/providers/line-parser.d.ts +5 -0
  29. package/dist/src/providers/line-parser.js +103 -0
  30. package/dist/src/providers/metadata.d.ts +8 -0
  31. package/dist/src/providers/metadata.js +193 -0
  32. package/dist/src/providers/ollama.d.ts +2 -0
  33. package/dist/src/providers/ollama.js +82 -0
  34. package/dist/src/providers/openai.js +6 -4
  35. package/dist/src/providers/openrouter.d.ts +2 -0
  36. package/dist/src/providers/openrouter.js +90 -0
  37. package/dist/src/providers/sse-adapter.js +129 -44
  38. package/dist/src/providers/sse-parser.d.ts +1 -1
  39. package/dist/src/providers/sse-parser.js +74 -38
  40. package/dist/src/providers/sse.d.ts +7 -1
  41. package/dist/src/providers/sse.js +6 -0
  42. package/dist/src/providers/types.d.ts +145 -0
  43. package/dist/src/server/sse-response.d.ts +16 -0
  44. package/dist/src/server/sse-response.js +102 -0
  45. package/dist/src/shared/utils.d.ts +1 -0
  46. package/dist/src/shared/utils.js +8 -1
  47. package/package.json +9 -2
@@ -1,6 +1,7 @@
1
1
  import type { Cleanup, EffectCallback, EffectOptions, ObserverDependency, ReactiveObserver, SignalListener, SubscribeOptions } from './signal-types.js';
2
2
  export declare class SignalNode<T> implements ObserverDependency {
3
3
  value: T;
4
+ level: number;
4
5
  subscribers: Set<ReactiveObserver>;
5
6
  listeners: Set<SignalListener<T>>;
6
7
  constructor(initialValue: T);
@@ -13,6 +14,8 @@ export declare class SignalNode<T> implements ObserverDependency {
13
14
  }
14
15
  export declare class ComputedNode<T> implements ObserverDependency, ReactiveObserver {
15
16
  getter: () => T;
17
+ private owner;
18
+ level: number;
16
19
  subscribers: Set<ReactiveObserver>;
17
20
  listeners: Set<SignalListener<T>>;
18
21
  deps: Set<ObserverDependency>;
@@ -31,6 +34,8 @@ export declare class ComputedNode<T> implements ObserverDependency, ReactiveObse
31
34
  export declare class EffectNode implements ReactiveObserver {
32
35
  fn: EffectCallback;
33
36
  scheduler: EffectOptions['scheduler'];
37
+ private owner;
38
+ level: number;
34
39
  deps: Set<ObserverDependency>;
35
40
  active: boolean;
36
41
  scheduled: boolean;
@@ -1,8 +1,10 @@
1
- import { cleanupObserver, getActiveObserver, removePendingObserver, scheduleObserver, withActiveObserver } from './signal-context.js';
1
+ import { batch, cleanupObserver, getActiveObserver, removePendingObserver, scheduleObserver, withActiveObserver } from './signal-context.js';
2
+ import { createOwnedScope, disposeOwner, onCleanup, resetOwner, withOwner } from './owner.js';
2
3
  import { scheduleEffectRun } from './signal-scheduler.js';
3
4
  // A mutable signal node stores a value and fan-outs updates to listeners and observers.
4
5
  export class SignalNode {
5
6
  value;
7
+ level = 0;
6
8
  subscribers = new Set();
7
9
  listeners = new Set();
8
10
  constructor(initialValue) {
@@ -45,14 +47,18 @@ export class SignalNode {
45
47
  for (const listener of Array.from(this.listeners)) {
46
48
  listener(this.value);
47
49
  }
48
- for (const observer of Array.from(this.subscribers)) {
49
- scheduleObserver(observer);
50
- }
50
+ batch(() => {
51
+ for (const observer of Array.from(this.subscribers)) {
52
+ scheduleObserver(observer);
53
+ }
54
+ });
51
55
  }
52
56
  }
53
57
  // A computed node re-runs its getter whenever one of its dependencies changes.
54
58
  export class ComputedNode {
55
59
  getter;
60
+ owner = createOwnedScope();
61
+ level = 1;
56
62
  subscribers = new Set();
57
63
  listeners = new Set();
58
64
  deps = new Set();
@@ -61,6 +67,7 @@ export class ComputedNode {
61
67
  value;
62
68
  constructor(getter) {
63
69
  this.getter = getter;
70
+ onCleanup(() => this.stop());
64
71
  this.recompute();
65
72
  }
66
73
  get() {
@@ -96,11 +103,16 @@ export class ComputedNode {
96
103
  return;
97
104
  }
98
105
  const previousValue = this.value;
106
+ resetOwner(this.owner);
99
107
  cleanupObserver(this);
100
- withActiveObserver(this, () => {
108
+ withOwner(this.owner, () => withActiveObserver(this, () => {
101
109
  const nextValue = this.getter();
102
110
  const changed = !this.initialized || !Object.is(previousValue, nextValue);
111
+ const nextLevel = this.deps.size > 0
112
+ ? Math.max(...Array.from(this.deps, (dependency) => dependency.level)) + 1
113
+ : 1;
103
114
  this.value = nextValue;
115
+ this.level = nextLevel;
104
116
  this.initialized = true;
105
117
  if (!changed) {
106
118
  return;
@@ -108,10 +120,12 @@ export class ComputedNode {
108
120
  for (const listener of Array.from(this.listeners)) {
109
121
  listener(this.value);
110
122
  }
111
- for (const observer of Array.from(this.subscribers)) {
112
- scheduleObserver(observer);
113
- }
114
- });
123
+ batch(() => {
124
+ for (const observer of Array.from(this.subscribers)) {
125
+ scheduleObserver(observer);
126
+ }
127
+ });
128
+ }));
115
129
  }
116
130
  stop() {
117
131
  if (!this.active) {
@@ -120,6 +134,7 @@ export class ComputedNode {
120
134
  this.active = false;
121
135
  cleanupObserver(this);
122
136
  removePendingObserver(this);
137
+ disposeOwner(this.owner);
123
138
  this.subscribers.clear();
124
139
  this.listeners.clear();
125
140
  }
@@ -128,6 +143,8 @@ export class ComputedNode {
128
143
  export class EffectNode {
129
144
  fn;
130
145
  scheduler;
146
+ owner = createOwnedScope();
147
+ level = 1;
131
148
  deps = new Set();
132
149
  active = true;
133
150
  scheduled = false;
@@ -137,6 +154,7 @@ export class EffectNode {
137
154
  constructor(fn, options = {}) {
138
155
  this.fn = fn;
139
156
  this.scheduler = options.scheduler ?? 'sync';
157
+ onCleanup(() => this.stop());
140
158
  this.run();
141
159
  }
142
160
  schedule() {
@@ -166,18 +184,29 @@ export class EffectNode {
166
184
  }
167
185
  this.running = true;
168
186
  this.needsRun = false;
169
- cleanupObserver(this);
170
- if (typeof this.cleanup === 'function') {
171
- this.cleanup();
172
- this.cleanup = null;
173
- }
174
- withActiveObserver(this, () => {
175
- const maybeCleanup = this.fn();
176
- this.cleanup = typeof maybeCleanup === 'function' ? maybeCleanup : null;
177
- });
178
- this.running = false;
179
- if (this.needsRun && this.active) {
180
- this.schedule();
187
+ const previousCleanup = this.cleanup;
188
+ this.cleanup = null;
189
+ try {
190
+ // Run the previous cleanup before dropping old dependencies so a thrown cleanup
191
+ // does not silently unsubscribe the effect from future source updates.
192
+ if (typeof previousCleanup === 'function') {
193
+ previousCleanup();
194
+ }
195
+ resetOwner(this.owner);
196
+ cleanupObserver(this);
197
+ withOwner(this.owner, () => withActiveObserver(this, () => {
198
+ const maybeCleanup = this.fn();
199
+ this.cleanup = typeof maybeCleanup === 'function' ? maybeCleanup : null;
200
+ }));
201
+ this.level = this.deps.size > 0
202
+ ? Math.max(...Array.from(this.deps, (dependency) => dependency.level)) + 1
203
+ : 1;
204
+ }
205
+ finally {
206
+ this.running = false;
207
+ if (this.needsRun && this.active) {
208
+ this.schedule();
209
+ }
181
210
  }
182
211
  }
183
212
  stop() {
@@ -189,6 +218,7 @@ export class EffectNode {
189
218
  this.needsRun = false;
190
219
  cleanupObserver(this);
191
220
  removePendingObserver(this);
221
+ disposeOwner(this.owner);
192
222
  if (typeof this.cleanup === 'function') {
193
223
  this.cleanup();
194
224
  this.cleanup = null;
@@ -9,11 +9,13 @@ export interface EffectOptions {
9
9
  scheduler?: EffectScheduler;
10
10
  }
11
11
  export interface ObserverDependency {
12
+ level: number;
12
13
  subscribers: Set<ReactiveObserver>;
13
14
  }
14
15
  export interface ReactiveObserver {
15
16
  deps: Set<ObserverDependency>;
16
17
  active: boolean;
18
+ level: number;
17
19
  schedule(): void;
18
20
  notify(): void;
19
21
  }
@@ -16,4 +16,5 @@ export declare function signal<T>(initialValue: T): Signal<T>;
16
16
  export declare function computed<T>(getter: () => T): ComputedSignal<T>;
17
17
  export declare function effect(fn: EffectCallback, options?: EffectOptions): Cleanup;
18
18
  export { batch, untrack } from './signal-context.js';
19
+ export { createRoot, onCleanup } from './owner.js';
19
20
  export declare function isSignal<T = unknown>(value: unknown): value is ReadonlySignal<T>;
@@ -1,3 +1,4 @@
1
+ import { createRoot, onCleanup } from './owner.js';
1
2
  import { batch, READ, untrack } from './signal-context.js';
2
3
  import { ComputedNode, EffectNode, SignalNode } from './signal-nodes.js';
3
4
  // Create a mutable signal function with helper methods attached to it.
@@ -35,6 +36,7 @@ export function effect(fn, options = {}) {
35
36
  return () => node.stop();
36
37
  }
37
38
  export { batch, untrack } from './signal-context.js';
39
+ export { createRoot, onCleanup } from './owner.js';
38
40
  // Detect Qore signal-like values by their callable shape plus peek helper.
39
41
  export function isSignal(value) {
40
42
  return typeof value === 'function' && typeof value.peek === 'function';
@@ -6,6 +6,7 @@ export interface BackpressureOptions {
6
6
  buffer?: number;
7
7
  overflow?: OverflowStrategy;
8
8
  }
9
+ export type RetryBackoff = number | number[] | 'exponential' | ((retry: number, error: unknown) => MaybePromise<number>);
9
10
  export interface NormalizedBackpressure {
10
11
  interval: number;
11
12
  buffer: number;
@@ -21,6 +22,11 @@ export interface StreamOptions<TChunk = unknown, TValue = string> {
21
22
  reduce?: (currentValue: TValue, chunk: TChunk, index: number) => TValue;
22
23
  backpressure?: number | BackpressureOptions | null;
23
24
  }
25
+ export interface RetryableStreamOptions<TChunk = unknown, TValue = string> extends StreamOptions<TChunk, TValue> {
26
+ maxRetries?: number;
27
+ backoff?: RetryBackoff;
28
+ }
29
+ export type StreamPipeStage<TChunk = unknown, TValue = string> = (value: TValue, index: number) => MaybePromise<StreamInput<TChunk, TValue>>;
24
30
  export interface StreamController<TChunk = unknown, TValue = string> {
25
31
  readonly signal: GlobalAbortSignal;
26
32
  push(chunk: TChunk): Promise<TValue>;
@@ -57,5 +63,11 @@ export interface StreamFactory {
57
63
  latest<TChunk>(sourceOrSetup: StreamInput<TChunk, TChunk | null>, options?: StreamOptions<TChunk, TChunk | null>): QoreStream<TChunk, TChunk | null>;
58
64
  withBackpressure<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, backpressure: number | BackpressureOptions, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
59
65
  paced<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, interval: number, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
66
+ merge<TChunk = unknown, TValue = string>(sources: Array<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
67
+ concat<TChunk = unknown, TValue = string>(sources: Array<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
68
+ pipe<TChunk = unknown, TValue = string>(sourceOrSetup: StreamInput<TChunk, TValue>, stages: Array<StreamPipeStage<TChunk, TValue>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
69
+ race<TChunk = unknown, TValue = string>(sources: Array<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
70
+ retryable<TChunk = unknown, TValue = string>(sourceFactory: (attempt: number) => StreamInput<TChunk, TValue>, options?: RetryableStreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
71
+ switchMap<TInput, TChunk = unknown, TValue = string>(source: SourceLike<TInput>, mapper: (value: TInput, index: number) => MaybePromise<SourceLike<TChunk>>, options?: StreamOptions<TChunk, TValue>): QoreStream<TChunk, TValue>;
60
72
  }
61
73
  export type StreamResponseState<TChunk, TValue> = ResponseState<TChunk, TValue>;
@@ -7,5 +7,5 @@ export declare function from<TChunk, TValue = string>(source: SourceLike<TChunk>
7
7
  export declare function mapStream<TInput, TOutput, TValue = string>(source: SourceLike<TInput>, mapper: (chunk: TInput, index: number) => MaybePromise<TOutput>, options?: StreamOptions<TOutput, TValue>): QoreStream<TOutput, TValue>;
8
8
  export declare function scanStream<TInput, TOutput>(source: SourceLike<TInput>, reducer: (currentValue: TOutput, chunk: TInput, index: number) => MaybePromise<TOutput>, seed: TOutput, options?: Omit<StreamOptions<TOutput, TOutput>, 'seed' | 'reduce'>): QoreStream<TOutput, TOutput>;
9
9
  export { createStream } from './stream-runtime.js';
10
- export type { BackpressureOptions, QoreStream, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './stream-types.js';
10
+ export type { BackpressureOptions, QoreStream, RetryBackoff, RetryableStreamOptions, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './stream-types.js';
11
11
  export { toAsyncIterable } from './iterable.js';
@@ -1,5 +1,6 @@
1
1
  import { toAsyncIterable } from './iterable.js';
2
2
  import { createStream } from './stream-runtime.js';
3
+ import { startSource } from './stream-source.js';
3
4
  import { reduceText } from './stream-state.js';
4
5
  import { sleep } from '../shared/utils.js';
5
6
  const streamFactory = ((sourceOrSetup, options = {}) => createStream(sourceOrSetup, {
@@ -36,8 +37,151 @@ streamFactory.withBackpressure = (sourceOrSetup, backpressure, options = {}) =>
36
37
  });
37
38
  // Shorthand for the first backpressure primitive: minimum interval between chunks.
38
39
  streamFactory.paced = (sourceOrSetup, interval, options = {}) => streamFactory.withBackpressure(sourceOrSetup, { interval }, options);
40
+ streamFactory.merge = (sources, options = {}) => createStream(async (controller) => {
41
+ await Promise.all(sources.map(async (source) => {
42
+ await startSource(source, controller);
43
+ }));
44
+ }, options);
45
+ streamFactory.concat = (sources, options = {}) => createStream(async (controller) => {
46
+ for (const source of sources) {
47
+ if (controller.signal.aborted) {
48
+ break;
49
+ }
50
+ await startSource(source, controller);
51
+ }
52
+ }, options);
53
+ streamFactory.pipe = (sourceOrSetup, stages, options = {}) => createStream(async (controller) => {
54
+ const consumeSource = async (currentSource) => {
55
+ const currentStream = createStream(currentSource, options);
56
+ for await (const chunk of currentStream) {
57
+ if (controller.signal.aborted) {
58
+ break;
59
+ }
60
+ await controller.push(chunk);
61
+ }
62
+ return currentStream.ready;
63
+ };
64
+ let currentValue = await consumeSource(sourceOrSetup);
65
+ for (const [index, stage] of stages.entries()) {
66
+ if (controller.signal.aborted) {
67
+ break;
68
+ }
69
+ const nextSource = await stage(currentValue, index);
70
+ currentValue = await consumeSource(nextSource);
71
+ }
72
+ }, options);
73
+ streamFactory.race = (sources, options = {}) => createStream(async (controller) => {
74
+ const activeIterators = sources.map((source, index) => ({
75
+ index,
76
+ iterator: toAsyncIterable(source)[Symbol.asyncIterator]()
77
+ }));
78
+ let winner = null;
79
+ try {
80
+ while (!controller.signal.aborted && winner === null && activeIterators.length > 0) {
81
+ const nextResult = await Promise.race(activeIterators.map(async ({ index, iterator }) => ({
82
+ index,
83
+ result: await iterator.next()
84
+ })));
85
+ if (nextResult.result.done) {
86
+ const exhaustedIndex = activeIterators.findIndex(({ index }) => index === nextResult.index);
87
+ if (exhaustedIndex >= 0) {
88
+ activeIterators.splice(exhaustedIndex, 1);
89
+ }
90
+ continue;
91
+ }
92
+ winner = activeIterators.find(({ index }) => index === nextResult.index)?.iterator ?? null;
93
+ await controller.push(nextResult.result.value);
94
+ }
95
+ if (!winner || controller.signal.aborted) {
96
+ return;
97
+ }
98
+ for (const candidate of activeIterators) {
99
+ if (candidate.iterator !== winner) {
100
+ await candidate.iterator.return?.();
101
+ }
102
+ }
103
+ while (!controller.signal.aborted) {
104
+ const nextChunk = await winner.next();
105
+ if (nextChunk.done) {
106
+ break;
107
+ }
108
+ await controller.push(nextChunk.value);
109
+ }
110
+ }
111
+ finally {
112
+ for (const { iterator } of activeIterators) {
113
+ if (iterator !== winner) {
114
+ await iterator.return?.();
115
+ }
116
+ }
117
+ }
118
+ }, options);
119
+ streamFactory.retryable = (sourceFactory, options = {}) => {
120
+ const { maxRetries = 0, backoff = 'exponential', ...streamOptions } = options;
121
+ return createStream(async (controller) => {
122
+ let retries = 0;
123
+ while (!controller.signal.aborted) {
124
+ try {
125
+ await startSource(sourceFactory(retries), controller);
126
+ return;
127
+ }
128
+ catch (error) {
129
+ if (retries >= maxRetries || controller.signal.aborted) {
130
+ throw error;
131
+ }
132
+ retries += 1;
133
+ const delay = await resolveRetryDelay(backoff, retries, error);
134
+ if (delay > 0) {
135
+ await sleep(delay, controller.signal);
136
+ }
137
+ }
138
+ }
139
+ }, streamOptions);
140
+ };
141
+ streamFactory.switchMap = (source, mapper, options = {}) => createStream(async (controller) => {
142
+ let index = 0;
143
+ let activeToken = 0;
144
+ let activeTask = null;
145
+ const startInner = (token, innerSource) => (async () => {
146
+ for await (const chunk of toAsyncIterable(innerSource)) {
147
+ if (controller.signal.aborted || token !== activeToken) {
148
+ break;
149
+ }
150
+ await controller.push(chunk);
151
+ }
152
+ })().catch((error) => {
153
+ if (token !== activeToken || controller.signal.aborted) {
154
+ return;
155
+ }
156
+ throw error;
157
+ });
158
+ for await (const value of toAsyncIterable(source)) {
159
+ if (controller.signal.aborted) {
160
+ break;
161
+ }
162
+ activeToken += 1;
163
+ const token = activeToken;
164
+ const innerSource = await mapper(value, index);
165
+ activeTask = startInner(token, innerSource);
166
+ index += 1;
167
+ }
168
+ await activeTask;
169
+ }, options);
39
170
  // Create a text-accumulating stream by default.
40
171
  export const stream = streamFactory;
172
+ async function resolveRetryDelay(backoff, retry, error) {
173
+ if (typeof backoff === 'function') {
174
+ return Math.max(0, await backoff(retry, error));
175
+ }
176
+ if (Array.isArray(backoff)) {
177
+ const nextDelay = backoff[Math.min(retry - 1, backoff.length - 1)] ?? 0;
178
+ return Math.max(0, nextDelay);
179
+ }
180
+ if (backoff === 'exponential') {
181
+ return 250 * 2 ** (retry - 1);
182
+ }
183
+ return Math.max(0, backoff);
184
+ }
41
185
  // Turn any iterable or async iterable into a stream, optionally adding source delay.
42
186
  export function from(source, options = {}) {
43
187
  const { delay = 0, ...streamOptions } = options;
@@ -2,11 +2,10 @@ import { dynamic, fragment, h, list, mount, renderResponse, show, text } from '.
2
2
  import { batch, computed, effect, signal, untrack } from '../core/signal.js';
3
3
  import { response } from '../core/response.js';
4
4
  import { from, mapStream, scanStream, stream } from '../core/stream.js';
5
+ import { assertDocument } from './scope.js';
5
6
  // Resolve a CSS selector or direct node into the root mount target.
6
7
  function resolveTarget(target) {
7
- if (typeof document === 'undefined') {
8
- throw new Error('Qore app mounting requires a browser-like environment');
9
- }
8
+ assertDocument('createApp(...).mount(...)');
10
9
  if (typeof target === 'string') {
11
10
  const element = document.querySelector(target);
12
11
  if (!element) {
@@ -5,6 +5,7 @@ export declare function dynamic<T>(source: ReactiveValue<T>, render?: (value: T)
5
5
  export declare function show<T>(source: ReactiveValue<T>, render?: (value: T) => QoreChild, fallback?: QoreChild | ((value: T) => QoreChild)): QoreDocumentFragment;
6
6
  export declare function list<T>(source: ReactiveValue<Iterable<T> | ArrayLike<T> | null | undefined>, render: (item: T, index: number) => QoreChild, options?: {
7
7
  fallback?: QoreChild | ((items: T[]) => QoreChild);
8
+ key?: (item: T, index: number) => unknown;
8
9
  }): QoreDocumentFragment;
9
10
  export declare function renderResponse<TChunk, TValue>(responseState: ResponseState<TChunk, TValue>, views?: ResponseViews<TChunk, TValue>): QoreDocumentFragment;
10
11
  export declare function h(tag: string, props?: Record<string, unknown> | null, ...children: QoreChild[]): QoreElement;
@@ -63,6 +63,27 @@ function replaceRange(start, end, nextValue) {
63
63
  parent.insertBefore(node, end);
64
64
  }
65
65
  }
66
+ function insertBefore(parent, reference, value) {
67
+ for (const node of materialize(value)) {
68
+ parent.insertBefore(node, reference);
69
+ }
70
+ }
71
+ function destroyKeyedEntry(entry) {
72
+ disposeScope(entry.scope);
73
+ clearRange(entry.start, entry.end);
74
+ entry.start.remove();
75
+ entry.end.remove();
76
+ }
77
+ function renderKeyedEntry(parent, reference, item, index, render) {
78
+ const start = document.createComment('qore-list-item-start');
79
+ const end = document.createComment('qore-list-item-end');
80
+ const scope = createScope();
81
+ parent.insertBefore(start, reference);
82
+ const content = withScope(scope, () => render(item, index));
83
+ insertBefore(parent, reference, content);
84
+ parent.insertBefore(end, reference);
85
+ return { key: null, value: item, index, start, end, scope };
86
+ }
66
87
  // Allow mount targets to be passed as selectors or direct nodes.
67
88
  function resolveRoot(root) {
68
89
  if (typeof root === 'string') {
@@ -76,7 +97,7 @@ function resolveRoot(root) {
76
97
  }
77
98
  // Build a fragment from a variadic list of children.
78
99
  export function fragment(...children) {
79
- assertDocument();
100
+ assertDocument('fragment()');
80
101
  const node = document.createDocumentFragment();
81
102
  for (const child of children) {
82
103
  appendChild(node, child);
@@ -85,7 +106,7 @@ export function fragment(...children) {
85
106
  }
86
107
  // Render a live region between comment markers and refresh it when the source changes.
87
108
  export function dynamic(source, render = (value) => value) {
88
- assertDocument();
109
+ assertDocument('dynamic()');
89
110
  const start = document.createComment('qore-dynamic-start');
90
111
  const end = document.createComment('qore-dynamic-end');
91
112
  const node = document.createDocumentFragment();
@@ -115,18 +136,84 @@ export function show(source, render, fallback = null) {
115
136
  }
116
137
  // Render a list reactively, or a fallback when the collection is empty.
117
138
  export function list(source, render, options = {}) {
118
- const { fallback = null } = options;
119
- return dynamic(source, (value) => {
139
+ const { fallback = null, key } = options;
140
+ if (!key) {
141
+ return dynamic(source, (value) => {
142
+ const items = value == null
143
+ ? []
144
+ : Array.isArray(value)
145
+ ? value
146
+ : Array.from(value);
147
+ if (items.length === 0) {
148
+ return resolveTemplate(fallback, items);
149
+ }
150
+ return items.map((item, index) => render(item, index));
151
+ });
152
+ }
153
+ assertDocument('list()');
154
+ const start = document.createComment('qore-list-start');
155
+ const end = document.createComment('qore-list-end');
156
+ const node = document.createDocumentFragment();
157
+ node.append(start, end);
158
+ let entries = [];
159
+ let fallbackScope = null;
160
+ const stop = effect(() => {
161
+ const value = resolveAccessor(source);
120
162
  const items = value == null
121
163
  ? []
122
164
  : Array.isArray(value)
123
165
  ? value
124
166
  : Array.from(value);
167
+ const parent = end.parentNode;
168
+ if (!parent) {
169
+ return;
170
+ }
125
171
  if (items.length === 0) {
126
- return resolveTemplate(fallback, items);
172
+ for (const entry of entries) {
173
+ destroyKeyedEntry(entry);
174
+ }
175
+ entries = [];
176
+ disposeScope(fallbackScope);
177
+ fallbackScope = createScope();
178
+ const renderedFallback = withScope(fallbackScope, () => resolveTemplate(fallback, items));
179
+ replaceRange(start, end, renderedFallback);
180
+ return;
181
+ }
182
+ disposeScope(fallbackScope);
183
+ fallbackScope = null;
184
+ const nextKeys = items.map((item, index) => key(item, index));
185
+ const canAppend = entries.length <= items.length
186
+ && entries.every((entry, index) => Object.is(entry.key, nextKeys[index]));
187
+ if (!canAppend) {
188
+ for (const entry of entries) {
189
+ destroyKeyedEntry(entry);
190
+ }
191
+ clearRange(start, end);
192
+ entries = [];
193
+ }
194
+ if (!canAppend) {
195
+ for (let index = 0; index < items.length; index += 1) {
196
+ const entry = renderKeyedEntry(parent, end, items[index], index, render);
197
+ entry.key = nextKeys[index];
198
+ entries.push(entry);
199
+ }
200
+ return;
201
+ }
202
+ for (let index = entries.length; index < items.length; index += 1) {
203
+ const entry = renderKeyedEntry(parent, end, items[index], index, render);
204
+ entry.key = nextKeys[index];
205
+ entries.push(entry);
127
206
  }
128
- return items.map((item, index) => render(item, index));
129
207
  });
208
+ registerCleanup(() => {
209
+ stop();
210
+ disposeScope(fallbackScope);
211
+ for (const entry of entries) {
212
+ destroyKeyedEntry(entry);
213
+ }
214
+ entries = [];
215
+ });
216
+ return node;
130
217
  }
131
218
  // Render response state through status-aware template overrides.
132
219
  export function renderResponse(responseState, views = {}) {
@@ -142,7 +229,7 @@ export function renderResponse(responseState, views = {}) {
142
229
  });
143
230
  }
144
231
  export function h(tag, props = null, ...children) {
145
- assertDocument();
232
+ assertDocument('h()');
146
233
  if (typeof tag === 'function') {
147
234
  return tag({
148
235
  ...(props ?? {}),
@@ -162,7 +249,7 @@ export function h(tag, props = null, ...children) {
162
249
  }
163
250
  // Create a text node and keep it in sync with a reactive getter when necessary.
164
251
  export function text(valueOrGetter) {
165
- assertDocument();
252
+ assertDocument('text()');
166
253
  const node = document.createTextNode('');
167
254
  if (isReactiveValue(valueOrGetter)) {
168
255
  const stop = effect(() => {
@@ -177,7 +264,7 @@ export function text(valueOrGetter) {
177
264
  }
178
265
  // Mount a view into a root element and return a disposer for its reactive scope.
179
266
  export function mount(root, view) {
180
- assertDocument();
267
+ assertDocument('mount()');
181
268
  const target = resolveRoot(root);
182
269
  target[ROOT_CLEANUP]?.();
183
270
  const scope = createScope();
@@ -3,7 +3,9 @@ export declare const ROOT_CLEANUP: unique symbol;
3
3
  export interface Scope {
4
4
  cleanups: Cleanup[];
5
5
  }
6
- export declare function assertDocument(): void;
6
+ export declare function canUseDOM(): boolean;
7
+ export declare function assertCanUseDOM(apiName?: string): void;
8
+ export declare function assertDocument(apiName?: string): void;
7
9
  export declare function createScope(): Scope;
8
10
  export declare function withScope<T>(scope: Scope, fn: () => T): T;
9
11
  export declare function registerCleanup<T extends Cleanup | null | undefined>(cleanup: T): T;
@@ -1,12 +1,19 @@
1
1
  // Store mount cleanup directly on the root node so remounts can tear down old scopes.
2
2
  export const ROOT_CLEANUP = Symbol('qore.dom.cleanup');
3
3
  let activeScope = null;
4
+ // Detect whether the current runtime can create and mutate real DOM nodes.
5
+ export function canUseDOM() {
6
+ return typeof document !== 'undefined';
7
+ }
4
8
  // Guard DOM helpers so they only run in browser-like environments.
5
- export function assertDocument() {
6
- if (typeof document === 'undefined') {
7
- throw new Error('Qore DOM APIs require a browser-like environment');
9
+ export function assertCanUseDOM(apiName = 'Qore DOM APIs') {
10
+ if (!canUseDOM()) {
11
+ throw new Error(`${apiName} requires a browser-like environment`);
8
12
  }
9
13
  }
14
+ export function assertDocument(apiName = 'Qore DOM APIs') {
15
+ assertCanUseDOM(apiName);
16
+ }
10
17
  // A scope collects effect disposers created while rendering a subtree.
11
18
  export function createScope() {
12
19
  return { cleanups: [] };
@@ -1,16 +1,22 @@
1
- export { signal, computed, effect, batch, untrack, isSignal } from './core/signal.js';
1
+ export { signal, computed, createRoot, effect, onCleanup, batch, untrack, isSignal } from './core/signal.js';
2
2
  export type { ComputedSignal, ReadonlySignal, Signal } from './core/signal.js';
3
3
  export type { EffectOptions, EffectScheduler, SubscribeOptions } from './core/signal-types.js';
4
4
  export { stream, createStream, from, mapStream, scanStream, toAsyncIterable } from './core/stream.js';
5
- export type { BackpressureOptions, QoreStream, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './core/stream.js';
5
+ export type { BackpressureOptions, QoreStream, RetryBackoff, RetryableStreamOptions, StreamController, StreamFactory, StreamInput, StreamOptions, StreamSetup } from './core/stream.js';
6
6
  export { createResponse, response } from './core/response.js';
7
7
  export type { CreateResponseOptions, GlobalAbortSignal, MaybePromise, ResponseConsumeContext, ResponseExecutorContext, ResponseFactory, ResponseReactiveState, ResponseRunOptions, ResponseSnapshot, ResponseSource, ResponseSourceFactory, ResponseState, ResponseStatus, SourceLike } from './core/response.js';
8
8
  export { createApp } from './dom/app.js';
9
9
  export type { AppContext, AppSetupResult, QoreApp } from './dom/app.js';
10
10
  export { dynamic, fragment, h, list, mount, renderResponse, show, text } from './dom/dom.js';
11
+ export { assertCanUseDOM, canUseDOM } from './dom/scope.js';
12
+ export { createSSEResponse } from './server/sse-response.js';
13
+ export type { CreateSSEResponseOptions, SSEFrame } from './server/sse-response.js';
11
14
  export type { GlobalDocumentFragment, GlobalElement, GlobalNode, GlobalText, MountTarget, MountView, QoreChild, QoreComponent, QoreDocumentFragment, QoreElement, QoreNode, QoreTemplate, QoreText, ReactiveValue, ResponseRenderState, ResponseViews } from './dom/types.js';
12
15
  export { createAnthropic } from './providers/anthropic.js';
16
+ export { createDeepSeek } from './providers/deepseek.js';
17
+ export { createOllama } from './providers/ollama.js';
13
18
  export { createOpenAI } from './providers/openai.js';
14
- export { createSSEAdapter } from './providers/sse.js';
15
- export type { AnthropicAdapter, AnthropicChatInput, AnthropicEvent, AnthropicMessage, AnthropicOptions, AnthropicRequest, FetchLike, OpenAIAdapter, OpenAIChatInput, OpenAIEvent, OpenAIMessage, OpenAIOptions, OpenAIRequest, ProviderHeaders, ProviderRequestOptions, SSEAdapter, SSEAdapterOptions, SSEEvent, SSERequestConfig } from './providers/sse.js';
19
+ export { createOpenRouter } from './providers/openrouter.js';
20
+ export { collectProviderMetadata, createLineAdapter, createSSEAdapter, extractAnthropicMetadata, extractDeepSeekMetadata, extractOllamaMetadata, extractOpenAIMetadata, extractOpenRouterMetadata, mergeProviderMetadata } from './providers/sse.js';
21
+ export type { AnthropicAdapter, AnthropicChatInput, AnthropicEvent, AnthropicMessage, AnthropicOptions, AnthropicRequest, DeepSeekAdapter, DeepSeekChatInput, DeepSeekEvent, DeepSeekMessage, DeepSeekOptions, DeepSeekRequest, FetchLike, LineAdapter, LineAdapterOptions, LineEvent, LineRequestConfig, OpenAIAdapter, OpenAIChatInput, OpenAIEvent, OpenAIMessage, OpenAIOptions, OpenAIRequest, OllamaAdapter, OllamaChatInput, OllamaEvent, OllamaMessage, OllamaOptions, OllamaRequest, OpenRouterAdapter, OpenRouterChatInput, OpenRouterEvent, OpenRouterMessage, OpenRouterOptions, OpenRouterRequest, ProviderHeaders, ProviderMetadataUpdate, ProviderRetryBackoff, ProviderRetryOptions, ProviderRequestOptions, ProviderStreamMetadata, ProviderUsage, SSEAdapter, SSEAdapterOptions, SSEEvent, SSERequestConfig } from './providers/sse.js';
16
22
  export { normalizeError, sleep } from './shared/utils.js';