@qorejs/qore 0.7.0 → 0.7.2
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/README.md +170 -52
- package/dist/src/core/iterable.d.ts +1 -0
- package/dist/src/core/iterable.js +17 -0
- package/dist/src/core/response-runtime.d.ts +2 -0
- package/dist/src/core/response-runtime.js +233 -0
- package/dist/src/core/response-state.d.ts +3 -0
- package/dist/src/core/response-state.js +37 -0
- package/dist/src/core/response-types.d.ts +73 -0
- package/dist/src/core/response-types.js +1 -0
- package/dist/src/core/response.d.ts +4 -0
- package/dist/src/core/response.js +26 -0
- package/dist/src/core/signal-context.d.ts +10 -0
- package/dist/src/core/signal-context.js +69 -0
- package/dist/src/core/signal-nodes.d.ts +45 -0
- package/dist/src/core/signal-nodes.js +197 -0
- package/dist/src/core/signal-scheduler.d.ts +2 -0
- package/dist/src/core/signal-scheduler.js +19 -0
- package/dist/src/core/signal-types.d.ts +19 -0
- package/dist/src/core/signal-types.js +1 -0
- package/dist/src/core/signal.d.ts +19 -0
- package/dist/src/core/signal.js +41 -0
- package/dist/src/core/stream-backpressure.d.ts +3 -0
- package/dist/src/core/stream-backpressure.js +36 -0
- package/dist/src/core/stream-buffer.d.ts +17 -0
- package/dist/src/core/stream-buffer.js +162 -0
- package/dist/src/core/stream-iterator.d.ts +6 -0
- package/dist/src/core/stream-iterator.js +14 -0
- package/dist/src/core/stream-lifecycle.d.ts +10 -0
- package/dist/src/core/stream-lifecycle.js +28 -0
- package/dist/src/core/stream-queue.d.ts +17 -0
- package/dist/src/core/stream-queue.js +62 -0
- package/dist/src/core/stream-runtime.d.ts +2 -0
- package/dist/src/core/stream-runtime.js +127 -0
- package/dist/src/core/stream-source.d.ts +2 -0
- package/dist/src/core/stream-source.js +28 -0
- package/dist/src/core/stream-state.d.ts +4 -0
- package/dist/src/core/stream-state.js +20 -0
- package/dist/src/core/stream-types.d.ts +61 -0
- package/dist/src/core/stream-types.js +1 -0
- package/dist/src/core/stream.d.ts +11 -0
- package/dist/src/core/stream.js +90 -0
- package/dist/src/dom/app.d.ts +38 -0
- package/dist/src/dom/app.js +102 -0
- package/dist/src/dom/dom.d.ts +13 -0
- package/dist/src/dom/dom.js +197 -0
- package/dist/src/dom/properties.d.ts +3 -0
- package/dist/src/dom/properties.js +147 -0
- package/dist/src/dom/reactive.d.ts +6 -0
- package/dist/src/dom/reactive.js +14 -0
- package/dist/src/dom/response-view.d.ts +4 -0
- package/dist/src/dom/response-view.js +37 -0
- package/dist/src/dom/scope.d.ts +10 -0
- package/dist/src/dom/scope.js +49 -0
- package/dist/src/dom/types.d.ts +34 -0
- package/dist/src/dom/types.js +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +10 -0
- package/dist/src/providers/anthropic.d.ts +2 -0
- package/dist/src/providers/anthropic.js +102 -0
- package/dist/src/providers/openai.d.ts +2 -0
- package/dist/src/providers/openai.js +96 -0
- package/dist/src/providers/sse-adapter.d.ts +2 -0
- package/dist/src/providers/sse-adapter.js +83 -0
- package/dist/src/providers/sse-env.d.ts +4 -0
- package/dist/src/providers/sse-env.js +33 -0
- package/dist/src/providers/sse-parser.d.ts +5 -0
- package/dist/src/providers/sse-parser.js +99 -0
- package/dist/src/providers/sse.d.ts +4 -0
- package/dist/src/providers/sse.js +3 -0
- package/dist/src/providers/types.d.ts +94 -0
- package/dist/src/providers/types.js +1 -0
- package/dist/src/shared/utils.d.ts +2 -0
- package/dist/src/shared/utils.js +32 -0
- package/package.json +25 -11
- package/src/anthropic.js +0 -122
- package/src/app.js +0 -123
- package/src/dom.js +0 -527
- package/src/index.d.ts +0 -405
- package/src/index.js +0 -10
- package/src/iterable.js +0 -20
- package/src/openai.js +0 -112
- package/src/response.js +0 -312
- package/src/signal.js +0 -328
- package/src/sse.js +0 -264
- package/src/stream.js +0 -582
- package/src/utils.js +0 -39
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { dynamic, fragment, h, list, renderResponse, show, text } from './dom.js';
|
|
2
|
+
import { batch, computed, effect, signal, untrack } from '../core/signal.js';
|
|
3
|
+
import { response } from '../core/response.js';
|
|
4
|
+
import { from, mapStream, scanStream, stream } from '../core/stream.js';
|
|
5
|
+
import type { MountTarget, MountView, QoreChild, QoreElement } from './types.js';
|
|
6
|
+
export interface AppContext<Props extends Record<string, unknown>> {
|
|
7
|
+
app: QoreApp<Props>;
|
|
8
|
+
root: QoreElement;
|
|
9
|
+
props: Props;
|
|
10
|
+
signal: typeof signal;
|
|
11
|
+
computed: typeof computed;
|
|
12
|
+
effect: typeof effect;
|
|
13
|
+
batch: typeof batch;
|
|
14
|
+
untrack: typeof untrack;
|
|
15
|
+
stream: typeof stream;
|
|
16
|
+
from: typeof from;
|
|
17
|
+
mapStream: typeof mapStream;
|
|
18
|
+
scanStream: typeof scanStream;
|
|
19
|
+
response: typeof response;
|
|
20
|
+
h: typeof h;
|
|
21
|
+
text: typeof text;
|
|
22
|
+
dynamic: typeof dynamic;
|
|
23
|
+
show: typeof show;
|
|
24
|
+
list: typeof list;
|
|
25
|
+
fragment: typeof fragment;
|
|
26
|
+
renderResponse: typeof renderResponse;
|
|
27
|
+
onCleanup(handler: (() => void) | null | undefined): (() => void) | null | undefined;
|
|
28
|
+
}
|
|
29
|
+
export type AppSetupResult = QoreChild | {
|
|
30
|
+
view?: MountView;
|
|
31
|
+
onMount?: (root: QoreElement) => void;
|
|
32
|
+
};
|
|
33
|
+
export interface QoreApp<Props extends Record<string, unknown>> {
|
|
34
|
+
mount(target: MountTarget, props?: Props): QoreElement;
|
|
35
|
+
unmount(): QoreApp<Props>;
|
|
36
|
+
readonly root: QoreElement | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function createApp<Props extends Record<string, unknown> = Record<string, unknown>>(setup: (context: AppContext<Props>) => AppSetupResult): QoreApp<Props>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { dynamic, fragment, h, list, mount, renderResponse, show, text } from './dom.js';
|
|
2
|
+
import { batch, computed, effect, signal, untrack } from '../core/signal.js';
|
|
3
|
+
import { response } from '../core/response.js';
|
|
4
|
+
import { from, mapStream, scanStream, stream } from '../core/stream.js';
|
|
5
|
+
// Resolve a CSS selector or direct node into the root mount target.
|
|
6
|
+
function resolveTarget(target) {
|
|
7
|
+
if (typeof document === 'undefined') {
|
|
8
|
+
throw new Error('Qore app mounting requires a browser-like environment');
|
|
9
|
+
}
|
|
10
|
+
if (typeof target === 'string') {
|
|
11
|
+
const element = document.querySelector(target);
|
|
12
|
+
if (!element) {
|
|
13
|
+
throw new Error(`Qore could not find a mount target for selector: ${target}`);
|
|
14
|
+
}
|
|
15
|
+
return element;
|
|
16
|
+
}
|
|
17
|
+
return target;
|
|
18
|
+
}
|
|
19
|
+
// Create a tiny application shell around Qore's lower-level primitives.
|
|
20
|
+
export function createApp(setup) {
|
|
21
|
+
let dispose = null;
|
|
22
|
+
let mountedRoot = null;
|
|
23
|
+
let cleanupHandlers = [];
|
|
24
|
+
const app = {
|
|
25
|
+
// Mount the app, provide framework primitives to setup, and render the resulting view.
|
|
26
|
+
mount(target, props = {}) {
|
|
27
|
+
const root = resolveTarget(target);
|
|
28
|
+
app.unmount();
|
|
29
|
+
cleanupHandlers = [];
|
|
30
|
+
mountedRoot = root;
|
|
31
|
+
// Expose the core runtime pieces so an app can stay entirely within Qore primitives.
|
|
32
|
+
const context = {
|
|
33
|
+
app,
|
|
34
|
+
root,
|
|
35
|
+
props,
|
|
36
|
+
signal,
|
|
37
|
+
computed,
|
|
38
|
+
effect,
|
|
39
|
+
batch,
|
|
40
|
+
untrack,
|
|
41
|
+
stream,
|
|
42
|
+
from,
|
|
43
|
+
mapStream,
|
|
44
|
+
scanStream,
|
|
45
|
+
response,
|
|
46
|
+
h,
|
|
47
|
+
text,
|
|
48
|
+
dynamic,
|
|
49
|
+
show,
|
|
50
|
+
list,
|
|
51
|
+
fragment,
|
|
52
|
+
renderResponse,
|
|
53
|
+
onCleanup(handler) {
|
|
54
|
+
if (typeof handler === 'function') {
|
|
55
|
+
cleanupHandlers.push(handler);
|
|
56
|
+
}
|
|
57
|
+
return handler;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Allow setup to return either a raw view or an object with lifecycle hooks.
|
|
61
|
+
const result = setup(context);
|
|
62
|
+
const lifecycleResult = result && typeof result === 'object'
|
|
63
|
+
? result
|
|
64
|
+
: null;
|
|
65
|
+
const view = lifecycleResult && 'view' in lifecycleResult
|
|
66
|
+
? lifecycleResult.view
|
|
67
|
+
: result;
|
|
68
|
+
const onMount = lifecycleResult?.onMount ?? null;
|
|
69
|
+
dispose = mount(root, view);
|
|
70
|
+
if (typeof onMount === 'function') {
|
|
71
|
+
onMount(root);
|
|
72
|
+
}
|
|
73
|
+
return root;
|
|
74
|
+
},
|
|
75
|
+
// Tear down the mounted tree and any user-registered cleanup handlers.
|
|
76
|
+
unmount() {
|
|
77
|
+
if (dispose) {
|
|
78
|
+
const stop = dispose;
|
|
79
|
+
dispose = null;
|
|
80
|
+
stop();
|
|
81
|
+
}
|
|
82
|
+
for (let index = cleanupHandlers.length - 1; index >= 0; index -= 1) {
|
|
83
|
+
try {
|
|
84
|
+
const cleanup = cleanupHandlers[index];
|
|
85
|
+
if (cleanup) {
|
|
86
|
+
cleanup();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignore user cleanup errors so the app can still unmount.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
cleanupHandlers = [];
|
|
94
|
+
mountedRoot = null;
|
|
95
|
+
return app;
|
|
96
|
+
},
|
|
97
|
+
get root() {
|
|
98
|
+
return mountedRoot;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return app;
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResponseState } from '../core/response.js';
|
|
2
|
+
import type { MountTarget, MountView, QoreDocumentFragment, QoreElement, QoreChild, QoreComponent, QoreText, ReactiveValue, ResponseViews } from './types.js';
|
|
3
|
+
export declare function fragment(...children: QoreChild[]): QoreDocumentFragment;
|
|
4
|
+
export declare function dynamic<T>(source: ReactiveValue<T>, render?: (value: T) => QoreChild): QoreDocumentFragment;
|
|
5
|
+
export declare function show<T>(source: ReactiveValue<T>, render?: (value: T) => QoreChild, fallback?: QoreChild | ((value: T) => QoreChild)): QoreDocumentFragment;
|
|
6
|
+
export declare function list<T>(source: ReactiveValue<Iterable<T> | ArrayLike<T> | null | undefined>, render: (item: T, index: number) => QoreChild, options?: {
|
|
7
|
+
fallback?: QoreChild | ((items: T[]) => QoreChild);
|
|
8
|
+
}): QoreDocumentFragment;
|
|
9
|
+
export declare function renderResponse<TChunk, TValue>(responseState: ResponseState<TChunk, TValue>, views?: ResponseViews<TChunk, TValue>): QoreDocumentFragment;
|
|
10
|
+
export declare function h(tag: string, props?: Record<string, unknown> | null, ...children: QoreChild[]): QoreElement;
|
|
11
|
+
export declare function h<TProps extends Record<string, unknown>>(tag: QoreComponent<TProps>, props?: TProps | null, ...children: QoreChild[]): QoreChild;
|
|
12
|
+
export declare function text(valueOrGetter: ReactiveValue<unknown>): QoreText;
|
|
13
|
+
export declare function mount(root: MountTarget, view: MountView): () => QoreElement;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { effect } from '../core/signal.js';
|
|
2
|
+
import { bindProp } from './properties.js';
|
|
3
|
+
import { isReactiveValue, resolveAccessor, resolveTemplate } from './reactive.js';
|
|
4
|
+
import { pickResponseTemplate, readResponseState } from './response-view.js';
|
|
5
|
+
import { ROOT_CLEANUP, assertDocument, createScope, disposeScope, registerCleanup, withScope } from './scope.js';
|
|
6
|
+
function isDomNode(value) {
|
|
7
|
+
return typeof Node !== 'undefined' && value instanceof Node;
|
|
8
|
+
}
|
|
9
|
+
// Convert supported child types into concrete nodes that can be inserted into the DOM.
|
|
10
|
+
function materializeChild(buffer, child) {
|
|
11
|
+
if (Array.isArray(child)) {
|
|
12
|
+
for (const entry of child) {
|
|
13
|
+
materializeChild(buffer, entry);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (child == null || child === false || child === true) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (isReactiveValue(child)) {
|
|
21
|
+
buffer.push(dynamic(child));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (isDomNode(child)) {
|
|
25
|
+
buffer.push(child);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof child === 'string' || typeof child === 'number' || typeof child === 'bigint') {
|
|
29
|
+
buffer.push(document.createTextNode(String(child)));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw new TypeError(`Unsupported Qore child: ${typeof child}. Use strings, numbers, DOM nodes, arrays, or reactive getters.`);
|
|
33
|
+
}
|
|
34
|
+
// Flatten any child payload into a linear list of nodes.
|
|
35
|
+
function materialize(value) {
|
|
36
|
+
const nodes = [];
|
|
37
|
+
materializeChild(nodes, value);
|
|
38
|
+
return nodes;
|
|
39
|
+
}
|
|
40
|
+
// Append normalized child content to a parent node or fragment.
|
|
41
|
+
function appendChild(parent, child) {
|
|
42
|
+
for (const node of materialize(child)) {
|
|
43
|
+
parent.appendChild(node);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Remove every node between the two markers of a dynamic region.
|
|
47
|
+
function clearRange(start, end) {
|
|
48
|
+
let current = start.nextSibling;
|
|
49
|
+
while (current && current !== end) {
|
|
50
|
+
const next = current.nextSibling;
|
|
51
|
+
current.remove();
|
|
52
|
+
current = next;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Replace a dynamic region without recreating its boundary markers.
|
|
56
|
+
function replaceRange(start, end, nextValue) {
|
|
57
|
+
const parent = end.parentNode;
|
|
58
|
+
if (!parent) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
clearRange(start, end);
|
|
62
|
+
for (const node of materialize(nextValue)) {
|
|
63
|
+
parent.insertBefore(node, end);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Allow mount targets to be passed as selectors or direct nodes.
|
|
67
|
+
function resolveRoot(root) {
|
|
68
|
+
if (typeof root === 'string') {
|
|
69
|
+
const element = document.querySelector(root);
|
|
70
|
+
if (!element) {
|
|
71
|
+
throw new Error(`Qore could not find a mount target for selector: ${root}`);
|
|
72
|
+
}
|
|
73
|
+
return element;
|
|
74
|
+
}
|
|
75
|
+
return root;
|
|
76
|
+
}
|
|
77
|
+
// Build a fragment from a variadic list of children.
|
|
78
|
+
export function fragment(...children) {
|
|
79
|
+
assertDocument();
|
|
80
|
+
const node = document.createDocumentFragment();
|
|
81
|
+
for (const child of children) {
|
|
82
|
+
appendChild(node, child);
|
|
83
|
+
}
|
|
84
|
+
return node;
|
|
85
|
+
}
|
|
86
|
+
// Render a live region between comment markers and refresh it when the source changes.
|
|
87
|
+
export function dynamic(source, render = (value) => value) {
|
|
88
|
+
assertDocument();
|
|
89
|
+
const start = document.createComment('qore-dynamic-start');
|
|
90
|
+
const end = document.createComment('qore-dynamic-end');
|
|
91
|
+
const node = document.createDocumentFragment();
|
|
92
|
+
node.append(start, end);
|
|
93
|
+
let childScope = null;
|
|
94
|
+
const stop = effect(() => {
|
|
95
|
+
const nextValue = resolveAccessor(source);
|
|
96
|
+
disposeScope(childScope);
|
|
97
|
+
childScope = createScope();
|
|
98
|
+
const renderedValue = withScope(childScope, () => resolveTemplate(render, nextValue));
|
|
99
|
+
replaceRange(start, end, renderedValue);
|
|
100
|
+
});
|
|
101
|
+
registerCleanup(() => {
|
|
102
|
+
stop();
|
|
103
|
+
disposeScope(childScope);
|
|
104
|
+
});
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
// Conditionally render one branch or a fallback from a truthy source.
|
|
108
|
+
export function show(source, render, fallback = null) {
|
|
109
|
+
const truthyView = render === undefined
|
|
110
|
+
? (value) => value
|
|
111
|
+
: render;
|
|
112
|
+
return dynamic(source, (value) => value
|
|
113
|
+
? resolveTemplate(truthyView, value)
|
|
114
|
+
: resolveTemplate(fallback, value));
|
|
115
|
+
}
|
|
116
|
+
// Render a list reactively, or a fallback when the collection is empty.
|
|
117
|
+
export function list(source, render, options = {}) {
|
|
118
|
+
const { fallback = null } = options;
|
|
119
|
+
return dynamic(source, (value) => {
|
|
120
|
+
const items = value == null
|
|
121
|
+
? []
|
|
122
|
+
: Array.isArray(value)
|
|
123
|
+
? value
|
|
124
|
+
: Array.from(value);
|
|
125
|
+
if (items.length === 0) {
|
|
126
|
+
return resolveTemplate(fallback, items);
|
|
127
|
+
}
|
|
128
|
+
return items.map((item, index) => render(item, index));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Render response state through status-aware template overrides.
|
|
132
|
+
export function renderResponse(responseState, views = {}) {
|
|
133
|
+
return dynamic(() => readResponseState(responseState), (state) => {
|
|
134
|
+
const template = pickResponseTemplate(state.status, views);
|
|
135
|
+
if (template !== undefined) {
|
|
136
|
+
return resolveTemplate(template, state);
|
|
137
|
+
}
|
|
138
|
+
if (state.status === 'error') {
|
|
139
|
+
return state.error?.message ?? 'Qore response failed.';
|
|
140
|
+
}
|
|
141
|
+
return state.value;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
export function h(tag, props = null, ...children) {
|
|
145
|
+
assertDocument();
|
|
146
|
+
if (typeof tag === 'function') {
|
|
147
|
+
return tag({
|
|
148
|
+
...(props ?? {}),
|
|
149
|
+
children
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const element = document.createElement(tag);
|
|
153
|
+
if (props) {
|
|
154
|
+
for (const [key, value] of Object.entries(props)) {
|
|
155
|
+
bindProp(element, key, value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const child of children) {
|
|
159
|
+
appendChild(element, child);
|
|
160
|
+
}
|
|
161
|
+
return element;
|
|
162
|
+
}
|
|
163
|
+
// Create a text node and keep it in sync with a reactive getter when necessary.
|
|
164
|
+
export function text(valueOrGetter) {
|
|
165
|
+
assertDocument();
|
|
166
|
+
const node = document.createTextNode('');
|
|
167
|
+
if (isReactiveValue(valueOrGetter)) {
|
|
168
|
+
const stop = effect(() => {
|
|
169
|
+
const nextValue = resolveAccessor(valueOrGetter);
|
|
170
|
+
node.textContent = nextValue == null ? '' : String(nextValue);
|
|
171
|
+
});
|
|
172
|
+
registerCleanup(stop);
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
node.textContent = valueOrGetter == null ? '' : String(valueOrGetter);
|
|
176
|
+
return node;
|
|
177
|
+
}
|
|
178
|
+
// Mount a view into a root element and return a disposer for its reactive scope.
|
|
179
|
+
export function mount(root, view) {
|
|
180
|
+
assertDocument();
|
|
181
|
+
const target = resolveRoot(root);
|
|
182
|
+
target[ROOT_CLEANUP]?.();
|
|
183
|
+
const scope = createScope();
|
|
184
|
+
const content = withScope(scope, () => typeof view === 'function' ? view() : view);
|
|
185
|
+
target.replaceChildren();
|
|
186
|
+
appendChild(target, content);
|
|
187
|
+
const dispose = () => {
|
|
188
|
+
if (target[ROOT_CLEANUP] === dispose) {
|
|
189
|
+
delete target[ROOT_CLEANUP];
|
|
190
|
+
}
|
|
191
|
+
disposeScope(scope);
|
|
192
|
+
target.replaceChildren();
|
|
193
|
+
return target;
|
|
194
|
+
};
|
|
195
|
+
target[ROOT_CLEANUP] = dispose;
|
|
196
|
+
return dispose;
|
|
197
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { effect, isSignal } from '../core/signal.js';
|
|
2
|
+
import { isReactiveValue, resolveAccessor } from './reactive.js';
|
|
3
|
+
import { registerCleanup } from './scope.js';
|
|
4
|
+
// Normalize class payloads so callers can pass strings, arrays, or object maps.
|
|
5
|
+
function normalizeClassName(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value
|
|
8
|
+
.map((entry) => normalizeClassName(entry))
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.join(' ');
|
|
11
|
+
}
|
|
12
|
+
if (value && typeof value === 'object') {
|
|
13
|
+
return Object.entries(value)
|
|
14
|
+
.filter(([, active]) => Boolean(active))
|
|
15
|
+
.map(([className]) => className)
|
|
16
|
+
.join(' ');
|
|
17
|
+
}
|
|
18
|
+
if (value == null || value === false) {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
// Apply styles from either a raw string or a property map.
|
|
24
|
+
function setStyleValue(element, value) {
|
|
25
|
+
element.style.cssText = '';
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
element.style.cssText = value;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!value || typeof value !== 'object') {
|
|
31
|
+
if (value == null || value === false) {
|
|
32
|
+
element.removeAttribute('style');
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const styleRecord = element.style;
|
|
37
|
+
for (const [property, propertyValue] of Object.entries(value)) {
|
|
38
|
+
styleRecord[property] = propertyValue == null || propertyValue === false
|
|
39
|
+
? ''
|
|
40
|
+
: String(propertyValue);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Centralize DOM property and attribute writes behind one compatibility layer.
|
|
44
|
+
function setDomProperty(element, key, value) {
|
|
45
|
+
const attributeName = key === 'className' ? 'class' : key;
|
|
46
|
+
const domRecord = element;
|
|
47
|
+
if (key === 'className' || key === 'class') {
|
|
48
|
+
element.className = normalizeClassName(value);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (key === 'style') {
|
|
52
|
+
setStyleValue(element, value);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (value == null || value === false) {
|
|
56
|
+
if (key in element && key !== 'list' && key !== 'form') {
|
|
57
|
+
try {
|
|
58
|
+
if (typeof domRecord[key] === 'boolean') {
|
|
59
|
+
domRecord[key] = false;
|
|
60
|
+
}
|
|
61
|
+
else if (key === 'value') {
|
|
62
|
+
domRecord[key] = '';
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
domRecord[key] = '';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Ignore readonly DOM properties and fall back to attribute cleanup.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
element.removeAttribute(attributeName);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (value === true) {
|
|
76
|
+
if (key in element && key !== 'list' && key !== 'form') {
|
|
77
|
+
try {
|
|
78
|
+
domRecord[key] = true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Fall through to attribute mode.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
element.setAttribute(attributeName, '');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (key in element && key !== 'list' && key !== 'form') {
|
|
88
|
+
try {
|
|
89
|
+
domRecord[key] = value;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Fall back to setAttribute for readonly DOM properties.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
element.setAttribute(attributeName, String(value));
|
|
97
|
+
}
|
|
98
|
+
// Bind events and only treat signal-like values as reactive handler containers.
|
|
99
|
+
function bindEvent(element, key, handler) {
|
|
100
|
+
const eventName = key.slice(2).toLowerCase();
|
|
101
|
+
if (isSignal(handler)) {
|
|
102
|
+
let activeHandler = null;
|
|
103
|
+
const stop = effect(() => {
|
|
104
|
+
const nextHandler = handler();
|
|
105
|
+
if (activeHandler) {
|
|
106
|
+
element.removeEventListener(eventName, activeHandler);
|
|
107
|
+
}
|
|
108
|
+
activeHandler = typeof nextHandler === 'function' ? nextHandler : null;
|
|
109
|
+
if (activeHandler) {
|
|
110
|
+
element.addEventListener(eventName, activeHandler);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
registerCleanup(() => {
|
|
114
|
+
stop();
|
|
115
|
+
if (activeHandler) {
|
|
116
|
+
element.removeEventListener(eventName, activeHandler);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (typeof handler === 'function') {
|
|
122
|
+
const listener = handler;
|
|
123
|
+
element.addEventListener(eventName, listener);
|
|
124
|
+
registerCleanup(() => element.removeEventListener(eventName, listener));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Bind a prop key, upgrading reactive values into tracked effects when needed.
|
|
128
|
+
export function bindProp(element, key, value) {
|
|
129
|
+
if (key === 'ref') {
|
|
130
|
+
if (typeof value === 'function') {
|
|
131
|
+
value(element);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (key.startsWith('on')) {
|
|
136
|
+
bindEvent(element, key, value);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (isReactiveValue(value)) {
|
|
140
|
+
const stop = effect(() => {
|
|
141
|
+
setDomProperty(element, key, resolveAccessor(value));
|
|
142
|
+
});
|
|
143
|
+
registerCleanup(stop);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
setDomProperty(element, key, value);
|
|
147
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReactiveValue, QoreTemplate } from './types.js';
|
|
2
|
+
export declare function isReactiveValue<T>(value: ReactiveValue<T> | unknown): value is ReactiveValue<T>;
|
|
3
|
+
export declare function resolveAccessor<T>(value: ReactiveValue<T>): T;
|
|
4
|
+
export declare function resolveAccessor<T>(value: T): T;
|
|
5
|
+
export declare function resolveTemplate<T>(template: QoreTemplate<T>, value: T): QoreTemplate<T>;
|
|
6
|
+
export declare function resolveTemplate<T>(template: QoreTemplate<T> | undefined, value: T): QoreTemplate<T> | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isSignal } from '../core/signal.js';
|
|
2
|
+
// Treat signals and getters as reactive values the DOM layer should subscribe to.
|
|
3
|
+
export function isReactiveValue(value) {
|
|
4
|
+
return isSignal(value) || typeof value === 'function';
|
|
5
|
+
}
|
|
6
|
+
export function resolveAccessor(value) {
|
|
7
|
+
if (isSignal(value) || typeof value === 'function') {
|
|
8
|
+
return value();
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export function resolveTemplate(template, value) {
|
|
13
|
+
return typeof template === 'function' ? template(value) : template;
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResponseState, ResponseStatus } from '../core/response.js';
|
|
2
|
+
import type { ResponseRenderState, ResponseViews } from './types.js';
|
|
3
|
+
export declare function readResponseState<TChunk, TValue>(responseState: ResponseState<TChunk, TValue>): ResponseRenderState<TChunk, TValue>;
|
|
4
|
+
export declare function pickResponseTemplate<TChunk, TValue>(status: ResponseStatus, views: ResponseViews<TChunk, TValue>): unknown;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Read a response into one plain object so templates can branch on status cleanly.
|
|
2
|
+
export function readResponseState(responseState) {
|
|
3
|
+
return {
|
|
4
|
+
response: responseState,
|
|
5
|
+
status: responseState.status(),
|
|
6
|
+
value: responseState.value(),
|
|
7
|
+
error: responseState.error(),
|
|
8
|
+
chunks: responseState.chunks(),
|
|
9
|
+
startedAt: responseState.startedAt(),
|
|
10
|
+
finishedAt: responseState.finishedAt(),
|
|
11
|
+
pending: responseState.pending(),
|
|
12
|
+
streaming: responseState.streaming(),
|
|
13
|
+
completed: responseState.completed(),
|
|
14
|
+
failed: responseState.failed(),
|
|
15
|
+
aborted: responseState.aborted(),
|
|
16
|
+
chunkCount: responseState.chunkCount()
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Pick the best matching view override for the current response lifecycle state.
|
|
20
|
+
export function pickResponseTemplate(status, views) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 'idle':
|
|
23
|
+
return views.idle ?? views.pending ?? views.default;
|
|
24
|
+
case 'pending':
|
|
25
|
+
return views.pending ?? views.default;
|
|
26
|
+
case 'streaming':
|
|
27
|
+
return views.streaming ?? views.pending ?? views.default;
|
|
28
|
+
case 'completed':
|
|
29
|
+
return views.completed ?? views.default;
|
|
30
|
+
case 'error':
|
|
31
|
+
return views.error ?? views.default;
|
|
32
|
+
case 'aborted':
|
|
33
|
+
return views.aborted ?? views.default;
|
|
34
|
+
default:
|
|
35
|
+
return views.default;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Cleanup } from '../core/signal-types.js';
|
|
2
|
+
export declare const ROOT_CLEANUP: unique symbol;
|
|
3
|
+
export interface Scope {
|
|
4
|
+
cleanups: Cleanup[];
|
|
5
|
+
}
|
|
6
|
+
export declare function assertDocument(): void;
|
|
7
|
+
export declare function createScope(): Scope;
|
|
8
|
+
export declare function withScope<T>(scope: Scope, fn: () => T): T;
|
|
9
|
+
export declare function registerCleanup<T extends Cleanup | null | undefined>(cleanup: T): T;
|
|
10
|
+
export declare function disposeScope(scope: Scope | null | undefined): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Store mount cleanup directly on the root node so remounts can tear down old scopes.
|
|
2
|
+
export const ROOT_CLEANUP = Symbol('qore.dom.cleanup');
|
|
3
|
+
let activeScope = null;
|
|
4
|
+
// 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');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// A scope collects effect disposers created while rendering a subtree.
|
|
11
|
+
export function createScope() {
|
|
12
|
+
return { cleanups: [] };
|
|
13
|
+
}
|
|
14
|
+
// Temporarily switch the active scope while a subtree is being materialized.
|
|
15
|
+
export function withScope(scope, fn) {
|
|
16
|
+
const previousScope = activeScope;
|
|
17
|
+
activeScope = scope;
|
|
18
|
+
try {
|
|
19
|
+
return fn();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
activeScope = previousScope;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Register a cleanup callback on the currently active scope, if one exists.
|
|
26
|
+
export function registerCleanup(cleanup) {
|
|
27
|
+
if (typeof cleanup === 'function' && activeScope) {
|
|
28
|
+
activeScope.cleanups.push(cleanup);
|
|
29
|
+
}
|
|
30
|
+
return cleanup;
|
|
31
|
+
}
|
|
32
|
+
// Dispose nested resources in reverse order so teardown mirrors setup.
|
|
33
|
+
export function disposeScope(scope) {
|
|
34
|
+
if (!scope) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (let index = scope.cleanups.length - 1; index >= 0; index -= 1) {
|
|
38
|
+
try {
|
|
39
|
+
const cleanup = scope.cleanups[index];
|
|
40
|
+
if (cleanup) {
|
|
41
|
+
cleanup();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore cleanup errors during teardown so the rest of the scope can unwind.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
scope.cleanups.length = 0;
|
|
49
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResponseState, ResponseStatus } from '../core/response.js';
|
|
2
|
+
import type { ReadonlySignal } from '../core/signal.js';
|
|
3
|
+
export type QoreNode = Node;
|
|
4
|
+
export type QoreElement = Element;
|
|
5
|
+
export type QoreText = Text;
|
|
6
|
+
export type QoreDocumentFragment = DocumentFragment;
|
|
7
|
+
export type GlobalNode = QoreNode;
|
|
8
|
+
export type GlobalElement = QoreElement;
|
|
9
|
+
export type GlobalText = QoreText;
|
|
10
|
+
export type GlobalDocumentFragment = QoreDocumentFragment;
|
|
11
|
+
export type ReactiveValue<T> = T | ReadonlySignal<T> | (() => T);
|
|
12
|
+
export type QoreChild = QoreNode | string | number | bigint | boolean | null | undefined | ReactiveValue<unknown> | QoreChild[];
|
|
13
|
+
export type QoreTemplate<T> = QoreChild | ((value: T) => QoreChild);
|
|
14
|
+
export interface ResponseRenderState<TChunk = unknown, TValue = unknown> {
|
|
15
|
+
response: ResponseState<TChunk, TValue>;
|
|
16
|
+
status: ResponseStatus;
|
|
17
|
+
value: TValue;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
chunks: TChunk[];
|
|
20
|
+
startedAt: number | null;
|
|
21
|
+
finishedAt: number | null;
|
|
22
|
+
pending: boolean;
|
|
23
|
+
streaming: boolean;
|
|
24
|
+
completed: boolean;
|
|
25
|
+
failed: boolean;
|
|
26
|
+
aborted: boolean;
|
|
27
|
+
chunkCount: number;
|
|
28
|
+
}
|
|
29
|
+
export type ResponseViews<TChunk = unknown, TValue = unknown> = Partial<Record<ResponseStatus | 'default', QoreTemplate<ResponseRenderState<TChunk, TValue>>>>;
|
|
30
|
+
export type QoreComponent<Props extends Record<string, unknown> = Record<string, unknown>> = (props: Props & {
|
|
31
|
+
children: QoreChild[];
|
|
32
|
+
}) => QoreChild;
|
|
33
|
+
export type MountTarget = string | QoreElement;
|
|
34
|
+
export type MountView = QoreChild | (() => QoreChild);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|