@jxsuite/runtime 6.0.1 → 6.1.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/dist/runtime.js +101 -27
- package/dist/runtime.js.map +3 -3
- package/package.json +2 -2
- package/src/runtime.ts +181 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"upgrade": "bun update --latest && bun install"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@jxsuite/schema": "^5.
|
|
36
|
+
"@jxsuite/schema": "^5.2.0",
|
|
37
37
|
"@vue/reactivity": "3.5.44"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
package/src/runtime.ts
CHANGED
|
@@ -1163,6 +1163,69 @@ export function setTemplateErrorHandler(
|
|
|
1163
1163
|
_templateErrorHandler = handler;
|
|
1164
1164
|
}
|
|
1165
1165
|
|
|
1166
|
+
/** Warn about a throwing template, once per source. */
|
|
1167
|
+
function warnTemplateError(source: string, error: Error): void {
|
|
1168
|
+
if (!reportedTemplates.has(source)) {
|
|
1169
|
+
reportedTemplates.add(source);
|
|
1170
|
+
console.warn(`jx: template ${JSON.stringify(source)} threw:`, error);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Lookups in flight whose answers can make a throwing template valid (§6.1). */
|
|
1175
|
+
let pendingLookups = 0;
|
|
1176
|
+
|
|
1177
|
+
/** Each binding still throwing while a lookup is in flight, by its `run`, with its latest throw. */
|
|
1178
|
+
const heldTemplateErrors = new Map<() => void, { error: Error; source: string }>();
|
|
1179
|
+
|
|
1180
|
+
/** Who {@link lookupsSettled} has yet to answer. */
|
|
1181
|
+
const lookupWaiters: (() => void)[] = [];
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Hold template warnings until `lookup` and every other lookup in flight have settled.
|
|
1185
|
+
*
|
|
1186
|
+
* `lookup` must write its answer to state before it settles: the write re-runs the bindings waiting
|
|
1187
|
+
* on it, so whatever is still held when the last one settles is a template the data did not fix,
|
|
1188
|
+
* and only that is reported. A lookup its own answer starts is counted before this one is
|
|
1189
|
+
* released.
|
|
1190
|
+
*
|
|
1191
|
+
* @param {Promise<void>} lookup - Never rejects: it reports its own failure
|
|
1192
|
+
*/
|
|
1193
|
+
function holdTemplateErrorsFor(lookup: Promise<void>): void {
|
|
1194
|
+
pendingLookups += 1;
|
|
1195
|
+
void lookup.finally(() => {
|
|
1196
|
+
pendingLookups -= 1;
|
|
1197
|
+
if (pendingLookups === 0) {
|
|
1198
|
+
/* The console, even if a handler was installed meanwhile: these throws happened outside that
|
|
1199
|
+
handler's render, and Studio's render critic would refuse its page for them. */
|
|
1200
|
+
for (const { error, source } of heldTemplateErrors.values()) {
|
|
1201
|
+
warnTemplateError(source, error);
|
|
1202
|
+
}
|
|
1203
|
+
heldTemplateErrors.clear();
|
|
1204
|
+
for (const wake of lookupWaiters.splice(0)) {
|
|
1205
|
+
wake();
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Answers once no lookup the runtime started is in flight (embedding.md §5.2).
|
|
1213
|
+
*
|
|
1214
|
+
* A host whose question is whether a document renders awaits this between `buildScope` and
|
|
1215
|
+
* `renderNode`, so its templates read the data rather than the `null` before it. A lookup can hang,
|
|
1216
|
+
* and this waits for it, so bounding the wait is the host's job.
|
|
1217
|
+
*
|
|
1218
|
+
* @returns {Promise<void>}
|
|
1219
|
+
*/
|
|
1220
|
+
export function lookupsSettled(): Promise<void> {
|
|
1221
|
+
if (pendingLookups === 0) {
|
|
1222
|
+
return Promise.resolve();
|
|
1223
|
+
}
|
|
1224
|
+
return new Promise((wake) => {
|
|
1225
|
+
lookupWaiters.push(wake);
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1166
1229
|
/**
|
|
1167
1230
|
* Run a template binding as an effect that survives its expression throwing.
|
|
1168
1231
|
*
|
|
@@ -1172,7 +1235,8 @@ export function setTemplateErrorHandler(
|
|
|
1172
1235
|
* and the throw unwound the whole `renderNode` above it: one early read blanked the page. Caught
|
|
1173
1236
|
* INSIDE the effect, every dependency read before the throw stays tracked, so the binding re-runs
|
|
1174
1237
|
* on the change that makes it valid. Until then it shows `fallback`'s answer (nothing written, by
|
|
1175
|
-
* default), and the error is reported once per source
|
|
1238
|
+
* default), and the error is reported once per source, after any lookup in flight has settled: a
|
|
1239
|
+
* template that only needed that lookup's answer is not reported at all.
|
|
1176
1240
|
*
|
|
1177
1241
|
* @param {string} source - The template, for the report
|
|
1178
1242
|
* @param {() => void} run - Evaluates the template and writes it
|
|
@@ -1182,12 +1246,15 @@ function bindTemplate(source: string, run: () => void, fallback?: () => void): v
|
|
|
1182
1246
|
effect(() => {
|
|
1183
1247
|
try {
|
|
1184
1248
|
run();
|
|
1249
|
+
heldTemplateErrors.delete(run);
|
|
1185
1250
|
} catch (error) {
|
|
1251
|
+
const thrown = error instanceof Error ? error : new Error(String(error));
|
|
1186
1252
|
if (_templateErrorHandler) {
|
|
1187
|
-
_templateErrorHandler(
|
|
1188
|
-
} else if (
|
|
1189
|
-
|
|
1190
|
-
|
|
1253
|
+
_templateErrorHandler(thrown, source);
|
|
1254
|
+
} else if (pendingLookups > 0) {
|
|
1255
|
+
heldTemplateErrors.set(run, { error: thrown, source });
|
|
1256
|
+
} else {
|
|
1257
|
+
warnTemplateError(source, thrown);
|
|
1191
1258
|
}
|
|
1192
1259
|
fallback?.();
|
|
1193
1260
|
}
|
|
@@ -1857,11 +1924,74 @@ export function renderNode(
|
|
|
1857
1924
|
el.append(renderNode(child, localState, childOpts));
|
|
1858
1925
|
}
|
|
1859
1926
|
}
|
|
1927
|
+
} else if (isTemplateString(kids)) {
|
|
1928
|
+
renderComputedChildren(el, kids, localState, {
|
|
1929
|
+
...options,
|
|
1930
|
+
_ns: childNs,
|
|
1931
|
+
_path: [...path, "children"],
|
|
1932
|
+
});
|
|
1860
1933
|
}
|
|
1861
1934
|
|
|
1862
1935
|
return el;
|
|
1863
1936
|
}
|
|
1864
1937
|
|
|
1938
|
+
/**
|
|
1939
|
+
* Draw a whole-`children` template, the way the site build's template pass expands one (spec.md
|
|
1940
|
+
* §8.4). This is what an item's or an entry's parsed body looks like to something that renders the
|
|
1941
|
+
* source document rather than the build's output: Studio's Preview, chiefly.
|
|
1942
|
+
*
|
|
1943
|
+
* A result that is not an array draws nothing, because a plain string is not a `children` value.
|
|
1944
|
+
* The children are rendered in a detached scope of their own, replaced when the template's value
|
|
1945
|
+
* changes (a collection that was still loading), and with tracking paused for the reason
|
|
1946
|
+
* {@link renderSwitch} gives: the bindings inside track for themselves.
|
|
1947
|
+
*
|
|
1948
|
+
* @param {Element} el - The element whose children these are
|
|
1949
|
+
* @param {string} template - The `children` value
|
|
1950
|
+
* @param {JxScope} state
|
|
1951
|
+
* @param {JxRenderOptions} options - `_path` is the `children` position of `el`
|
|
1952
|
+
*/
|
|
1953
|
+
function renderComputedChildren(
|
|
1954
|
+
el: Element,
|
|
1955
|
+
template: string,
|
|
1956
|
+
state: JxScope,
|
|
1957
|
+
options: JxRenderOptions,
|
|
1958
|
+
): void {
|
|
1959
|
+
const path = options._path ?? [];
|
|
1960
|
+
let live: EffectScope | null = null;
|
|
1961
|
+
const retire = (): void => {
|
|
1962
|
+
// Detached before it is stopped, for the reason `teardown` in `renderMappedArray` gives.
|
|
1963
|
+
el.replaceChildren();
|
|
1964
|
+
live?.stop();
|
|
1965
|
+
live = null;
|
|
1966
|
+
};
|
|
1967
|
+
bindTemplate(
|
|
1968
|
+
template,
|
|
1969
|
+
() => {
|
|
1970
|
+
const nodes = evaluateValueTemplate(template, state);
|
|
1971
|
+
retire();
|
|
1972
|
+
if (!Array.isArray(nodes)) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const scope = effectScope(true);
|
|
1976
|
+
live = scope;
|
|
1977
|
+
pauseTracking();
|
|
1978
|
+
try {
|
|
1979
|
+
scope.run(() => {
|
|
1980
|
+
/* SAFETY: a children template that yields an array stands for the child list itself, the
|
|
1981
|
+
same reading the site build's template pass gives it. */
|
|
1982
|
+
for (const [i, child] of (nodes as (JxElement | string)[]).entries()) {
|
|
1983
|
+
el.append(renderNode(child, state, { ...options, _path: [...path, i] }));
|
|
1984
|
+
}
|
|
1985
|
+
});
|
|
1986
|
+
} finally {
|
|
1987
|
+
resetTracking();
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
retire,
|
|
1991
|
+
);
|
|
1992
|
+
onScopeDispose(() => live?.stop(), true);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1865
1995
|
// ─── Template string utilities ────────────────────────────────────────────────
|
|
1866
1996
|
|
|
1867
1997
|
/**
|
|
@@ -3297,29 +3427,32 @@ export async function resolvePrototype(
|
|
|
3297
3427
|
}
|
|
3298
3428
|
});
|
|
3299
3429
|
|
|
3300
|
-
const doFetch = () =>
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
.then((r): Promise<JsonValue> => (r.ok ? r.json() : Promise.reject(r.statusText)))
|
|
3310
|
-
.then((d) => {
|
|
3311
|
-
s.value = d;
|
|
3430
|
+
const doFetch = () => {
|
|
3431
|
+
holdTemplateErrorsFor(
|
|
3432
|
+
fetch(url, {
|
|
3433
|
+
method: def.method ?? "GET",
|
|
3434
|
+
signal: controller.signal,
|
|
3435
|
+
...(def.headers && { headers: def.headers }),
|
|
3436
|
+
...(def.body && {
|
|
3437
|
+
body: isNonNullObject(def.body) ? JSON.stringify(def.body) : def.body,
|
|
3438
|
+
}),
|
|
3312
3439
|
})
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
s.value =
|
|
3316
|
-
}
|
|
3317
|
-
|
|
3440
|
+
.then((r): Promise<JsonValue> => (r.ok ? r.json() : Promise.reject(r.statusText)))
|
|
3441
|
+
.then((d) => {
|
|
3442
|
+
s.value = d;
|
|
3443
|
+
})
|
|
3444
|
+
.catch((error) => {
|
|
3445
|
+
if (!(error instanceof Error && error.name === "AbortError")) {
|
|
3446
|
+
s.value = { error: String(error) };
|
|
3447
|
+
}
|
|
3448
|
+
}),
|
|
3449
|
+
);
|
|
3450
|
+
};
|
|
3318
3451
|
|
|
3319
3452
|
if (debounceMs > 0) {
|
|
3320
3453
|
debounceTimer = setTimeout(doFetch, debounceMs);
|
|
3321
3454
|
} else {
|
|
3322
|
-
|
|
3455
|
+
doFetch();
|
|
3323
3456
|
}
|
|
3324
3457
|
});
|
|
3325
3458
|
}
|
|
@@ -3850,24 +3983,25 @@ async function resolveViaDevProxy(def: JxPrototypeDef, state: JxScope, key: stri
|
|
|
3850
3983
|
|
|
3851
3984
|
// Always wrap in ref for reactivity
|
|
3852
3985
|
const s: Ref<JxScopeValue> = ref(null);
|
|
3986
|
+
const resolveInto = (resolvedConfig: JxScope) => {
|
|
3987
|
+
holdTemplateErrorsFor(
|
|
3988
|
+
doResolve(resolvedConfig)
|
|
3989
|
+
.then((value) => {
|
|
3990
|
+
s.value = value;
|
|
3991
|
+
})
|
|
3992
|
+
.catch((error) => console.error("Jx dev proxy:", error)),
|
|
3993
|
+
);
|
|
3994
|
+
};
|
|
3853
3995
|
if (hasTemplates) {
|
|
3854
3996
|
effect(() => {
|
|
3855
3997
|
const resolvedConfig: JxScope = {};
|
|
3856
3998
|
for (const [k, v] of Object.entries(config)) {
|
|
3857
3999
|
resolvedConfig[k] = isTemplateString(v) ? evaluateTemplate(v, state) : v;
|
|
3858
4000
|
}
|
|
3859
|
-
|
|
3860
|
-
.then((value) => {
|
|
3861
|
-
s.value = value;
|
|
3862
|
-
})
|
|
3863
|
-
.catch((error) => console.error("Jx dev proxy:", error));
|
|
4001
|
+
resolveInto(resolvedConfig);
|
|
3864
4002
|
});
|
|
3865
4003
|
} else {
|
|
3866
|
-
|
|
3867
|
-
.then((value) => {
|
|
3868
|
-
s.value = value;
|
|
3869
|
-
})
|
|
3870
|
-
.catch((error) => console.error("Jx dev proxy:", error));
|
|
4004
|
+
resolveInto(config);
|
|
3871
4005
|
}
|
|
3872
4006
|
return s;
|
|
3873
4007
|
}
|
|
@@ -3900,19 +4034,21 @@ function latestServerCall(
|
|
|
3900
4034
|
const n = seq;
|
|
3901
4035
|
/* The executor runs before the constructor returns, so the call still starts synchronously,
|
|
3902
4036
|
and a throw inside it rejects this promise instead of escaping to the caller. */
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
.then((result) => {
|
|
3907
|
-
if (n === seq) {
|
|
3908
|
-
s.value = result;
|
|
3909
|
-
}
|
|
4037
|
+
holdTemplateErrorsFor(
|
|
4038
|
+
new Promise<JsonValue>((settle) => {
|
|
4039
|
+
settle(invoke(args));
|
|
3910
4040
|
})
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4041
|
+
.then((result) => {
|
|
4042
|
+
if (n === seq) {
|
|
4043
|
+
s.value = result;
|
|
4044
|
+
}
|
|
4045
|
+
})
|
|
4046
|
+
.catch((error) => {
|
|
4047
|
+
if (n === seq) {
|
|
4048
|
+
console.error(label, error);
|
|
4049
|
+
}
|
|
4050
|
+
}),
|
|
4051
|
+
);
|
|
3916
4052
|
};
|
|
3917
4053
|
}
|
|
3918
4054
|
|