@qorejs/qore 0.6.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/src/stream.ts ADDED
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Qore Stream - AI Streaming & Server-Side Streaming Support
3
+ * Minimal API for AI responses and SSR streaming
4
+ */
5
+
6
+ import { signal, effect, computed } from './signal';
7
+ import { VNode, Component } from './render';
8
+
9
+ export interface StreamWriter {
10
+ (chunk: string): void;
11
+ clear(): void;
12
+ done(): void;
13
+ }
14
+
15
+ export interface StreamOptions {
16
+ container: HTMLElement;
17
+ parseMarkdown?: boolean;
18
+ onComplete?: () => void;
19
+ onError?: (error: Error) => void;
20
+ }
21
+
22
+ /**
23
+ * AI streaming response (client-side)
24
+ */
25
+ export function stream(
26
+ fn: (write: StreamWriter) => Promise<void>,
27
+ options: StreamOptions
28
+ ): { abort: () => void } {
29
+ const { container, parseMarkdown = false, onComplete, onError } = options;
30
+
31
+ let aborted = false;
32
+ let content = '';
33
+
34
+ container.innerHTML = '';
35
+ const output = document.createElement('div');
36
+ output.className = 'stream-output';
37
+ container.appendChild(output);
38
+
39
+ const update = () => {
40
+ output.innerHTML = parseMarkdown ? doParseMarkdown(content) : content;
41
+ };
42
+
43
+ const write: StreamWriter = (chunk: string) => {
44
+ if (aborted) return;
45
+ content += chunk;
46
+ update();
47
+ };
48
+
49
+ write.clear = () => { content = ''; update(); };
50
+ write.done = () => { if (!aborted) onComplete?.(); };
51
+
52
+ Promise.resolve().then(() => fn(write))
53
+ .catch((err: Error) => { if (!aborted) onError?.(err); });
54
+
55
+ return {
56
+ abort: () => { aborted = true; }
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Simple Markdown parser
62
+ */
63
+ function doParseMarkdown(text: string): string {
64
+ return text
65
+ .replace(/^### (.*$)/gim, '<h3>$1</h3>')
66
+ .replace(/^## (.*$)/gim, '<h2>$1</h2>')
67
+ .replace(/^# (.*$)/gim, '<h1>$1</h1>')
68
+ .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
69
+ .replace(/\*(.*)\*/gim, '<em>$1</em>')
70
+ .replace(/`([^`]+)`/gim, '<code>$1</code>')
71
+ .replace(/\n/gim, '<br>');
72
+ }
73
+
74
+ /**
75
+ * Typewriter effect
76
+ */
77
+ export function streamText(
78
+ text: string,
79
+ options: { container: HTMLElement; speed?: number; onComplete?: () => void }
80
+ ): { abort: () => void } {
81
+ const { container, speed = 30, onComplete } = options;
82
+
83
+ return stream(async (write) => {
84
+ for (let i = 0; i < text.length; i++) {
85
+ await new Promise(r => setTimeout(r, speed));
86
+ write(text[i]);
87
+ }
88
+ write.done();
89
+ }, { container, onComplete });
90
+ }
91
+
92
+ // ============== Server-Side Streaming ==============
93
+
94
+ /**
95
+ * Server-side stream renderer
96
+ * Support chunked HTML fragment output
97
+ */
98
+ export class StreamRenderer {
99
+ private chunks: string[] = [];
100
+ private callbacks: ((chunk: string) => void)[] = [];
101
+ private resolved = false;
102
+ private error: Error | null = null;
103
+
104
+ /**
105
+ * Write an HTML chunk
106
+ */
107
+ write(chunk: string): void {
108
+ this.chunks.push(chunk);
109
+ this.callbacks.forEach(cb => cb(chunk));
110
+ }
111
+
112
+ /**
113
+ * Complete stream rendering
114
+ */
115
+ end(): void {
116
+ this.resolved = true;
117
+ }
118
+
119
+ /**
120
+ * Throw error
121
+ */
122
+ fail(err: Error): void {
123
+ this.error = err;
124
+ this.resolved = true;
125
+ }
126
+
127
+ /**
128
+ * Subscribe to stream output
129
+ */
130
+ subscribe(callback: (chunk: string) => void): () => void {
131
+ this.callbacks.push(callback);
132
+ // Immediately send existing chunks
133
+ this.chunks.forEach(chunk => callback(chunk));
134
+
135
+ return () => {
136
+ const idx = this.callbacks.indexOf(callback);
137
+ if (idx !== -1) this.callbacks.splice(idx, 1);
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Get complete HTML
143
+ */
144
+ getHTML(): string {
145
+ return this.chunks.join('');
146
+ }
147
+
148
+ /**
149
+ * Async iterator - for use with for await...of
150
+ */
151
+ async *[Symbol.asyncIterator](): AsyncGenerator<string, void, unknown> {
152
+ let index = 0;
153
+
154
+ while (index < this.chunks.length || !this.resolved) {
155
+ if (index < this.chunks.length) {
156
+ yield this.chunks[index++];
157
+ } else {
158
+ await new Promise(resolve => {
159
+ const check = () => {
160
+ if (index < this.chunks.length || this.resolved) {
161
+ resolve(undefined);
162
+ } else {
163
+ setTimeout(check, 10);
164
+ }
165
+ };
166
+ check();
167
+ });
168
+ }
169
+ }
170
+
171
+ if (this.error) {
172
+ throw this.error;
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Stream HTML fragment generator
179
+ */
180
+ export function createStreamHTML(): {
181
+ renderer: StreamRenderer;
182
+ html: () => string;
183
+ stream: () => AsyncGenerator<string>;
184
+ } {
185
+ const renderer = new StreamRenderer();
186
+
187
+ return {
188
+ renderer,
189
+ html: () => renderer.getHTML(),
190
+ stream: () => renderer[Symbol.asyncIterator]()
191
+ };
192
+ }
193
+
194
+ // ============== Suspense & Lazy Loading ==============
195
+
196
+ /**
197
+ * Suspense state
198
+ */
199
+ export type SuspenseState = 'pending' | 'resolved' | 'error';
200
+
201
+ /**
202
+ * Suspense component props
203
+ */
204
+ export interface SuspenseProps {
205
+ fallback: VNode;
206
+ children: () => VNode;
207
+ onError?: (error: Error) => void;
208
+ }
209
+
210
+ /**
211
+ * Suspense boundary component
212
+ * Wraps asynchronously loaded components - each instance has independent state
213
+ */
214
+ export function Suspense({ fallback, children, onError }: SuspenseProps): Component {
215
+ // State sinks to component instance level, avoiding global singleton issues
216
+ const state = signal<SuspenseState>('pending');
217
+ const errorSig = signal<Error | null>(null);
218
+
219
+ return () => {
220
+ const s = state();
221
+ const err = errorSig();
222
+
223
+ if (s === 'error') {
224
+ onError?.(err!);
225
+ return fallback;
226
+ }
227
+
228
+ if (s === 'pending') {
229
+ return fallback;
230
+ }
231
+
232
+ return children();
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Create Suspense component with state
238
+ * Allow external control of loading state
239
+ */
240
+ export function createSuspense({ fallback, children, onError }: SuspenseProps): {
241
+ component: Component;
242
+ setState: (state: SuspenseState, error?: Error) => void;
243
+ getState: () => SuspenseState;
244
+ } {
245
+ const state = signal<SuspenseState>('pending');
246
+ const errorSig = signal<Error | null>(null);
247
+
248
+ const component: Component = () => {
249
+ const s = state();
250
+ const err = errorSig();
251
+
252
+ if (s === 'error') {
253
+ onError?.(err!);
254
+ return fallback;
255
+ }
256
+
257
+ if (s === 'pending') {
258
+ return fallback;
259
+ }
260
+
261
+ return children();
262
+ };
263
+
264
+ return {
265
+ component,
266
+ setState: (newState: SuspenseState, error?: Error) => {
267
+ state(newState);
268
+ if (error) errorSig(error);
269
+ },
270
+ getState: () => state()
271
+ };
272
+ }
273
+
274
+ /**
275
+ * lazy() - lazy load component
276
+ * Returns a wrapped component that displays Suspense fallback on first render
277
+ */
278
+ export function lazy<T extends Component>(
279
+ importFn: () => Promise<{ default: T }>
280
+ ): () => { load: () => Promise<T>; component: T | null } {
281
+ let loadedComponent: T | null = null;
282
+ let loadPromise: Promise<T> | null = null;
283
+
284
+ const load = async (): Promise<T> => {
285
+ if (loadedComponent) return loadedComponent;
286
+ if (loadPromise) return loadPromise;
287
+
288
+ loadPromise = importFn().then(mod => {
289
+ loadedComponent = mod.default;
290
+ return loadedComponent;
291
+ });
292
+
293
+ return loadPromise;
294
+ };
295
+
296
+ return () => {
297
+ return {
298
+ load,
299
+ component: loadedComponent
300
+ };
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Async component wrapper
306
+ */
307
+ export function asyncComponent<T extends Component>(
308
+ importFn: () => Promise<{ default: T }>,
309
+ fallback: VNode
310
+ ): Component {
311
+ const lazyFactory = lazy(importFn);
312
+ const state = signal<SuspenseState>('pending');
313
+ const component = signal<T | null>(null);
314
+
315
+ // 触发加载
316
+ lazyFactory().load()
317
+ .then(comp => {
318
+ component(comp);
319
+ state('resolved');
320
+ })
321
+ .catch(err => {
322
+ console.error('Async component load failed:', err);
323
+ state('error');
324
+ });
325
+
326
+ return () => {
327
+ if (state() === 'pending') {
328
+ return fallback;
329
+ }
330
+
331
+ if (state() === 'error') {
332
+ return fallback;
333
+ }
334
+
335
+ const comp = component();
336
+ return comp ? comp() : fallback;
337
+ };
338
+ }
339
+
340
+ // ============== Incremental DOM Updates ==============
341
+
342
+ /**
343
+ * Incremental update chunk
344
+ */
345
+ export interface IncrementalUpdate {
346
+ id: string;
347
+ html: string;
348
+ type: 'replace' | 'append' | 'prepend' | 'remove';
349
+ }
350
+
351
+ /**
352
+ * Create incremental update message
353
+ */
354
+ export function createUpdate(id: string, html: string, type: IncrementalUpdate['type'] = 'replace'): IncrementalUpdate {
355
+ return { id, html, type };
356
+ }
357
+
358
+ /**
359
+ * Apply incremental update to DOM
360
+ */
361
+ export function applyUpdate(container: HTMLElement, update: IncrementalUpdate): void {
362
+ const { id, html, type } = update;
363
+ const element = container.querySelector(`[data-stream-id="${id}"]`);
364
+
365
+ switch (type) {
366
+ case 'replace':
367
+ if (element) {
368
+ element.outerHTML = html;
369
+ } else {
370
+ const temp = document.createElement('div');
371
+ temp.innerHTML = html;
372
+ const newEl = temp.firstElementChild;
373
+ if (newEl) {
374
+ newEl.setAttribute('data-stream-id', id);
375
+ container.appendChild(newEl);
376
+ }
377
+ }
378
+ break;
379
+
380
+ case 'append':
381
+ if (element) {
382
+ element.insertAdjacentHTML('beforeend', html);
383
+ }
384
+ break;
385
+
386
+ case 'prepend':
387
+ if (element) {
388
+ element.insertAdjacentHTML('afterbegin', html);
389
+ }
390
+ break;
391
+
392
+ case 'remove':
393
+ if (element) {
394
+ element.remove();
395
+ }
396
+ break;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Stream render to target element
402
+ * Support server-push incremental updates
403
+ */
404
+ export function renderToStream(
405
+ container: HTMLElement,
406
+ stream: AsyncGenerator<string, void, unknown>
407
+ ): { abort: () => void } {
408
+ let aborted = false;
409
+
410
+ (async () => {
411
+ try {
412
+ for await (const chunk of stream) {
413
+ if (aborted) break;
414
+
415
+ // Parse incremental update
416
+ try {
417
+ const update: IncrementalUpdate = JSON.parse(chunk);
418
+ applyUpdate(container, update);
419
+ } catch {
420
+ // If not JSON, directly append HTML
421
+ container.insertAdjacentHTML('beforeend', chunk);
422
+ }
423
+ }
424
+ } catch (err) {
425
+ console.error('Stream rendering error:', err);
426
+ }
427
+ })();
428
+
429
+ return {
430
+ abort: () => { aborted = true; }
431
+ };
432
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Qore Utilities
3
+ */
4
+
5
+ export function debounce<T extends (...args: any[]) => any>(
6
+ fn: T,
7
+ delay: number
8
+ ): (...args: Parameters<T>) => void {
9
+ let timer: ReturnType<typeof setTimeout> | null = null;
10
+ return (...args) => {
11
+ if (timer) clearTimeout(timer);
12
+ timer = setTimeout(() => fn(...args), delay);
13
+ };
14
+ }
15
+
16
+ export function throttle<T extends (...args: any[]) => any>(
17
+ fn: T,
18
+ interval: number
19
+ ): (...args: Parameters<T>) => void {
20
+ let last = 0;
21
+ return (...args) => {
22
+ const now = Date.now();
23
+ if (now - last >= interval) {
24
+ last = now;
25
+ fn(...args);
26
+ }
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Class name utility - conditional classes
32
+ */
33
+ export function cx(...classes: (string | false | null | undefined | Record<string, boolean>)[]): string {
34
+ const result: string[] = [];
35
+
36
+ for (const cls of classes) {
37
+ if (!cls) continue;
38
+
39
+ if (typeof cls === 'string') {
40
+ result.push(cls);
41
+ } else if (typeof cls === 'object') {
42
+ for (const [key, value] of Object.entries(cls)) {
43
+ if (value) result.push(key);
44
+ }
45
+ }
46
+ }
47
+
48
+ return result.join(' ');
49
+ }
50
+
51
+ /**
52
+ * Style merging utility - merge multiple style objects
53
+ */
54
+ export function style(...styles: (Record<string, string | number> | false | null | undefined)[]): Record<string, string | number> {
55
+ const result: Record<string, string | number> = {};
56
+
57
+ for (const s of styles) {
58
+ if (!s) continue;
59
+ Object.assign(result, s);
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ /**
66
+ * Event listener utility with cleanup
67
+ */
68
+ export function on<K extends keyof HTMLElementEventMap>(
69
+ el: HTMLElement,
70
+ type: K,
71
+ handler: (e: HTMLElementEventMap[K]) => void,
72
+ options?: boolean | AddEventListenerOptions
73
+ ): () => void {
74
+ el.addEventListener(type, handler as EventListener, options);
75
+ return () => el.removeEventListener(type, handler as EventListener, options);
76
+ }
77
+
78
+ /**
79
+ * Event handler creators for common events
80
+ */
81
+ export const onEvent = {
82
+ click: (handler: (e: MouseEvent) => void) => ({ onClick: handler }),
83
+ change: (handler: (e: Event) => void) => ({ onChange: handler }),
84
+ input: (handler: (e: Event) => void) => ({ onInput: handler }),
85
+ submit: (handler: (e: SubmitEvent) => void) => ({ onSubmit: handler }),
86
+ keydown: (handler: (e: KeyboardEvent) => void) => ({ onKeyDown: handler }),
87
+ keyup: (handler: (e: KeyboardEvent) => void) => ({ onKeyUp: handler }),
88
+ focus: (handler: (e: FocusEvent) => void) => ({ onFocus: handler }),
89
+ blur: (handler: (e: FocusEvent) => void) => ({ onBlur: handler }),
90
+ mouseenter: (handler: (e: MouseEvent) => void) => ({ onMouseEnter: handler }),
91
+ mouseleave: (handler: (e: MouseEvent) => void) => ({ onMouseLeave: handler }),
92
+ };
93
+
94
+ /**
95
+ * Create event handler with prevent default
96
+ */
97
+ export function preventDefault<T extends Event>(handler: (e: T) => void): (e: T) => void {
98
+ return (e: T) => {
99
+ e.preventDefault();
100
+ handler(e);
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Create event handler with stop propagation
106
+ */
107
+ export function stopPropagation<T extends Event>(handler: (e: T) => void): (e: T) => void {
108
+ return (e: T) => {
109
+ e.stopPropagation();
110
+ handler(e);
111
+ };
112
+ }
113
+
114
+ export function sleep(ms: number): Promise<void> {
115
+ return new Promise(resolve => setTimeout(resolve, ms));
116
+ }