@velarscript/web 0.12.0 → 0.13.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/README.md +2 -2
- package/dist/analyzer.d.ts +164 -23
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +1254 -133
- package/dist/analyzer.js.map +1 -1
- package/dist/ast.d.ts +11 -0
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +22 -0
- package/dist/ast.js.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +69 -44
- package/dist/compiler.js.map +1 -1
- package/dist/css-string.d.ts +32 -0
- package/dist/css-string.d.ts.map +1 -0
- package/dist/css-string.js +66 -0
- package/dist/css-string.js.map +1 -0
- package/dist/css-tokens.d.ts +16 -0
- package/dist/css-tokens.d.ts.map +1 -1
- package/dist/css-tokens.js +84 -0
- package/dist/css-tokens.js.map +1 -1
- package/dist/elements.d.ts +55 -0
- package/dist/elements.d.ts.map +1 -1
- package/dist/elements.js +152 -0
- package/dist/elements.js.map +1 -1
- package/dist/emitter.d.ts +10 -0
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +588 -159
- package/dist/emitter.js.map +1 -1
- package/dist/keyframes.d.ts +17 -0
- package/dist/keyframes.d.ts.map +1 -1
- package/dist/keyframes.js +28 -9
- package/dist/keyframes.js.map +1 -1
- package/dist/lexer.d.ts +21 -7
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +32 -12
- package/dist/lexer.js.map +1 -1
- package/dist/look-static.d.ts.map +1 -1
- package/dist/look-static.js +120 -20
- package/dist/look-static.js.map +1 -1
- package/dist/look.d.ts +15 -0
- package/dist/look.d.ts.map +1 -1
- package/dist/look.js +7 -2
- package/dist/look.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +15 -20
- package/dist/parser.js.map +1 -1
- package/dist/runtime-foundation.d.ts.map +1 -1
- package/dist/runtime-foundation.js +321 -70
- package/dist/runtime-foundation.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +320 -179
- package/dist/runtime.js.map +1 -1
- package/dist/stable-order.d.ts +16 -0
- package/dist/stable-order.d.ts.map +1 -0
- package/dist/stable-order.js +18 -0
- package/dist/stable-order.js.map +1 -0
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -16
- package/dist/types.js.map +1 -1
- package/dist/websocket-runtime.d.ts.map +1 -1
- package/dist/websocket-runtime.js +9 -5
- package/dist/websocket-runtime.js.map +1 -1
- package/dist/worker-runtime.d.ts.map +1 -1
- package/dist/worker-runtime.js +9 -7
- package/dist/worker-runtime.js.map +1 -1
- package/package.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { VELAR_ERROR_NORMALIZATION_RUNTIME, VELAR_REACTIVE_BRIDGE_MODULE, VELAR_REACTIVE_BRIDGE_MODULE_SOURCE, VELAR_RUNTIME_REGISTRY_KEY, VELAR_RUNTIME_SCHEMA_VERSION, VELAR_STRICT_JSON_RUNTIME, VELAR_TEXT_METHOD_RUNTIME, VELAR_TYPE_REGISTRY_RUNTIME, VELAR_UTF8_RUNTIME, } from "@velarscript/compiler/extension";
|
|
2
|
+
import { CSS_STRING_RUNTIME } from "./css-string.js";
|
|
3
|
+
import { LOOK_TRANSITION_PROPERTY_KEYWORDS } from "./look.js";
|
|
2
4
|
import { WEB_DOM_HOST_RUNTIME, WEB_ERROR_HOST_RUNTIME, WEB_RUNTIME_FOUNDATION } from "./runtime-foundation.js";
|
|
3
5
|
import { VELAR_WEB_WORKER_RUNTIME } from "./worker-runtime.js";
|
|
4
6
|
import { VELAR_WEB_WEBSOCKET_RUNTIME } from "./websocket-runtime.js";
|
|
@@ -96,8 +98,18 @@ function __velarListReactiveRuntime() {
|
|
|
96
98
|
__velarRequireListIntrinsics();
|
|
97
99
|
const descriptor = __velarListReflectApply(__velarListGetOwnPropertyDescriptor, __velarListNativeObject, [globalThis, __velarListRuntimeKey]);
|
|
98
100
|
const runtime = descriptor && "value" in descriptor ? descriptor.value : null;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
// No registry means no reactive runtime in this realm, which is ordinary Web
|
|
102
|
+
// behavior. A registry from another generation is a mixed build: reading past
|
|
103
|
+
// it would copy raw values and silently lose every collectionRead dependency,
|
|
104
|
+
// so it fails closed ahead of the callable duck-checks. The one installer
|
|
105
|
+
// that writes this slot always writes version, toRaw, collectionRead and
|
|
106
|
+
// report together, so this test is the same one Core, the JSON bridge and the
|
|
107
|
+
// Web foundation apply to the same slot — one tolerance rule per realm.
|
|
108
|
+
if (!runtime || (typeof runtime !== "object" && typeof runtime !== "function")) return null;
|
|
109
|
+
if (runtime.version !== ${JSON.stringify(VELAR_RUNTIME_SCHEMA_VERSION)}) {
|
|
110
|
+
throw new TypeError("VelarScript reactive runtime schema " + (typeof runtime.version === "string" ? runtime.version : "(unknown)") + " does not match this module's schema ${VELAR_RUNTIME_SCHEMA_VERSION}; one build mixed two generations of @velarscript/* — run 'npm ls @velarscript/compiler' and pin one version");
|
|
111
|
+
}
|
|
112
|
+
return typeof runtime.toRaw === "function" && typeof runtime.collectionRead === "function" ? runtime : null;
|
|
101
113
|
}
|
|
102
114
|
function __velarRequireList(value, name) {
|
|
103
115
|
__velarRequireListIntrinsics();
|
|
@@ -171,6 +183,17 @@ function __velarFreezeOptionsValue(value) {
|
|
|
171
183
|
return __velarOptionsReflectApply(__velarOptionsFreeze, __velarOptionsNativeObject, [value]);
|
|
172
184
|
}
|
|
173
185
|
function __velarOptions(value, name, allowed) {
|
|
186
|
+
return __velarOptionsRecord(value, name, allowed, false);
|
|
187
|
+
}
|
|
188
|
+
// A live props store publishes every prop as a tracked getter, so the rule that
|
|
189
|
+
// a field holds a data value cannot hold for it -- and the read through that
|
|
190
|
+
// getter is exactly what subscribes the reader to the prop. Every other part of
|
|
191
|
+
// the strict record rule still does: a plain record, no symbol fields, and no
|
|
192
|
+
// field outside the declared set.
|
|
193
|
+
function __velarLiveOptions(value, name, allowed) {
|
|
194
|
+
return __velarOptionsRecord(value, name, allowed, true);
|
|
195
|
+
}
|
|
196
|
+
function __velarOptionsRecord(value, name, allowed, live) {
|
|
174
197
|
__velarRequireOptionsIntrinsics();
|
|
175
198
|
let prototype = null;
|
|
176
199
|
if (value && typeof value === "object") {
|
|
@@ -190,8 +213,10 @@ function __velarOptions(value, name, allowed) {
|
|
|
190
213
|
const key = keys[index];
|
|
191
214
|
if (!__velarOptionsReflectApply(__velarOptionsSetHas, allowed, [key])) throw new TypeError("Unknown " + name + " field '" + key + "'");
|
|
192
215
|
const descriptor = __velarOptionsReflectApply(__velarOptionsGetOwnPropertyDescriptor, __velarOptionsNativeObject, [value, key]);
|
|
193
|
-
if (!descriptor?.enumerable || !("value" in descriptor))
|
|
194
|
-
|
|
216
|
+
if (!descriptor?.enumerable || (!live && !("value" in descriptor))) {
|
|
217
|
+
throw new TypeError(name + " field '" + key + "' must be an enumerable " + (live ? "field" : "data value"));
|
|
218
|
+
}
|
|
219
|
+
output[key] = "value" in descriptor ? descriptor.value : value[key];
|
|
195
220
|
}
|
|
196
221
|
return __velarFreezeOptionsValue(output);
|
|
197
222
|
}
|
|
@@ -514,6 +539,32 @@ function lookResult(value) {
|
|
|
514
539
|
if (value.length > 1024 * 1024) throw new RangeError("A constructed Look value cannot exceed 1 MiB");
|
|
515
540
|
return value;
|
|
516
541
|
}
|
|
542
|
+
// A standard module ships to the browser as its own source, so it carries the
|
|
543
|
+
// CSS string serializer rather than reaching for the emitted runtime's copy.
|
|
544
|
+
// css-string.ts publishes the one implementation both spellings read.
|
|
545
|
+
${CSS_STRING_RUNTIME.trim()}
|
|
546
|
+
// charter section 3549: the two transition longhands take the vocabularies the
|
|
547
|
+
// matching builders take. The longhand read a closed set while the builder read
|
|
548
|
+
// nothing, so 'backgroundColor' -- the camelCase spelling every other Look
|
|
549
|
+
// property is written with -- compiled clean and reached the browser as a
|
|
550
|
+
// declaration it discards. The set is the analyzer's, derived from the Look
|
|
551
|
+
// property table rather than restated here.
|
|
552
|
+
const transitionProperties = ${JSON.stringify([...LOOK_TRANSITION_PROPERTY_KEYWORDS])};
|
|
553
|
+
function lookTransitionProperty(value) {
|
|
554
|
+
value = lookText(value, "Transition property");
|
|
555
|
+
if (transitionProperties.includes(value)) return value;
|
|
556
|
+
// The longhand's diagnostic teaches the CSS spelling, so the rejection here
|
|
557
|
+
// teaches the same one rather than only naming the value.
|
|
558
|
+
let dashed = "";
|
|
559
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
560
|
+
const code = value.charCodeAt(index);
|
|
561
|
+
dashed += code >= 65 && code <= 90 ? "-" + value[index].toLowerCase() : value[index];
|
|
562
|
+
}
|
|
563
|
+
if (dashed !== value && transitionProperties.includes(dashed)) {
|
|
564
|
+
throw new TypeError("Transition property '" + value + "' is not a CSS property name; did you mean '" + dashed + "'?");
|
|
565
|
+
}
|
|
566
|
+
throw new TypeError("Transition property '" + value + "' is not an animatable CSS property name");
|
|
567
|
+
}
|
|
517
568
|
function lookType(name, predicate) {
|
|
518
569
|
return __velarRegisterRuntimeType(Object.freeze({
|
|
519
570
|
is(value) { return predicate(value); },
|
|
@@ -587,7 +638,7 @@ export function shadow(x, y, blur, value, spread = "0px", inset = false) {
|
|
|
587
638
|
export function linearGradient(angle, start, end) {
|
|
588
639
|
return lookResult("linear-gradient(" + lookVisual(angle, "Gradient angle") + ", " + lookText(start, "Gradient start") + ", " + lookText(end, "Gradient end") + ")");
|
|
589
640
|
}
|
|
590
|
-
export function asset(path) { return lookResult("url(" +
|
|
641
|
+
export function asset(path) { return lookResult("url(" + __velarCssString(lookText(path, "Asset path")) + ")"); }
|
|
591
642
|
export function minmax(minimum, maximum) { return lookResult("minmax(" + lookVisual(minimum, "Minimum track") + ", " + lookVisual(maximum, "Maximum track") + ")"); }
|
|
592
643
|
export function repeat(count, size) { return lookResult("repeat(" + lookVisual(count, "Repeat count") + ", " + lookVisual(size, "Repeat size") + ")"); }
|
|
593
644
|
export function tracks(first, ...rest) {
|
|
@@ -597,7 +648,7 @@ export function tracks(first, ...rest) {
|
|
|
597
648
|
}
|
|
598
649
|
export function transition(property, duration, easing = "ease", delay) {
|
|
599
650
|
const suffix = delay === undefined ? "" : " " + lookVisual(delay, "Transition delay");
|
|
600
|
-
return lookResult(
|
|
651
|
+
return lookResult(lookTransitionProperty(property) + " " + lookVisual(duration, "Transition duration") + " " + lookText(easing, "Transition easing") + suffix);
|
|
601
652
|
}
|
|
602
653
|
export function spacing(first, second, third, fourth) {
|
|
603
654
|
return lookResult([first, second, third, fourth].filter((value) => value !== undefined)
|
|
@@ -720,7 +771,7 @@ function reportLinkEventFailure(failure) {
|
|
|
720
771
|
const error = __velarNormalizeError(failure);
|
|
721
772
|
const runtime = globalThis[Symbol.for(${JSON.stringify(VELAR_RUNTIME_REGISTRY_KEY)})];
|
|
722
773
|
if (runtime && typeof runtime.report === "function") runtime.report(error, { phase: "event", detail: "link", unhandled: true });
|
|
723
|
-
else
|
|
774
|
+
else __velarBrowserCallCaptured(__velarBrowserQueueMicrotask, __velarBrowserWindow, [() => { throw error; }], "queueMicrotask");
|
|
724
775
|
}
|
|
725
776
|
|
|
726
777
|
export function domId(prefix = "velar") {
|
|
@@ -936,8 +987,10 @@ export function currentRoute() {
|
|
|
936
987
|
return Object.freeze({ path, params: new Map(), query: queryValues(), hash: routeHash() });
|
|
937
988
|
}
|
|
938
989
|
|
|
939
|
-
|
|
940
|
-
|
|
990
|
+
const headPropFields = __velarOptionFields(["title", "description", "canonical", "robots", "image", "themeColor", "language"]);
|
|
991
|
+
|
|
992
|
+
function headMetadata(props) {
|
|
993
|
+
props = __velarLiveOptions(props, "Head props", headPropFields);
|
|
941
994
|
let { title, description = "", canonical = "", robots = "", image = "", themeColor = "", language = "" } = props;
|
|
942
995
|
title = __velarString(title, "Head title");
|
|
943
996
|
description = __velarString(description, "Head description");
|
|
@@ -955,32 +1008,122 @@ export function Head(props) {
|
|
|
955
1008
|
if (language && !/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u.test(language)) {
|
|
956
1009
|
throw new TypeError("Head language must be a simple BCP 47 language tag");
|
|
957
1010
|
}
|
|
958
|
-
|
|
1011
|
+
return { title, description, canonical, robots, image, themeColor, language };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Head follows Vel's ordinary rules: its props are read inside a DOM observer,
|
|
1015
|
+
// so a title built from state tracks that state. Reading each prop exactly once
|
|
1016
|
+
// at construction froze '<Head title={f"Inbox ({unread})"} />' at whatever
|
|
1017
|
+
// count the first render saw, and the frozen-read report that exists to catch
|
|
1018
|
+
// exactly that is muted on the snapshot path by construction.
|
|
1019
|
+
// The observer is created at construction so the props are validated where they
|
|
1020
|
+
// always were, and the document is not touched until mount: what a Head owns it
|
|
1021
|
+
// takes on insertion and gives back on removal.
|
|
1022
|
+
export function Head(props) {
|
|
1023
|
+
let live = false;
|
|
1024
|
+
let pending = null;
|
|
1025
|
+
let applied = null;
|
|
959
1026
|
let previousTitle = "";
|
|
960
1027
|
let previousLanguage = null;
|
|
961
1028
|
let restorers = [];
|
|
962
|
-
|
|
1029
|
+
const release = () => {
|
|
1030
|
+
if (applied === null) return;
|
|
1031
|
+
if (document.title === applied.title) document.title = previousTitle;
|
|
1032
|
+
if (applied.language && document.documentElement.getAttribute("lang") === applied.language) {
|
|
1033
|
+
if (previousLanguage == null) document.documentElement.removeAttribute("lang");
|
|
1034
|
+
else document.documentElement.setAttribute("lang", previousLanguage);
|
|
1035
|
+
}
|
|
1036
|
+
for (const restore of restorers.reverse()) restore();
|
|
1037
|
+
restorers = [];
|
|
1038
|
+
applied = null;
|
|
1039
|
+
};
|
|
1040
|
+
const take = (next) => {
|
|
963
1041
|
previousTitle = document.title;
|
|
964
|
-
document.title = title;
|
|
1042
|
+
document.title = next.title;
|
|
965
1043
|
previousLanguage = document.documentElement.getAttribute("lang");
|
|
966
|
-
if (language) document.documentElement.setAttribute("lang", language);
|
|
1044
|
+
if (next.language) document.documentElement.setAttribute("lang", next.language);
|
|
967
1045
|
restorers = [
|
|
968
|
-
ownHead('meta[name="description"]', "meta", "name", "description", "content", description),
|
|
969
|
-
ownHead('link[rel="canonical"]', "link", "rel", "canonical", "href", canonical),
|
|
970
|
-
ownHead('meta[name="robots"]', "meta", "name", "robots", "content", robots),
|
|
971
|
-
ownHead('meta[property="og:image"]', "meta", "property", "og:image", "content", image),
|
|
972
|
-
ownHead('meta[name="theme-color"]', "meta", "name", "theme-color", "content", themeColor),
|
|
1046
|
+
ownHead('meta[name="description"]', "meta", "name", "description", "content", next.description),
|
|
1047
|
+
ownHead('link[rel="canonical"]', "link", "rel", "canonical", "href", next.canonical),
|
|
1048
|
+
ownHead('meta[name="robots"]', "meta", "name", "robots", "content", next.robots),
|
|
1049
|
+
ownHead('meta[property="og:image"]', "meta", "property", "og:image", "content", next.image),
|
|
1050
|
+
ownHead('meta[name="theme-color"]', "meta", "name", "theme-color", "content", next.themeColor),
|
|
973
1051
|
];
|
|
1052
|
+
applied = next;
|
|
1053
|
+
};
|
|
1054
|
+
// The observer runs once here, so an invalid Head is still rejected before a
|
|
1055
|
+
// single DOM call is made -- and the validating read happens inside the
|
|
1056
|
+
// observer, where D70's frozen-read report correctly ignores it.
|
|
1057
|
+
const observer = webObserve(() => {
|
|
1058
|
+
pending = headMetadata(props);
|
|
1059
|
+
if (!live) return;
|
|
1060
|
+
release();
|
|
1061
|
+
take(pending);
|
|
1062
|
+
}, "head", "Head");
|
|
1063
|
+
const node = document.createComment("velar head");
|
|
1064
|
+
return component(node, () => {
|
|
1065
|
+
live = true;
|
|
1066
|
+
take(pending);
|
|
974
1067
|
}, () => {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (previousLanguage == null) document.documentElement.removeAttribute("lang");
|
|
978
|
-
else document.documentElement.setAttribute("lang", previousLanguage);
|
|
979
|
-
}
|
|
980
|
-
for (const restore of restorers.reverse()) restore();
|
|
1068
|
+
observer.stop();
|
|
1069
|
+
release();
|
|
981
1070
|
});
|
|
982
1071
|
}
|
|
983
1072
|
|
|
1073
|
+
// A DOM-phase observer built from the shared runtime registry. A component the
|
|
1074
|
+
// runtime implements is rendered content like any other, so its reads belong in
|
|
1075
|
+
// the queue every other rendered position uses: the reactive graph settles
|
|
1076
|
+
// first, then the document is written once.
|
|
1077
|
+
function webObserve(run, detail, componentName) {
|
|
1078
|
+
const runtime = globalThis[Symbol.for(${JSON.stringify(VELAR_RUNTIME_REGISTRY_KEY)})];
|
|
1079
|
+
if (!runtime || typeof runtime.runTracked !== "function" || typeof runtime.schedule !== "function"
|
|
1080
|
+
|| typeof runtime.cleanupObserver !== "function") {
|
|
1081
|
+
throw new TypeError("A VelarScript Web component requires the VelarScript Web runtime");
|
|
1082
|
+
}
|
|
1083
|
+
let initial = true;
|
|
1084
|
+
const observer = {
|
|
1085
|
+
mode: "dom",
|
|
1086
|
+
stopped: false,
|
|
1087
|
+
dependencies: new Set(),
|
|
1088
|
+
run() {
|
|
1089
|
+
if (observer.stopped) return;
|
|
1090
|
+
// The first run happens while the component is being constructed, and
|
|
1091
|
+
// construction is transactional: that failure belongs to the caller.
|
|
1092
|
+
// Every later run is an update, whose failure is reported.
|
|
1093
|
+
try { runtime.runTracked(observer, run); }
|
|
1094
|
+
catch (failure) {
|
|
1095
|
+
// Construction is transactional, and now that these components read
|
|
1096
|
+
// live props the first run has already subscribed to whatever it read
|
|
1097
|
+
// on the way to failing. A component that never came into existence
|
|
1098
|
+
// leaves nothing behind that a later write could re-run.
|
|
1099
|
+
if (initial) { observer.stop(); throw failure; }
|
|
1100
|
+
const error = __velarNormalizeError(failure);
|
|
1101
|
+
if (typeof runtime.report === "function") runtime.report(error, { phase: "render", detail, component: componentName, unhandled: true });
|
|
1102
|
+
else __velarBrowserCallCaptured(__velarBrowserQueueMicrotask, __velarBrowserWindow, [() => { throw error; }], "queueMicrotask");
|
|
1103
|
+
}
|
|
1104
|
+
finally { initial = false; }
|
|
1105
|
+
},
|
|
1106
|
+
notify() { if (!observer.stopped) runtime.schedule(observer); },
|
|
1107
|
+
stop() { observer.stopped = true; runtime.cleanupObserver(observer); },
|
|
1108
|
+
};
|
|
1109
|
+
observer.run();
|
|
1110
|
+
return observer;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Reading props inside an observer is what makes a component follow its state;
|
|
1114
|
+
// building a subtree there is not. A component the runtime implements calls
|
|
1115
|
+
// another component directly rather than through the emitted instantiation
|
|
1116
|
+
// path, so it needs that path's guard: the reactive graph records a dependency
|
|
1117
|
+
// only for an observer it will run again, and a stopped one is exactly the
|
|
1118
|
+
// shape it refuses to record.
|
|
1119
|
+
function webUntracked(run) {
|
|
1120
|
+
const runtime = globalThis[Symbol.for(${JSON.stringify(VELAR_RUNTIME_REGISTRY_KEY)})];
|
|
1121
|
+
if (!runtime || typeof runtime.runTracked !== "function") {
|
|
1122
|
+
throw new TypeError("A VelarScript Web component requires the VelarScript Web runtime");
|
|
1123
|
+
}
|
|
1124
|
+
return runtime.runTracked({ mode: "dom", stopped: true, dependencies: new Set(), spareDependencies: null, notify() {} }, run);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
984
1127
|
function ownHead(selector, tag, identityName, identityValue, valueName, value) {
|
|
985
1128
|
if (!value) return () => {};
|
|
986
1129
|
let element = document.head.querySelector(selector);
|
|
@@ -1019,8 +1162,10 @@ export function announce(message, priority = "polite") {
|
|
|
1019
1162
|
return null;
|
|
1020
1163
|
}
|
|
1021
1164
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1165
|
+
const routerPropFields = __velarOptionFields(["routes", "fallback"]);
|
|
1166
|
+
|
|
1167
|
+
function routerTable(props) {
|
|
1168
|
+
props = __velarLiveOptions(props, "Router props", routerPropFields);
|
|
1024
1169
|
let { routes, fallback = null } = props;
|
|
1025
1170
|
const routeItems = __velarRequireList(routes, "Router routes");
|
|
1026
1171
|
if (routeItems.length > 10000) throw new RangeError("A Router cannot contain more than 10000 routes");
|
|
@@ -1031,7 +1176,15 @@ export function Router(props) {
|
|
|
1031
1176
|
return Object.freeze({ path: item.path, component: item.component, matcher: compileRoutePath(item.path) });
|
|
1032
1177
|
});
|
|
1033
1178
|
if (fallback != null && typeof fallback !== "function") throw new TypeError("A Router fallback must be a component");
|
|
1034
|
-
|
|
1179
|
+
return { routes, fallback };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
export function Router(props) {
|
|
1183
|
+
// The host element is created by the observer's first run, after the routes
|
|
1184
|
+
// table has been checked: a refused table must not leave a browser node
|
|
1185
|
+
// behind on its way out.
|
|
1186
|
+
let node = null;
|
|
1187
|
+
let table = null;
|
|
1035
1188
|
let active = null;
|
|
1036
1189
|
let mounted = false;
|
|
1037
1190
|
const notFound = ({ route }) => {
|
|
@@ -1047,9 +1200,9 @@ export function Router(props) {
|
|
|
1047
1200
|
const render = () => {
|
|
1048
1201
|
try {
|
|
1049
1202
|
const path = applicationPath(webLocationField("pathname", __velarBrowserLocationPathname));
|
|
1050
|
-
const match = path === null ? null : matchRoute(routes, path);
|
|
1203
|
+
const match = path === null ? null : matchRoute(table.routes, path);
|
|
1051
1204
|
const context = match?.context ?? { path: path ?? "/", params: new Map(), query: queryValues(), hash: routeHash() };
|
|
1052
|
-
const next = match ? match.item.component({ route: context }) : (fallback ?? notFound)({ route: context });
|
|
1205
|
+
const next = webUntracked(() => (match ? match.item.component({ route: context }) : (table.fallback ?? notFound)({ route: context })));
|
|
1053
1206
|
if (!next || !next.__velarComponent) throw new TypeError("A VelarScript Router target must render a component");
|
|
1054
1207
|
if (active) active.destroy();
|
|
1055
1208
|
active = next;
|
|
@@ -1062,35 +1215,69 @@ export function Router(props) {
|
|
|
1062
1215
|
if (runtime && typeof runtime.report === "function") {
|
|
1063
1216
|
runtime.report(error, { phase: "render", detail: "router", component: "Router", unhandled: true });
|
|
1064
1217
|
} else {
|
|
1065
|
-
|
|
1218
|
+
__velarBrowserCallCaptured(__velarBrowserQueueMicrotask, __velarBrowserWindow, [() => { throw error; }], "queueMicrotask");
|
|
1066
1219
|
}
|
|
1067
1220
|
}
|
|
1068
1221
|
};
|
|
1069
1222
|
const changed = () => render();
|
|
1070
1223
|
let removeRouteListener = null;
|
|
1071
|
-
|
|
1224
|
+
// The routes table is read inside the observer that renders from it, so a
|
|
1225
|
+
// table or fallback built from state re-renders the position that shows it.
|
|
1226
|
+
// A constant table subscribes to nothing and recomputes nothing.
|
|
1227
|
+
const observer = webObserve(() => {
|
|
1228
|
+
table = routerTable(props);
|
|
1229
|
+
if (node === null) node = __velarDomCreateElement("velar-router");
|
|
1230
|
+
render();
|
|
1231
|
+
}, "router", "Router");
|
|
1072
1232
|
return component(node, () => {
|
|
1073
1233
|
mounted = true;
|
|
1074
1234
|
removeRouteListener = __velarBrowserListenGlobal("popstate", changed);
|
|
1075
1235
|
if (active) active.__mount();
|
|
1076
1236
|
}, () => {
|
|
1237
|
+
observer.stop();
|
|
1077
1238
|
if (removeRouteListener) removeRouteListener();
|
|
1078
1239
|
if (active) active.destroy(false);
|
|
1079
1240
|
});
|
|
1080
1241
|
}
|
|
1081
1242
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
to = __velarString(to, "Link target");
|
|
1243
|
+
const linkPropFields = __velarOptionFields(["to", "replace", "class", "look", "children"]);
|
|
1244
|
+
|
|
1245
|
+
function linkTarget(props) {
|
|
1246
|
+
const to = __velarString(props.to, "Link target");
|
|
1086
1247
|
if (to.length > 2 * 1024 * 1024) throw new RangeError("Link targets cannot exceed 2 MiB");
|
|
1248
|
+
requireNavigableTarget(to, "Link target");
|
|
1249
|
+
const replace = props.replace === undefined ? false : props.replace;
|
|
1087
1250
|
__velarBool(replace, "Link replace");
|
|
1088
1251
|
const external = isExternal(to);
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1252
|
+
return { to, replace, external, href: external ? to : internalHref(to) };
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Which fields a Link carries is fixed where the Link is written, so the record
|
|
1256
|
+
// shape is checked once; the values behind those fields are read inside the
|
|
1257
|
+
// observer that consumes them, which is what makes '<Link to={path}>' follow
|
|
1258
|
+
// the state 'path' holds. 'children' is rendered content owned by the position
|
|
1259
|
+
// that shows it, so it is taken once, where that position builds it -- that is
|
|
1260
|
+
// why the shape check sits here rather than inside the observer the way Head's
|
|
1261
|
+
// does: reading the slot again on every update would build content nothing
|
|
1262
|
+
// shows. Behind a live props store both reads answer from the same cached
|
|
1263
|
+
// derived value, so the author's prop expression still runs exactly once.
|
|
1264
|
+
export function Link(props) {
|
|
1265
|
+
const children = __velarLiveOptions(props, "Link props", linkPropFields).children;
|
|
1266
|
+
// The anchor is created by the observer's first run, after the target has
|
|
1267
|
+
// been checked: a refused target must not leave a browser node behind.
|
|
1268
|
+
let node = null;
|
|
1269
|
+
let target = null;
|
|
1270
|
+
let releaseHost = null;
|
|
1271
|
+
const observer = webObserve(() => {
|
|
1272
|
+
const next = linkTarget(props);
|
|
1273
|
+
if (node === null) node = __velarDomCreateElement("a");
|
|
1274
|
+
if (releaseHost) { releaseHost(); releaseHost = null; }
|
|
1275
|
+
releaseHost = forwardHost(node, props);
|
|
1276
|
+
node.href = next.href;
|
|
1277
|
+
target = next;
|
|
1278
|
+
}, "link", "Link");
|
|
1279
|
+
try { append(node, children); }
|
|
1280
|
+
catch (failure) { observer.stop(); if (releaseHost) releaseHost(); throw failure; }
|
|
1094
1281
|
const clicked = (event) => {
|
|
1095
1282
|
try {
|
|
1096
1283
|
const defaultPrevented = webEventField(event, "defaultPrevented", webEventDefaultPrevented);
|
|
@@ -1106,53 +1293,80 @@ export function Link(props) {
|
|
|
1106
1293
|
throw new TypeError("Link received an invalid click event");
|
|
1107
1294
|
}
|
|
1108
1295
|
if (defaultPrevented || button !== 0 || metaKey || ctrlKey || shiftKey || altKey) return;
|
|
1109
|
-
if (external) return;
|
|
1296
|
+
if (target.external) return;
|
|
1110
1297
|
if (webUrlField(webUrl(node.href, webLocationField("href", __velarBrowserLocationHref)), "origin", __velarBrowserUrlOrigin)
|
|
1111
1298
|
!== webLocationField("origin", __velarBrowserLocationOrigin)) return;
|
|
1112
1299
|
webEventCall(event, "preventDefault", webEventPreventDefault);
|
|
1113
|
-
navigate(to, { replace });
|
|
1300
|
+
navigate(target.to, { replace: target.replace });
|
|
1114
1301
|
} catch (failure) { reportLinkEventFailure(failure); }
|
|
1115
1302
|
};
|
|
1116
1303
|
let removeClick = null;
|
|
1117
|
-
return component(node, () => { removeClick = __velarBrowserListen(node, "click", clicked); }, () => {
|
|
1304
|
+
return component(node, () => { removeClick = __velarBrowserListen(node, "click", clicked); }, () => {
|
|
1305
|
+
observer.stop();
|
|
1306
|
+
if (removeClick) removeClick();
|
|
1307
|
+
if (releaseHost) releaseHost();
|
|
1308
|
+
});
|
|
1118
1309
|
}
|
|
1119
1310
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
to = __velarString(to, "NavLink target");
|
|
1311
|
+
const navLinkPropFields = __velarOptionFields(["to", "exact", "replace", "class", "look", "children"]);
|
|
1312
|
+
|
|
1313
|
+
function navLinkTarget(props) {
|
|
1314
|
+
const to = __velarString(props.to, "NavLink target");
|
|
1315
|
+
requireNavigableTarget(to, "NavLink target");
|
|
1316
|
+
const exact = props.exact === undefined ? false : props.exact;
|
|
1317
|
+
const replace = props.replace === undefined ? false : props.replace;
|
|
1124
1318
|
__velarBool(exact, "NavLink exact");
|
|
1125
1319
|
__velarBool(replace, "NavLink replace");
|
|
1126
1320
|
internalHref(to);
|
|
1127
|
-
|
|
1128
|
-
|
|
1321
|
+
return { exact, path: normalizeApplicationPath(webUrlField(webUrl(to, "https://velar.invalid"), "pathname", __velarBrowserUrlPathname)) };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
export function NavLink(props) {
|
|
1325
|
+
const children = __velarLiveOptions(props, "NavLink props", navLinkPropFields).children;
|
|
1326
|
+
let linked = null;
|
|
1327
|
+
let target = null;
|
|
1129
1328
|
const update = () => {
|
|
1130
1329
|
const application = applicationPath(webLocationField("pathname", __velarBrowserLocationPathname));
|
|
1131
1330
|
const current = application === null ? null : normalizeApplicationPath(application);
|
|
1132
|
-
const active = current !== null
|
|
1331
|
+
const active = current !== null
|
|
1332
|
+
&& (current === target.path || (!target.exact && target.path !== "/" && current.startsWith(target.path + "/")));
|
|
1133
1333
|
if (active) __velarDomSetAttribute(linked.node, "aria-current", "page");
|
|
1134
1334
|
else __velarDomRemoveAttribute(linked.node, "aria-current");
|
|
1135
1335
|
};
|
|
1336
|
+
// A NavLink rejects its own target before the Link it wraps sees it, so the
|
|
1337
|
+
// observer runs first and the message names NavLink. Its first run has no
|
|
1338
|
+
// Link to mark yet; the update below is that run's second half.
|
|
1339
|
+
const observer = webObserve(() => {
|
|
1340
|
+
target = navLinkTarget(props);
|
|
1341
|
+
if (linked !== null) update();
|
|
1342
|
+
}, "navlink", "NavLink");
|
|
1343
|
+
// The Link is handed the same live fields, not a copy of their values, so a
|
|
1344
|
+
// NavLink built from state moves its href as well as its aria-current.
|
|
1136
1345
|
let removeRouteListener = null;
|
|
1137
|
-
|
|
1346
|
+
try {
|
|
1347
|
+
linked = Link({
|
|
1348
|
+
get to() { return props.to; },
|
|
1349
|
+
get replace() { return props.replace; },
|
|
1350
|
+
get class() { return props.class; },
|
|
1351
|
+
get look() { return props.look; },
|
|
1352
|
+
children,
|
|
1353
|
+
});
|
|
1354
|
+
update();
|
|
1355
|
+
} catch (failure) {
|
|
1356
|
+
observer.stop();
|
|
1357
|
+
if (linked) linked.destroy(false);
|
|
1358
|
+
throw failure;
|
|
1359
|
+
}
|
|
1138
1360
|
return component(linked.node, () => {
|
|
1139
1361
|
linked.__mount();
|
|
1140
1362
|
removeRouteListener = __velarBrowserListenGlobal("popstate", update);
|
|
1141
1363
|
}, () => {
|
|
1364
|
+
observer.stop();
|
|
1142
1365
|
if (removeRouteListener) removeRouteListener();
|
|
1143
1366
|
linked.destroy(false);
|
|
1144
1367
|
});
|
|
1145
1368
|
}
|
|
1146
1369
|
|
|
1147
|
-
// These runtime-implemented components read their props exactly once at
|
|
1148
|
-
// construction; the emitted instantiation path snapshots each prop thunk for
|
|
1149
|
-
// them instead of building a live reactive props store, so their strict
|
|
1150
|
-
// record validation keeps rejecting accessor fields.
|
|
1151
|
-
Head.__velarSnapshotProps = true;
|
|
1152
|
-
Router.__velarSnapshotProps = true;
|
|
1153
|
-
Link.__velarSnapshotProps = true;
|
|
1154
|
-
NavLink.__velarSnapshotProps = true;
|
|
1155
|
-
|
|
1156
1370
|
function forwardHost(node, props) {
|
|
1157
1371
|
const cleanups = [];
|
|
1158
1372
|
if (props.class != null) {
|
|
@@ -1211,6 +1425,40 @@ function isExternal(to) {
|
|
|
1211
1425
|
return typeof to === "string" && (/^[a-z][a-z\d+.-]*:/i.test(to) || to.startsWith("//"));
|
|
1212
1426
|
}
|
|
1213
1427
|
|
|
1428
|
+
// The scheme policy a JSX URL attribute enforces, narrowed to what a Link is
|
|
1429
|
+
// for. 'javascript:' and 'vbscript:' are code, not locations: a Link classified
|
|
1430
|
+
// one as external, wrote it to node.href, and its click handler then let native
|
|
1431
|
+
// anchor activation run it. Only the two schemes a page can be served over are
|
|
1432
|
+
// navigation targets; anything else belongs to a plain <a> element.
|
|
1433
|
+
const navigableSchemes = ["http", "https"];
|
|
1434
|
+
function targetScheme(to) {
|
|
1435
|
+
let scheme = "";
|
|
1436
|
+
for (let index = 0; index < to.length; index += 1) {
|
|
1437
|
+
const code = to.charCodeAt(index);
|
|
1438
|
+
// The user agent strips ASCII whitespace and control characters before it
|
|
1439
|
+
// parses the scheme, so "java\tscript:" reads as "javascript:" to it and
|
|
1440
|
+
// has to read that way here too.
|
|
1441
|
+
if (code <= 0x20 || code === 0x7f) continue;
|
|
1442
|
+
if (code === 58 && scheme.length > 0) return scheme.toLowerCase();
|
|
1443
|
+
const letter = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
1444
|
+
if (letter || (scheme.length > 0 && ((code >= 48 && code <= 57) || code === 43 || code === 45 || code === 46))) {
|
|
1445
|
+
scheme += to[index];
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
// Anything else this early means the value names no scheme: it is a
|
|
1449
|
+
// relative path, which is always the application's own origin.
|
|
1450
|
+
return "";
|
|
1451
|
+
}
|
|
1452
|
+
return "";
|
|
1453
|
+
}
|
|
1454
|
+
function requireNavigableTarget(to, label) {
|
|
1455
|
+
const scheme = targetScheme(to);
|
|
1456
|
+
if (scheme !== "" && !navigableSchemes.includes(scheme)) {
|
|
1457
|
+
throw new TypeError(label + " rejected the '" + scheme + ":' URL scheme; only http and https targets navigate, so write a plain <a> element for another scheme");
|
|
1458
|
+
}
|
|
1459
|
+
return to;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1214
1462
|
function queryValues() {
|
|
1215
1463
|
const search = webLocationField("search", __velarBrowserLocationSearch);
|
|
1216
1464
|
if (typeof search !== "string") throw new TypeError("Route queries require browser text");
|
|
@@ -2068,7 +2316,7 @@ export function formBody() {
|
|
|
2068
2316
|
return __velarFreezeOptionsValue(body);
|
|
2069
2317
|
}
|
|
2070
2318
|
|
|
2071
|
-
export class
|
|
2319
|
+
export class HttpResponseError extends Error {
|
|
2072
2320
|
constructor(message, status, url, body = null) {
|
|
2073
2321
|
message = __velarString(message, "HTTP error message");
|
|
2074
2322
|
url = __velarString(url, "HTTP error URL");
|
|
@@ -2076,7 +2324,7 @@ export class HttpError extends Error {
|
|
|
2076
2324
|
if (url.length > 2 * 1024 * 1024) throw new RangeError("HTTP error URLs cannot exceed 2 MiB");
|
|
2077
2325
|
if (!Number.isInteger(status) || status < 100 || status > 599) throw new RangeError("HTTP error status must be an integer from 100 through 599");
|
|
2078
2326
|
super(message);
|
|
2079
|
-
this.name = "
|
|
2327
|
+
this.name = "HttpResponseError";
|
|
2080
2328
|
this.status = status;
|
|
2081
2329
|
this.url = url;
|
|
2082
2330
|
this.body = body;
|
|
@@ -2130,7 +2378,6 @@ class HttpResponse {
|
|
|
2130
2378
|
this.bytesValue = null;
|
|
2131
2379
|
this.bytesPending = null;
|
|
2132
2380
|
this.streaming = false;
|
|
2133
|
-
this.ok = response.ok;
|
|
2134
2381
|
this.status = response.status;
|
|
2135
2382
|
this.statusText = response.statusText;
|
|
2136
2383
|
this.url = response.url;
|
|
@@ -2273,7 +2520,7 @@ class HttpResponse {
|
|
|
2273
2520
|
}
|
|
2274
2521
|
}
|
|
2275
2522
|
async blob() { return new NativeBlob([await this.bytes()], { type: this.contentType }); }
|
|
2276
|
-
async parse(Type) { Type = runtimeHttpType(Type); return Type
|
|
2523
|
+
async parse(Type) { Type = runtimeHttpType(Type); return __velarJsonParseTyped(Type, await this.text(), "HTTP JSON text"); }
|
|
2277
2524
|
}
|
|
2278
2525
|
|
|
2279
2526
|
class Request {
|
|
@@ -2337,12 +2584,15 @@ class Request {
|
|
|
2337
2584
|
const response = await responseSnapshot(transportResponse);
|
|
2338
2585
|
if (this.abortError) throw this.abortError;
|
|
2339
2586
|
const wrapped = new HttpResponse(response, this);
|
|
2340
|
-
|
|
2587
|
+
// D90 R20: the 2xx question is asked here and nowhere else. The
|
|
2588
|
+
// transport snapshot still carries ok; the response an author holds does
|
|
2589
|
+
// not, because by the time it is returned the answer is always yes.
|
|
2590
|
+
if (!response.ok) {
|
|
2341
2591
|
const text = await wrapped.text();
|
|
2342
2592
|
let parsed = text;
|
|
2343
2593
|
try { parsed = text ? __velarJsonParse(text, "HTTP error JSON text") : null; } catch { parsed = text; }
|
|
2344
2594
|
const errorUrl = wrapped.url || this.url;
|
|
2345
|
-
throw new
|
|
2595
|
+
throw new HttpResponseError("HTTP " + wrapped.status + " for " + errorUrl, wrapped.status, errorUrl, parsed);
|
|
2346
2596
|
}
|
|
2347
2597
|
return wrapped;
|
|
2348
2598
|
} catch (error) {
|
|
@@ -2356,7 +2606,7 @@ class Request {
|
|
|
2356
2606
|
async bytes() { return (await this.response()).bytes(); }
|
|
2357
2607
|
async streamText(consume) { return (await this.response()).streamText(consume); }
|
|
2358
2608
|
async blob() { return (await this.response()).blob(); }
|
|
2359
|
-
async parse(Type) { Type = runtimeHttpType(Type); return
|
|
2609
|
+
async parse(Type) { Type = runtimeHttpType(Type); return (await this.response()).parse(Type); }
|
|
2360
2610
|
cancel() { this.abort("cancelled"); return null; }
|
|
2361
2611
|
}
|
|
2362
2612
|
|
|
@@ -2447,7 +2697,7 @@ function parsed(raw, Type, fallback, maxBytes) {
|
|
|
2447
2697
|
Type = storageType(Type);
|
|
2448
2698
|
if (raw == null) return fallback;
|
|
2449
2699
|
if (typeof raw !== "string" || __velarUtf8ByteLength(raw) > maxBytes) return fallback;
|
|
2450
|
-
try { return Type
|
|
2700
|
+
try { return __velarJsonParseTyped(Type, raw, "Stored JSON text"); } catch { return fallback; }
|
|
2451
2701
|
}
|
|
2452
2702
|
|
|
2453
2703
|
function createStore(storageArea, prefix = "", areaName = "local") {
|
|
@@ -2634,7 +2884,7 @@ export function database(name) {
|
|
|
2634
2884
|
};
|
|
2635
2885
|
const keyOf = (key) => storageText(key, "Database key");
|
|
2636
2886
|
return Object.freeze({
|
|
2637
|
-
async get(key, Type, fallback = null, maxBytes = storageMaxValueBytes) { Type = storageType(Type); maxBytes = storageByteBudget(maxBytes); const name = keyOf(key); const encoded = await request("readonly", (store) => objectOperation(store, "get", [name])); if (encoded === undefined || typeof encoded !== "string" || __velarUtf8ByteLength(encoded) > maxBytes) return fallback; try { return Type
|
|
2887
|
+
async get(key, Type, fallback = null, maxBytes = storageMaxValueBytes) { Type = storageType(Type); maxBytes = storageByteBudget(maxBytes); const name = keyOf(key); const encoded = await request("readonly", (store) => objectOperation(store, "get", [name])); if (encoded === undefined || typeof encoded !== "string" || __velarUtf8ByteLength(encoded) > maxBytes) return fallback; try { return __velarJsonParseTyped(Type, encoded, "Stored JSON text"); } catch { return fallback; } },
|
|
2638
2888
|
async set(key, value, maxBytes = storageMaxValueBytes) {
|
|
2639
2889
|
const name = keyOf(key);
|
|
2640
2890
|
maxBytes = storageByteBudget(maxBytes);
|
|
@@ -3033,6 +3283,7 @@ function requireDialog(value) {
|
|
|
3033
3283
|
}
|
|
3034
3284
|
`.trimStart()],
|
|
3035
3285
|
["velar/files", String.raw `
|
|
3286
|
+
${VELAR_ERROR_NORMALIZATION_RUNTIME}
|
|
3036
3287
|
${optionsRuntime}
|
|
3037
3288
|
${fileRegistryRuntime}
|
|
3038
3289
|
const defaultFileReadBytes = 16 * 1024 * 1024;
|
|
@@ -3157,7 +3408,6 @@ export function download(name, data, mime = "text/plain;charset=utf-8") {
|
|
|
3157
3408
|
["velar/realtime", String.raw `
|
|
3158
3409
|
${ownedCallbackRuntime}
|
|
3159
3410
|
${optionsRuntime}
|
|
3160
|
-
${VELAR_STRICT_JSON_RUNTIME}
|
|
3161
3411
|
const maxRealtimeTextCodeUnits = 16 * 1024 * 1024;
|
|
3162
3412
|
const maxRealtimeUrlCodeUnits = 2 * 1024 * 1024;
|
|
3163
3413
|
const realtimeMissingField = Object.freeze({});
|
|
@@ -3180,13 +3430,6 @@ function realtimePrototypeMember(prototype, name, kind) {
|
|
|
3180
3430
|
}
|
|
3181
3431
|
return null;
|
|
3182
3432
|
}
|
|
3183
|
-
const RealtimeWebSocket = realtimeGlobalConstructor("WebSocket");
|
|
3184
|
-
const realtimeWebSocketPrototype = realtimePrototype(RealtimeWebSocket);
|
|
3185
|
-
const realtimeWebSocketAddEventListener = realtimePrototypeMember(realtimeWebSocketPrototype, "addEventListener", "value");
|
|
3186
|
-
const realtimeWebSocketSend = realtimePrototypeMember(realtimeWebSocketPrototype, "send", "value");
|
|
3187
|
-
const realtimeWebSocketClose = realtimePrototypeMember(realtimeWebSocketPrototype, "close", "value");
|
|
3188
|
-
const realtimeWebSocketUrl = realtimePrototypeMember(realtimeWebSocketPrototype, "url", "get");
|
|
3189
|
-
const realtimeWebSocketReadyState = realtimePrototypeMember(realtimeWebSocketPrototype, "readyState", "get");
|
|
3190
3433
|
const RealtimeEventSource = realtimeGlobalConstructor("EventSource");
|
|
3191
3434
|
const realtimeEventSourcePrototype = realtimePrototype(RealtimeEventSource);
|
|
3192
3435
|
const realtimeEventSourceAddEventListener = realtimePrototypeMember(realtimeEventSourcePrototype, "addEventListener", "value");
|
|
@@ -3196,9 +3439,6 @@ const realtimeEventSourceReadyState = realtimePrototypeMember(realtimeEventSourc
|
|
|
3196
3439
|
const realtimeMessageEventPrototype = realtimePrototype(realtimeGlobalConstructor("MessageEvent"));
|
|
3197
3440
|
const realtimeMessageEventData = realtimePrototypeMember(realtimeMessageEventPrototype, "data", "get");
|
|
3198
3441
|
const realtimeMessageEventLastEventId = realtimePrototypeMember(realtimeMessageEventPrototype, "lastEventId", "get");
|
|
3199
|
-
const realtimeCloseEventPrototype = realtimePrototype(realtimeGlobalConstructor("CloseEvent"));
|
|
3200
|
-
const realtimeCloseEventCode = realtimePrototypeMember(realtimeCloseEventPrototype, "code", "get");
|
|
3201
|
-
const realtimeCloseEventReason = realtimePrototypeMember(realtimeCloseEventPrototype, "reason", "get");
|
|
3202
3442
|
function realtimeOwnDataField(value, name) {
|
|
3203
3443
|
if (value === null || (typeof value !== "object" && typeof value !== "function")) return realtimeMissingField;
|
|
3204
3444
|
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
@@ -3214,30 +3454,11 @@ function realtimeCall(operation, receiver, arguments_, name) {
|
|
|
3214
3454
|
if (typeof operation !== "function" || typeof realtimeReflectApply !== "function") throw new TypeError("The browser does not expose native " + name);
|
|
3215
3455
|
return realtimeReflectApply(operation, receiver, arguments_);
|
|
3216
3456
|
}
|
|
3217
|
-
function realtimeUtf8Length(value) {
|
|
3218
|
-
let bytes = 0;
|
|
3219
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
3220
|
-
const code = value.charCodeAt(index);
|
|
3221
|
-
if (code <= 0x7F) bytes += 1;
|
|
3222
|
-
else if (code <= 0x7FF) bytes += 2;
|
|
3223
|
-
else if (code >= 0xD800 && code <= 0xDBFF && index + 1 < value.length) {
|
|
3224
|
-
const next = value.charCodeAt(index + 1);
|
|
3225
|
-
if (next >= 0xDC00 && next <= 0xDFFF) { bytes += 4; index += 1; }
|
|
3226
|
-
else bytes += 3;
|
|
3227
|
-
} else bytes += 3;
|
|
3228
|
-
}
|
|
3229
|
-
return bytes;
|
|
3230
|
-
}
|
|
3231
3457
|
function realtimeUrl(value, name) {
|
|
3232
3458
|
value = __velarString(value, name);
|
|
3233
3459
|
if (value.length > maxRealtimeUrlCodeUnits) throw new RangeError(name + " cannot exceed 2 MiB");
|
|
3234
3460
|
return value;
|
|
3235
3461
|
}
|
|
3236
|
-
function realtimeMessage(value, name) {
|
|
3237
|
-
value = __velarString(value, name);
|
|
3238
|
-
if (value.length > maxRealtimeTextCodeUnits) throw new RangeError(name + " cannot exceed 16 MiB");
|
|
3239
|
-
return value;
|
|
3240
|
-
}
|
|
3241
3462
|
function handler(value, allowed) {
|
|
3242
3463
|
value = __velarOptions(value, "Realtime handlers", allowed);
|
|
3243
3464
|
for (const name of Object.getOwnPropertyNames(value)) {
|
|
@@ -3246,14 +3467,6 @@ function handler(value, allowed) {
|
|
|
3246
3467
|
}
|
|
3247
3468
|
return value;
|
|
3248
3469
|
}
|
|
3249
|
-
function webSocketState(value) {
|
|
3250
|
-
const readyState = realtimeHostField(value, "readyState", realtimeWebSocketReadyState);
|
|
3251
|
-
if (readyState === 0) return "connecting";
|
|
3252
|
-
if (readyState === 1) return "open";
|
|
3253
|
-
if (readyState === 2) return "closing";
|
|
3254
|
-
if (readyState === 3) return "closed";
|
|
3255
|
-
throw new TypeError("WebSocket returned an invalid state");
|
|
3256
|
-
}
|
|
3257
3470
|
function eventStreamState(value) {
|
|
3258
3471
|
const readyState = realtimeHostField(value, "readyState", realtimeEventSourceReadyState);
|
|
3259
3472
|
if (readyState === 0) return "connecting";
|
|
@@ -3261,78 +3474,6 @@ function eventStreamState(value) {
|
|
|
3261
3474
|
if (readyState === 2) return "closed";
|
|
3262
3475
|
throw new TypeError("Event stream returned an invalid state");
|
|
3263
3476
|
}
|
|
3264
|
-
export function socket(url, handlers = {}) {
|
|
3265
|
-
handlers = handler(handlers, __velarOptionFields(["open", "message", "error", "close"]));
|
|
3266
|
-
if (!RealtimeWebSocket) throw new TypeError("The browser does not expose native WebSocket");
|
|
3267
|
-
const value = new RealtimeWebSocket(realtimeUrl(url, "WebSocket URL"));
|
|
3268
|
-
let resolvedUrl;
|
|
3269
|
-
try {
|
|
3270
|
-
const hostUrl = realtimeHostField(value, "url", realtimeWebSocketUrl);
|
|
3271
|
-
if (hostUrl === realtimeMissingField) throw new TypeError("WebSocket returned an invalid resolved URL");
|
|
3272
|
-
resolvedUrl = realtimeUrl(hostUrl, "WebSocket resolved URL");
|
|
3273
|
-
}
|
|
3274
|
-
catch (failure) { try { realtimeCall(realtimeWebSocketClose, value, [], "WebSocket close"); } catch {} throw failure; }
|
|
3275
|
-
const opened = () => __velarInvokeOwnedCallback(handlers.open, [], "realtime", "socket:open");
|
|
3276
|
-
const messaged = (event) => {
|
|
3277
|
-
const data = realtimeHostField(event, "data", realtimeMessageEventData);
|
|
3278
|
-
if (data === realtimeMissingField) {
|
|
3279
|
-
__velarReportOwnedCallback(new TypeError("WebSocket message event is invalid"), "realtime", "socket:message");
|
|
3280
|
-
try {
|
|
3281
|
-
const state = webSocketState(value);
|
|
3282
|
-
if (state === "connecting" || state === "open") realtimeCall(realtimeWebSocketClose, value, [1003, "Invalid message event"], "WebSocket close");
|
|
3283
|
-
} catch {}
|
|
3284
|
-
return;
|
|
3285
|
-
}
|
|
3286
|
-
if (typeof data !== "string") {
|
|
3287
|
-
__velarInvokeOwnedCallback(handlers.error, ["Binary WebSocket messages are not supported by VelarScript Web API 0.10"], "realtime", "socket:error");
|
|
3288
|
-
const state = webSocketState(value);
|
|
3289
|
-
if (state === "connecting" || state === "open") realtimeCall(realtimeWebSocketClose, value, [1003, "Text messages only"], "WebSocket close");
|
|
3290
|
-
return;
|
|
3291
|
-
}
|
|
3292
|
-
if (data.length > maxRealtimeTextCodeUnits) {
|
|
3293
|
-
__velarInvokeOwnedCallback(handlers.error, ["WebSocket message exceeded 16 MiB"], "realtime", "socket:error");
|
|
3294
|
-
const state = webSocketState(value);
|
|
3295
|
-
if (state === "connecting" || state === "open") realtimeCall(realtimeWebSocketClose, value, [1009, "Message too large"], "WebSocket close");
|
|
3296
|
-
return;
|
|
3297
|
-
}
|
|
3298
|
-
__velarInvokeOwnedCallback(handlers.message, [data], "realtime", "socket:message");
|
|
3299
|
-
};
|
|
3300
|
-
const failed = () => __velarInvokeOwnedCallback(handlers.error, ["WebSocket connection error"], "realtime", "socket:error");
|
|
3301
|
-
const closed = (event) => {
|
|
3302
|
-
try {
|
|
3303
|
-
const code = realtimeHostField(event, "code", realtimeCloseEventCode);
|
|
3304
|
-
const hostReason = realtimeHostField(event, "reason", realtimeCloseEventReason);
|
|
3305
|
-
if (code === realtimeMissingField || hostReason === realtimeMissingField) throw new TypeError("WebSocket close event is invalid");
|
|
3306
|
-
const reason = realtimeMessage(hostReason, "WebSocket close event reason");
|
|
3307
|
-
if (!Number.isSafeInteger(code) || code < 0 || code > 65535) throw new TypeError("WebSocket close event returned an invalid code");
|
|
3308
|
-
if (reason.length > 123 || realtimeUtf8Length(reason) > 123) throw new RangeError("WebSocket close event reason cannot exceed 123 UTF-8 bytes");
|
|
3309
|
-
__velarInvokeOwnedCallback(handlers.close, [code, reason], "realtime", "socket:close");
|
|
3310
|
-
} catch (failure) { __velarReportOwnedCallback(failure, "realtime", "socket:close"); }
|
|
3311
|
-
};
|
|
3312
|
-
try {
|
|
3313
|
-
realtimeCall(realtimeWebSocketAddEventListener, value, ["open", opened], "WebSocket event listener");
|
|
3314
|
-
realtimeCall(realtimeWebSocketAddEventListener, value, ["message", messaged], "WebSocket event listener");
|
|
3315
|
-
realtimeCall(realtimeWebSocketAddEventListener, value, ["error", failed], "WebSocket event listener");
|
|
3316
|
-
realtimeCall(realtimeWebSocketAddEventListener, value, ["close", closed], "WebSocket event listener");
|
|
3317
|
-
} catch (failure) {
|
|
3318
|
-
try { realtimeCall(realtimeWebSocketClose, value, [], "WebSocket close"); } catch {}
|
|
3319
|
-
throw failure;
|
|
3320
|
-
}
|
|
3321
|
-
return Object.freeze({
|
|
3322
|
-
url: resolvedUrl,
|
|
3323
|
-
state: () => webSocketState(value),
|
|
3324
|
-
send(data) { data = realtimeMessage(data, "WebSocket message"); if (webSocketState(value) !== "open") throw new Error("WebSocket is not open"); realtimeCall(realtimeWebSocketSend, value, [data], "WebSocket send"); return null; },
|
|
3325
|
-
sendJson(data) { const text = __velarJsonStringify(data); if (webSocketState(value) !== "open") throw new Error("WebSocket is not open"); realtimeCall(realtimeWebSocketSend, value, [text], "WebSocket send"); return null; },
|
|
3326
|
-
close(code = 1000, reason = "") {
|
|
3327
|
-
if (!Number.isSafeInteger(code) || (code !== 1000 && (code < 3000 || code > 4999))) throw new RangeError("WebSocket close code must be 1000 or from 3000 through 4999");
|
|
3328
|
-
reason = __velarString(reason, "WebSocket close reason");
|
|
3329
|
-
if (reason.length > 123 || realtimeUtf8Length(reason) > 123) throw new RangeError("WebSocket close reason cannot exceed 123 UTF-8 bytes");
|
|
3330
|
-
const state = webSocketState(value);
|
|
3331
|
-
if (state === "connecting" || state === "open") realtimeCall(realtimeWebSocketClose, value, [code, reason], "WebSocket close");
|
|
3332
|
-
return null;
|
|
3333
|
-
},
|
|
3334
|
-
});
|
|
3335
|
-
}
|
|
3336
3477
|
export function eventStream(url, handlers = {}, credentials = false) {
|
|
3337
3478
|
handlers = handler(handlers, __velarOptionFields(["open", "message", "error"]));
|
|
3338
3479
|
credentials = __velarBool(credentials, "Event stream credentials");
|