@velarscript/web 0.12.1 → 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.
Files changed (67) hide show
  1. package/README.md +2 -2
  2. package/dist/analyzer.d.ts +164 -23
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +1254 -133
  5. package/dist/analyzer.js.map +1 -1
  6. package/dist/ast.d.ts +11 -0
  7. package/dist/ast.d.ts.map +1 -1
  8. package/dist/ast.js +22 -0
  9. package/dist/ast.js.map +1 -1
  10. package/dist/compiler.d.ts.map +1 -1
  11. package/dist/compiler.js +69 -44
  12. package/dist/compiler.js.map +1 -1
  13. package/dist/css-string.d.ts +32 -0
  14. package/dist/css-string.d.ts.map +1 -0
  15. package/dist/css-string.js +66 -0
  16. package/dist/css-string.js.map +1 -0
  17. package/dist/css-tokens.d.ts +16 -0
  18. package/dist/css-tokens.d.ts.map +1 -1
  19. package/dist/css-tokens.js +84 -0
  20. package/dist/css-tokens.js.map +1 -1
  21. package/dist/elements.d.ts +55 -0
  22. package/dist/elements.d.ts.map +1 -1
  23. package/dist/elements.js +152 -0
  24. package/dist/elements.js.map +1 -1
  25. package/dist/emitter.d.ts +10 -0
  26. package/dist/emitter.d.ts.map +1 -1
  27. package/dist/emitter.js +588 -159
  28. package/dist/emitter.js.map +1 -1
  29. package/dist/keyframes.d.ts +17 -0
  30. package/dist/keyframes.d.ts.map +1 -1
  31. package/dist/keyframes.js +28 -9
  32. package/dist/keyframes.js.map +1 -1
  33. package/dist/lexer.d.ts +21 -7
  34. package/dist/lexer.d.ts.map +1 -1
  35. package/dist/lexer.js +32 -12
  36. package/dist/lexer.js.map +1 -1
  37. package/dist/look-static.d.ts.map +1 -1
  38. package/dist/look-static.js +120 -20
  39. package/dist/look-static.js.map +1 -1
  40. package/dist/look.d.ts +15 -0
  41. package/dist/look.d.ts.map +1 -1
  42. package/dist/look.js +7 -2
  43. package/dist/look.js.map +1 -1
  44. package/dist/parser.d.ts.map +1 -1
  45. package/dist/parser.js +15 -20
  46. package/dist/parser.js.map +1 -1
  47. package/dist/runtime-foundation.d.ts.map +1 -1
  48. package/dist/runtime-foundation.js +321 -70
  49. package/dist/runtime-foundation.js.map +1 -1
  50. package/dist/runtime.d.ts.map +1 -1
  51. package/dist/runtime.js +320 -179
  52. package/dist/runtime.js.map +1 -1
  53. package/dist/stable-order.d.ts +16 -0
  54. package/dist/stable-order.d.ts.map +1 -0
  55. package/dist/stable-order.js +18 -0
  56. package/dist/stable-order.js.map +1 -0
  57. package/dist/types.d.ts +0 -1
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js +0 -16
  60. package/dist/types.js.map +1 -1
  61. package/dist/websocket-runtime.d.ts.map +1 -1
  62. package/dist/websocket-runtime.js +9 -5
  63. package/dist/websocket-runtime.js.map +1 -1
  64. package/dist/worker-runtime.d.ts.map +1 -1
  65. package/dist/worker-runtime.js +9 -7
  66. package/dist/worker-runtime.js.map +1 -1
  67. 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
- return runtime && runtime.version === ${JSON.stringify(VELAR_RUNTIME_SCHEMA_VERSION)} && typeof runtime.toRaw === "function"
100
- && typeof runtime.collectionRead === "function" ? runtime : null;
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)) throw new TypeError(name + " field '" + key + "' must be an enumerable data value");
194
- output[key] = descriptor.value;
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(" + JSON.stringify(lookText(path, "Asset path")) + ")"); }
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(lookText(property, "Transition property") + " " + lookVisual(duration, "Transition duration") + " " + lookText(easing, "Transition easing") + suffix);
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 __velarEnqueue(() => { throw error; });
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
- export function Head(props) {
940
- props = __velarOptions(props, "Head props", __velarOptionFields(["title", "description", "canonical", "robots", "image", "themeColor", "language"]));
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
- const node = document.createComment("velar head");
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
- return component(node, () => {
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
- if (document.title === title) document.title = previousTitle;
976
- if (language && document.documentElement.getAttribute("lang") === language) {
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
- export function Router(props) {
1023
- props = __velarOptions(props, "Router props", __velarOptionFields(["routes", "fallback"]));
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
- const node = __velarDomCreateElement("velar-router");
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
- __velarEnqueue(() => { throw error; });
1218
+ __velarBrowserCallCaptured(__velarBrowserQueueMicrotask, __velarBrowserWindow, [() => { throw error; }], "queueMicrotask");
1066
1219
  }
1067
1220
  }
1068
1221
  };
1069
1222
  const changed = () => render();
1070
1223
  let removeRouteListener = null;
1071
- render();
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
- export function Link(props) {
1083
- props = __velarOptions(props, "Link props", __velarOptionFields(["to", "replace", "class", "look", "children"]));
1084
- let { to, replace = false, children } = props;
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
- const href = external ? to : internalHref(to);
1090
- const node = __velarDomCreateElement("a");
1091
- const releaseHost = forwardHost(node, props);
1092
- node.href = href;
1093
- append(node, children);
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); }, () => { if (removeClick) removeClick(); releaseHost(); });
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
- export function NavLink(props) {
1121
- props = __velarOptions(props, "NavLink props", __velarOptionFields(["to", "exact", "replace", "class", "look", "children"]));
1122
- let { to, exact = false, replace = false, children } = props;
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
- const linked = Link({ to, replace, class: props.class, look: props.look, children });
1128
- const target = normalizeApplicationPath(webUrlField(webUrl(to, "https://velar.invalid"), "pathname", __velarBrowserUrlPathname));
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 && (current === target || (!exact && target !== "/" && current.startsWith(target + "/")));
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
- update();
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 HttpError extends Error {
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 = "HttpError";
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.parse(await this.json()); }
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
- if (!wrapped.ok) {
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 HttpError("HTTP " + wrapped.status + " for " + errorUrl, wrapped.status, errorUrl, parsed);
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 Type.parse(await this.json()); }
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.parse(__velarJsonParse(raw, "Stored JSON text")); } catch { return fallback; }
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.parse(__velarJsonParse(encoded, "Stored JSON text")); } catch { return fallback; } },
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");