@velarscript/node 0.11.1 → 0.12.1

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 (62) hide show
  1. package/README.md +102 -14
  2. package/dist/compiler.d.ts +3 -0
  3. package/dist/compiler.d.ts.map +1 -1
  4. package/dist/compiler.js +300 -76
  5. package/dist/compiler.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/node-host-runtime.d.ts.map +1 -1
  11. package/dist/node-host-runtime.js +30 -14
  12. package/dist/node-host-runtime.js.map +1 -1
  13. package/dist/node-host-worker-runtime.d.ts.map +1 -1
  14. package/dist/node-host-worker-runtime.js +495 -93
  15. package/dist/node-host-worker-runtime.js.map +1 -1
  16. package/dist/project-config.d.ts +17 -0
  17. package/dist/project-config.d.ts.map +1 -0
  18. package/dist/project-config.js +66 -0
  19. package/dist/project-config.js.map +1 -0
  20. package/dist/serve-runtime.d.ts.map +1 -1
  21. package/dist/serve-runtime.js +2942 -158
  22. package/dist/serve-runtime.js.map +1 -1
  23. package/dist/server-analyzer.d.ts +35 -0
  24. package/dist/server-analyzer.d.ts.map +1 -0
  25. package/dist/server-analyzer.js +692 -0
  26. package/dist/server-analyzer.js.map +1 -0
  27. package/dist/server-ast.d.ts +54 -0
  28. package/dist/server-ast.d.ts.map +1 -0
  29. package/dist/server-ast.js +16 -0
  30. package/dist/server-ast.js.map +1 -0
  31. package/dist/server-emitter.d.ts +17 -0
  32. package/dist/server-emitter.d.ts.map +1 -0
  33. package/dist/server-emitter.js +177 -0
  34. package/dist/server-emitter.js.map +1 -0
  35. package/dist/server-inspection.d.ts +3 -0
  36. package/dist/server-inspection.d.ts.map +1 -0
  37. package/dist/server-inspection.js +12 -0
  38. package/dist/server-inspection.js.map +1 -0
  39. package/dist/server-lexer.d.ts +13 -0
  40. package/dist/server-lexer.d.ts.map +1 -0
  41. package/dist/server-lexer.js +65 -0
  42. package/dist/server-lexer.js.map +1 -0
  43. package/dist/server-parser.d.ts +18 -0
  44. package/dist/server-parser.d.ts.map +1 -0
  45. package/dist/server-parser.js +232 -0
  46. package/dist/server-parser.js.map +1 -0
  47. package/dist/server-semantic.d.ts +3 -0
  48. package/dist/server-semantic.d.ts.map +1 -0
  49. package/dist/server-semantic.js +31 -0
  50. package/dist/server-semantic.js.map +1 -0
  51. package/dist/server-types.d.ts +25 -0
  52. package/dist/server-types.d.ts.map +1 -0
  53. package/dist/server-types.js +60 -0
  54. package/dist/server-types.js.map +1 -0
  55. package/dist/websocket-runtime.d.ts.map +1 -1
  56. package/dist/websocket-runtime.js +169 -15
  57. package/dist/websocket-runtime.js.map +1 -1
  58. package/package.json +10 -2
  59. package/dist/sqlite-runtime.d.ts +0 -3
  60. package/dist/sqlite-runtime.d.ts.map +0 -1
  61. package/dist/sqlite-runtime.js +0 -92
  62. package/dist/sqlite-runtime.js.map +0 -1
@@ -3,12 +3,14 @@
3
3
  // application code and npm dependencies remain in the application Realm.
4
4
  export const VELAR_NODE_HOST_WORKER_SOURCE = String.raw `
5
5
  import { Buffer } from "node:buffer";
6
- import { watch as watchNode } from "node:fs";
6
+ import { createReadStream, watch as watchNode } from "node:fs";
7
7
  import { appendFile, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { createServer, request as createHttpRequest } from "node:http";
9
9
  import { request as createHttpsRequest } from "node:https";
10
10
  import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
11
11
  import { URL as NodeURL } from "node:url";
12
+ import { brotliCompress as brotliCompressNode, gzip as gzipNode } from "node:zlib";
13
+ import { promisify } from "node:util";
12
14
  import { workerData } from "node:worker_threads";
13
15
 
14
16
  const port = workerData;
@@ -21,6 +23,16 @@ const maxServeFileBytes = 64 * 1024 * 1024;
21
23
  const maxServeStreamBytes = 64 * 1024 * 1024;
22
24
  const maxServeStreamChunkBytes = 1024 * 1024;
23
25
  const maxServeAggregateBytes = 128 * 1024 * 1024;
26
+ const maxServeRequestTargetBytes = 64 * 1024;
27
+ const maxServeRequestMetadataBytes = 512 * 1024;
28
+ const maxServeQueryFields = 1000;
29
+ const maxServeHeaderTextBytes = 64 * 1024;
30
+ const maxServeSockets = 4096;
31
+ const maxServeSocketsPerServer = 2048;
32
+ const serveHeadersTimeoutMilliseconds = 10_000;
33
+ const serveRequestTimeoutMilliseconds = 60_000;
34
+ const serveKeepAliveTimeoutMilliseconds = 5_000;
35
+ const serveShutdownTimeoutMilliseconds = 30_000;
24
36
  const maxHttpBodyBytes = 16 * 1024 * 1024;
25
37
  const maxHttpResponseBytes = 64 * 1024 * 1024;
26
38
  const maxHttpResponseChunks = 1000000;
@@ -35,7 +47,7 @@ const operations = new Set([
35
47
  "fs.readFile", "fs.createFile", "fs.replaceFileIfMatches", "fs.writeFile", "fs.appendFile", "fs.exists", "fs.list", "fs.info",
36
48
  "fs.canonical", "fs.makeDirectory", "fs.copyFile", "fs.move", "fs.removeFile", "fs.watchStart", "fs.watchNext", "fs.watchClose",
37
49
  "http.request", "http.read", "http.readBytes", "http.cancel", "http.close",
38
- "serve.start", "serve.stop", "serve.body", "serve.respond", "serve.respondFile",
50
+ "serve.start", "serve.stop", "serve.body", "serve.bodyBytes", "serve.readFile", "serve.respond", "serve.respondFile",
39
51
  "serve.streamStart", "serve.streamWrite", "serve.streamEnd", "serve.fail",
40
52
  ]);
41
53
  const servers = new Map();
@@ -48,6 +60,7 @@ let nextRequestHandle = 1;
48
60
  let nextTextReplacementIdentity = 1;
49
61
  let nextFileWatcherHandle = 1;
50
62
  let reservedServeBytes = 0;
63
+ let activeServeSockets = 0;
51
64
  const contentTypes = Object.freeze({
52
65
  ".css": "text/css; charset=utf-8", ".gif": "image/gif", ".html": "text/html; charset=utf-8",
53
66
  ".ico": "image/x-icon", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".js": "text/javascript; charset=utf-8",
@@ -55,8 +68,11 @@ const contentTypes = Object.freeze({
55
68
  ".png": "image/png", ".svg": "image/svg+xml", ".txt": "text/plain; charset=utf-8", ".wasm": "application/wasm",
56
69
  ".webp": "image/webp", ".woff": "font/woff", ".woff2": "font/woff2",
57
70
  });
71
+ const gzip = promisify(gzipNode);
72
+ const brotliCompress = promisify(brotliCompressNode);
58
73
 
59
74
  class StaticNotFound extends Error {}
75
+ class RequestHeadersTooLarge extends RangeError {}
60
76
 
61
77
  function allocateHandle(values, next, maximum, name) {
62
78
  let candidate = next;
@@ -106,7 +122,15 @@ function cleanupRequest(task) {
106
122
  releaseServeBytes(task);
107
123
  }
108
124
 
109
- function closeRequest(task) {
125
+ function cancelRequest(task, reason = "client_disconnect") {
126
+ if (task.cancelled || task.completed) return;
127
+ task.cancelled = true;
128
+ try { port.postMessage({kind: "event", event: "serve.cancel", value: {token: task.token, request: task.handle, reason}}); }
129
+ catch {}
130
+ }
131
+
132
+ function closeRequest(task, cancelled = false) {
133
+ if (cancelled) cancelRequest(task);
110
134
  task.transportDone = true;
111
135
  cleanupRequest(task);
112
136
  }
@@ -120,6 +144,24 @@ async function withRequest(task, action) {
120
144
  }
121
145
  }
122
146
 
147
+ async function withTerminalResponse(task, action) {
148
+ if (task.responseMode !== "idle") throw new Error("Node serve request already owns a response operation");
149
+ task.responseMode = "terminal";
150
+ try { return await withRequest(task, action); }
151
+ catch (error) {
152
+ if (!task.completed && !task.transportDone && !task.response.headersSent) task.responseMode = "idle";
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ async function withStreamWrite(task, action) {
158
+ if (task.responseMode !== "streaming") throw new Error("ServeResponse stream has not started");
159
+ if (task.writeActive) throw new Error("ServeResponse allows only one active stream write");
160
+ task.writeActive = true;
161
+ try { return await withRequest(task, action); }
162
+ finally { task.writeActive = false; }
163
+ }
164
+
123
165
  function completeRequest(task) {
124
166
  if (task.completed) throw new Error("Node serve request is already completed");
125
167
  task.completed = true;
@@ -650,14 +692,33 @@ function headerPairs(value) {
650
692
  throw new TypeError("ServeResponse cannot set transport-owned header '" + item[0] + "'");
651
693
  }
652
694
  units += item[0].length + item[1].length;
653
- if (units > 1024 * 1024) throw new RangeError("ServeResponse headers cannot exceed 1 MiB of text");
695
+ if (units > 64 * 1024) throw new RangeError("ServeResponse headers cannot exceed 64 KiB of text");
654
696
  output.push(item);
655
697
  }
656
698
  return output;
657
699
  }
658
700
 
659
- function setHeaders(response, values) {
660
- for (const [name, value] of headerPairs(values)) response.setHeader(name, value);
701
+ function cookieValues(value) {
702
+ if (!Array.isArray(value) || value.length > 64) throw new TypeError("ServeResponse cookies must be a bounded list");
703
+ const output = [];
704
+ let bytes = 0;
705
+ for (const cookie of value) {
706
+ if (typeof cookie !== "string" || cookie.length === 0 || cookie.length > 8192 || /[\0\r\n]/u.test(cookie)) throw new TypeError("ServeResponse cookie is invalid");
707
+ bytes += Buffer.byteLength(cookie, "utf8");
708
+ if (bytes > 64 * 1024) throw new RangeError("ServeResponse cookies cannot exceed 64 KiB");
709
+ output.push(cookie);
710
+ }
711
+ return output;
712
+ }
713
+
714
+ function setHeaders(response, values, cookies = []) {
715
+ const setCookies = [];
716
+ for (const [name, value] of headerPairs(values)) {
717
+ if (name.toLowerCase() === "set-cookie") setCookies.push(value);
718
+ else response.setHeader(name, value);
719
+ }
720
+ for (const cookie of cookieValues(cookies)) setCookies.push(cookie);
721
+ if (setCookies.length > 0) response.setHeader("Set-Cookie", setCookies);
661
722
  }
662
723
 
663
724
  function requestPath(value) {
@@ -675,7 +736,7 @@ function inside(root, target) {
675
736
  return path === "" || !path.startsWith("..") && !isAbsolute(path);
676
737
  }
677
738
 
678
- async function staticFile(task, rootValue, pathValue, fallbackValue) {
739
+ async function staticFile(rootValue, pathValue, fallbackValue) {
679
740
  const root = await realpath(resolve(boundedPath(rootValue, "fileResponse")));
680
741
  const relativePath = requestPath(pathValue);
681
742
  const fallback = fallbackValue === null ? null : requestPath(fallbackValue);
@@ -684,19 +745,103 @@ async function staticFile(task, rootValue, pathValue, fallbackValue) {
684
745
  if (!inside(root, target)) throw new StaticNotFound();
685
746
  const metadata = await stat(target);
686
747
  if (!metadata.isFile() || metadata.size > maxServeFileBytes) throw new StaticNotFound();
687
- reserveServeBytes(task, metadata.size);
688
- let reserved = metadata.size;
748
+ return {target, metadata, contentType: contentTypes[extname(target).toLowerCase()] ?? "application/octet-stream"};
749
+ };
750
+ try { return await load(relativePath); }
751
+ catch (error) {
752
+ if ((error instanceof StaticNotFound || missing(error) || error?.code === "EISDIR") && fallback !== null) return load(fallback);
753
+ throw error;
754
+ }
755
+ }
756
+
757
+ function staticEtag(metadata) {
758
+ const modified = Number.isFinite(metadata.mtimeMs) ? Math.floor(metadata.mtimeMs) : 0;
759
+ return 'W/"' + metadata.size.toString(16) + "-" + modified.toString(16) + '"';
760
+ }
761
+
762
+ function staticNotModified(request, metadata, etag) {
763
+ const noneMatch = request.headers["if-none-match"];
764
+ if (typeof noneMatch === "string") {
765
+ for (const candidate of noneMatch.split(",")) if (candidate.trim() === "*" || candidate.trim() === etag) return true;
766
+ return false;
767
+ }
768
+ const modifiedSince = request.headers["if-modified-since"];
769
+ if (typeof modifiedSince !== "string") return false;
770
+ const time = Date.parse(modifiedSince);
771
+ return Number.isFinite(time) && Math.floor(metadata.mtimeMs / 1000) * 1000 <= time;
772
+ }
773
+
774
+ function staticRange(request, metadata, etag) {
775
+ const value = request.headers.range;
776
+ if (typeof value !== "string") return null;
777
+ const ifRange = request.headers["if-range"];
778
+ if (typeof ifRange === "string" && ifRange !== etag) {
779
+ const time = Date.parse(ifRange);
780
+ if (!Number.isFinite(time) || Math.floor(metadata.mtimeMs / 1000) * 1000 > time) return null;
781
+ }
782
+ const match = /^bytes=(\d*)-(\d*)$/u.exec(value.trim());
783
+ if (!match || match[1] === "" && match[2] === "" || metadata.size === 0) return false;
784
+ let start;
785
+ let end;
786
+ if (match[1] === "") {
787
+ const suffix = Number(match[2]);
788
+ if (!Number.isSafeInteger(suffix) || suffix < 1) return false;
789
+ start = Math.max(0, metadata.size - suffix);
790
+ end = metadata.size - 1;
791
+ } else {
792
+ start = Number(match[1]);
793
+ end = match[2] === "" ? metadata.size - 1 : Number(match[2]);
794
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start >= metadata.size || end < start) return false;
795
+ if (end >= metadata.size) end = metadata.size - 1;
796
+ }
797
+ return {start, end};
798
+ }
799
+
800
+ async function writeStaticRange(task, file, start, end) {
801
+ if (end < start) return;
802
+ const source = createReadStream(file.target, {start, end, highWaterMark: 64 * 1024});
803
+ try {
804
+ for await (const chunk of source) {
805
+ const bytes = chunk.byteLength;
806
+ reserveTransientServeBytes(bytes);
807
+ try {
808
+ await new Promise((resolveWrite, rejectWrite) => {
809
+ let settled = false;
810
+ const cleanup = () => { task.response.off("error", failed); task.response.off("close", closed); };
811
+ const finish = action => { if (settled) return; settled = true; cleanup(); action(); };
812
+ const failed = error => finish(() => rejectWrite(error));
813
+ const closed = () => finish(() => rejectWrite(new Error("ServeResponse client connection is closed")));
814
+ task.response.once("error", failed);
815
+ task.response.once("close", closed);
816
+ task.response.write(chunk, error => error ? failed(error) : finish(resolveWrite));
817
+ });
818
+ } finally { releaseTransientServeBytes(bytes); }
819
+ }
820
+ } finally { source.destroy(); }
821
+ }
822
+
823
+ async function testStaticFile(rootValue, pathValue, fallbackValue) {
824
+ const root = await realpath(resolve(boundedPath(rootValue, "fileResponse")));
825
+ const relativePath = requestPath(pathValue);
826
+ const fallback = fallbackValue === null ? null : requestPath(fallbackValue);
827
+ const load = async path => {
828
+ const target = await realpath(resolve(root, path));
829
+ if (!inside(root, target)) throw new StaticNotFound();
830
+ const metadata = await stat(target);
831
+ if (!metadata.isFile() || metadata.size > maxServeBodyBytes) throw new StaticNotFound();
832
+ let reserved = metadata.size * 2;
833
+ reserveTransientServeBytes(reserved);
689
834
  try {
690
- const data = await readFile(target);
691
- if (data.byteLength > maxServeFileBytes) throw new StaticNotFound();
692
- if (data.byteLength > reserved) reserveServeBytes(task, data.byteLength - reserved);
693
- else if (data.byteLength < reserved) releaseServeBytes(task, reserved - data.byteLength);
694
- reserved = data.byteLength;
835
+ const source = await readFile(target);
836
+ if (source.byteLength > maxServeBodyBytes) throw new StaticNotFound();
837
+ if (source.byteLength > metadata.size) { const extra = (source.byteLength - metadata.size) * 2; reserveTransientServeBytes(extra); reserved += extra; }
838
+ else if (source.byteLength < metadata.size) { const surplus = (metadata.size - source.byteLength) * 2; releaseTransientServeBytes(surplus); reserved -= surplus; }
839
+ const data = new Uint8Array(source.byteLength);
840
+ data.set(source);
841
+ releaseTransientServeBytes(source.byteLength);
842
+ reserved -= source.byteLength;
695
843
  return {data, contentType: contentTypes[extname(target).toLowerCase()] ?? "application/octet-stream"};
696
- } catch (error) {
697
- releaseServeBytes(task, reserved);
698
- throw error;
699
- }
844
+ } catch (error) { releaseTransientServeBytes(reserved); throw error; }
700
845
  };
701
846
  try { return await load(relativePath); }
702
847
  catch (error) {
@@ -710,37 +855,76 @@ function opaqueFailure(task) {
710
855
  else {
711
856
  task.response.statusCode = 500;
712
857
  task.response.setHeader("Content-Type", "text/plain; charset=utf-8");
713
- task.response.end("Internal server error");
858
+ task.response.end(task.request.method === "HEAD" ? undefined : "Internal server error");
714
859
  }
715
860
  completeRequest(task);
716
861
  }
717
862
 
718
- function bodyOf(task) {
863
+ function responseHasNoBody(task, status) {
864
+ return task.request.method === "HEAD" || status >= 100 && status < 200 || status === 204 || status === 304;
865
+ }
866
+
867
+ async function endServeResponse(task, value) {
868
+ await new Promise((resolveEnd, rejectEnd) => {
869
+ let settled = false;
870
+ const cleanup = () => {
871
+ task.response.off("error", failed);
872
+ task.response.off("close", closed);
873
+ };
874
+ const finish = action => { if (settled) return; settled = true; cleanup(); action(); };
875
+ const failed = error => finish(() => rejectEnd(error));
876
+ const closed = () => finish(() => task.response.writableFinished ? resolveEnd(null) : rejectEnd(new Error("ServeResponse client connection is closed")));
877
+ task.response.once("error", failed);
878
+ task.response.once("close", closed);
879
+ task.response.end(value, () => finish(() => resolveEnd(null)));
880
+ if (task.response.destroyed && !task.response.writableFinished) closed();
881
+ });
882
+ }
883
+
884
+ function rawBodyOf(task, maximum) {
719
885
  if (task.body !== null) return task.body;
720
886
  task.body = (async () => {
721
887
  const declaredText = task.request.headers["content-length"];
888
+ let declared = null;
722
889
  if (typeof declaredText === "string" && /^[0-9]+$/u.test(declaredText)) {
723
- const declared = Number(declaredText);
724
- if (!Number.isSafeInteger(declared) || declared > maxServeBodyBytes) {
890
+ declared = Number(declaredText);
891
+ if (!Number.isSafeInteger(declared) || declared > maximum) {
725
892
  task.request.resume();
726
- throw new RangeError("Request body cannot exceed 16 MiB");
893
+ return {data: null, bytes: maximum, tooLarge: true};
727
894
  }
728
895
  }
896
+ if (declared !== null) {
897
+ const output = Buffer.allocUnsafe(declared);
898
+ reserveServeBytes(task, declared);
899
+ let offset = 0;
900
+ try {
901
+ for await (const chunk of task.request) {
902
+ const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
903
+ if (offset + data.byteLength > declared) { task.request.resume(); releaseServeBytes(task, declared); return {data: null, bytes: maximum, tooLarge: true}; }
904
+ data.copy(output, offset);
905
+ offset += data.byteLength;
906
+ }
907
+ if (offset !== declared) throw new TypeError("Request body length does not match Content-Length");
908
+ return {data: output, bytes: declared, tooLarge: false};
909
+ } catch (error) { if (task.reservedBytes >= declared) releaseServeBytes(task, declared); throw error; }
910
+ }
729
911
  const chunks = [];
730
912
  let total = 0;
731
913
  try {
732
914
  for await (const chunk of task.request) {
733
915
  const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
734
- if (total + data.byteLength > maxServeBodyBytes) {
916
+ if (total + data.byteLength > maximum) {
735
917
  task.request.resume();
736
- throw new RangeError("Request body cannot exceed 16 MiB");
918
+ releaseServeBytes(task, total);
919
+ return {data: null, bytes: maximum, tooLarge: true};
737
920
  }
738
921
  reserveServeBytes(task, data.byteLength);
739
922
  total += data.byteLength;
740
923
  chunks.push(data);
741
924
  }
742
- try { return {text: new TextDecoder("utf-8", {fatal: true}).decode(Buffer.concat(chunks, total)), bytes: total}; }
743
- catch { throw new TypeError("Request body must be valid UTF-8 text"); }
925
+ reserveTransientServeBytes(total);
926
+ try { return {data: Buffer.concat(chunks, total), bytes: total, tooLarge: false}; }
927
+ finally { releaseTransientServeBytes(total); }
744
928
  } catch (error) {
745
929
  releaseServeBytes(task, total);
746
930
  throw error;
@@ -749,44 +933,134 @@ function bodyOf(task) {
749
933
  return task.body;
750
934
  }
751
935
 
936
+ async function bodyOf(task, maximum) {
937
+ const value = await rawBodyOf(task, maximum);
938
+ if (value.tooLarge) return {text: null, bytes: value.bytes, tooLarge: true};
939
+ try { return {text: new TextDecoder("utf-8", {fatal: true}).decode(value.data), bytes: value.bytes, tooLarge: false}; }
940
+ catch { throw new TypeError("Request body must be valid UTF-8 text"); }
941
+ }
942
+
943
+ async function bodyBytesOf(task, maximum) {
944
+ const value = await rawBodyOf(task, maximum);
945
+ return value.tooLarge
946
+ ? {data: null, bytes: value.bytes, tooLarge: true}
947
+ : {data: new Uint8Array(value.data.buffer, value.data.byteOffset, value.data.byteLength), bytes: value.bytes, tooLarge: false};
948
+ }
949
+
950
+ function canonicalServePath(rawPath) {
951
+ const source = rawPath.split("/");
952
+ const decoded = new Array(source.length);
953
+ let units = 0;
954
+ for (let index = 0; index < source.length; index += 1) {
955
+ let segment;
956
+ try { segment = decodeURIComponent(source[index]); }
957
+ catch { throw new TypeError("Request path is not valid percent-encoded UTF-8"); }
958
+ if (segment.includes("/") || segment.includes("\\") || segment.includes("\0") || segment === "." || segment === "..") {
959
+ throw new TypeError("Request path contains an unsafe encoded segment");
960
+ }
961
+ units += segment.length + (index === 0 ? 0 : 1);
962
+ if (units > maxPathCodeUnits) throw new RangeError("Request target path is too long");
963
+ decoded[index] = segment;
964
+ }
965
+ const path = decoded.join("/");
966
+ if (!path.startsWith("/")) throw new TypeError("Request target must use an absolute path");
967
+ return path;
968
+ }
969
+
970
+ function serveHeaderPairs(request) {
971
+ const headers = [];
972
+ let bytes = 0;
973
+ for (const [name, value] of Object.entries(request.headers)) {
974
+ if (typeof value !== "string" && !Array.isArray(value)) continue;
975
+ const text = typeof value === "string" ? value : value.join(", ");
976
+ bytes += Buffer.byteLength(name, "utf8") + Buffer.byteLength(text, "utf8");
977
+ if (bytes > maxServeHeaderTextBytes || headers.length >= maxServeQueryFields) throw new RequestHeadersTooLarge("Request headers are too large");
978
+ headers.push([name, text]);
979
+ }
980
+ return {headers, bytes};
981
+ }
982
+
983
+ function serveQueryPairs(source) {
984
+ const query = [];
985
+ let bytes = 0;
986
+ if (source === "") return {query, bytes};
987
+ const fields = source.split("&");
988
+ if (fields.length > maxServeQueryFields) throw new RangeError("Request query is too large");
989
+ for (const field of fields) {
990
+ const separator = field.indexOf("=");
991
+ const rawName = separator < 0 ? field : field.slice(0, separator);
992
+ const rawValue = separator < 0 ? "" : field.slice(separator + 1);
993
+ let name;
994
+ let value;
995
+ try {
996
+ name = decodeURIComponent(rawName.replaceAll("+", " "));
997
+ value = decodeURIComponent(rawValue.replaceAll("+", " "));
998
+ } catch { throw new TypeError("Request query is not valid percent-encoded UTF-8"); }
999
+ bytes += Buffer.byteLength(name, "utf8") + Buffer.byteLength(value, "utf8");
1000
+ if (bytes > maxServeRequestTargetBytes) throw new RangeError("Request query is too large");
1001
+ query.push([name, value]);
1002
+ }
1003
+ return {query, bytes};
1004
+ }
1005
+
1006
+ function rejectIncomingRequest(request, response, status, message) {
1007
+ response.statusCode = status;
1008
+ response.setHeader("Connection", "close");
1009
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
1010
+ response.end(message);
1011
+ request.resume();
1012
+ }
1013
+
752
1014
  function incomingRequest(server, request, response) {
753
- if (requests.size >= maxRequests) { response.statusCode = 503; response.end("Service unavailable"); return; }
1015
+ if (server.stopping) { rejectIncomingRequest(request, response, 503, "Service unavailable"); return; }
1016
+ if (requests.size >= maxRequests) { rejectIncomingRequest(request, response, 503, "Service unavailable"); return; }
754
1017
  const target = request.url ?? "/";
755
- if (typeof target !== "string" || target.length === 0 || target.length > 2 * 1024 * 1024 || target.includes("\0")) {
756
- response.statusCode = 400; response.end("Bad request"); return;
1018
+ if (typeof target !== "string" || target.length === 0 || !target.startsWith("/") || Buffer.byteLength(target, "utf8") > maxServeRequestTargetBytes || target.includes("\0")) {
1019
+ rejectIncomingRequest(request, response, 414, "Request target too long"); return;
757
1020
  }
758
- let url;
759
1021
  let path;
1022
+ let headerResult;
1023
+ let queryResult;
760
1024
  try {
761
- url = new URL(target, "http://velar.invalid");
762
- path = decodeURIComponent(url.pathname);
763
- } catch { response.statusCode = 400; response.end("Bad request"); return; }
764
- if (path.length === 0 || path.length > maxPathCodeUnits || path.includes("\0")) {
765
- response.statusCode = 414; response.end("Request target too long"); return;
1025
+ if (target.includes("#")) throw new TypeError("Request target must not contain a URL fragment");
1026
+ const separator = target.indexOf("?");
1027
+ path = canonicalServePath(separator < 0 ? target : target.slice(0, separator));
1028
+ headerResult = serveHeaderPairs(request);
1029
+ queryResult = serveQueryPairs(separator < 0 ? "" : target.slice(separator + 1));
1030
+ } catch (error) {
1031
+ const status = error instanceof RequestHeadersTooLarge ? 431 : error instanceof RangeError ? 414 : 400;
1032
+ rejectIncomingRequest(request, response, status, status === 431 ? "Request headers too large" : status === 414 ? "Request target too long" : "Bad request");
1033
+ return;
766
1034
  }
767
- const headers = [];
768
- for (const [name, value] of Object.entries(request.headers)) {
769
- if (typeof value === "string") headers.push([name, value]);
770
- else if (Array.isArray(value)) headers.push([name, value.join(", ")]);
1035
+ const declaredText = request.headers["content-length"];
1036
+ if (typeof declaredText === "string" && /^[0-9]+$/u.test(declaredText)) {
1037
+ const declared = Number(declaredText);
1038
+ if (!Number.isSafeInteger(declared) || declared > maxServeBodyBytes) {
1039
+ rejectIncomingRequest(request, response, 413, "Request body too large");
1040
+ return;
1041
+ }
771
1042
  }
772
- const query = [];
773
- for (const [name, value] of url.searchParams) if (!query.some(item => item[0] === name)) query.push([name, value]);
774
1043
  const handle = allocateHandle(requests, nextRequestHandle, maxRequests, "Node serve request");
775
1044
  nextRequestHandle = advanceHandle(handle);
776
1045
  const task = {
777
- handle, server: server.handle, request, response, body: null, streamBytes: 0, streaming: false,
778
- reservedBytes: 0, completed: false, abandoned: false, transportDone: false, activeOperations: 0,
1046
+ handle, token: server.token, server: server.handle, request, response, body: null, streamBytes: 0, responseMode: "idle", writeActive: false, suppressBody: false,
1047
+ reservedBytes: 0, completed: false, abandoned: false, cancelled: false, transportDone: false, activeOperations: 0,
779
1048
  };
1049
+ const metadataBytes = (Buffer.byteLength(target, "utf8") + headerResult.bytes + queryResult.bytes + Buffer.byteLength(path, "utf8") + 256) * 2;
1050
+ if (metadataBytes > maxServeRequestMetadataBytes) { rejectIncomingRequest(request, response, 431, "Request metadata too large"); return; }
1051
+ try { reserveServeBytes(task, metadataBytes); }
1052
+ catch { rejectIncomingRequest(request, response, 503, "Service unavailable"); return; }
780
1053
  requests.set(handle, task);
781
- response.once("finish", () => closeRequest(task));
782
- response.once("close", () => closeRequest(task));
1054
+ request.once("aborted", () => cancelRequest(task));
1055
+ response.once("finish", () => closeRequest(task, false));
1056
+ response.once("close", () => closeRequest(task, !response.writableFinished));
783
1057
  port.postMessage({kind: "event", event: "serve.request", value: {
784
1058
  token: server.token,
785
1059
  request: handle,
786
1060
  method: request.method ?? "GET",
787
1061
  path,
788
- query,
789
- headers,
1062
+ query: queryResult.query,
1063
+ headers: headerResult.headers,
790
1064
  }});
791
1065
  }
792
1066
 
@@ -797,33 +1071,98 @@ async function startServer(args) {
797
1071
  const host = boundedHost(args[2]);
798
1072
  const handle = allocateHandle(servers, nextServerHandle, maxServers, "Node serve server");
799
1073
  nextServerHandle = advanceHandle(handle);
800
- const task = {handle, token, server: null};
801
- const server = createServer((request, response) => incomingRequest(task, request, response));
1074
+ const task = {handle, token, server: null, sockets: new Set(), stopping: false, stopPromise: null};
1075
+ const server = createServer({
1076
+ maxHeaderSize: maxServeHeaderTextBytes,
1077
+ headersTimeout: serveHeadersTimeoutMilliseconds,
1078
+ requestTimeout: serveRequestTimeoutMilliseconds,
1079
+ keepAliveTimeout: serveKeepAliveTimeoutMilliseconds,
1080
+ connectionsCheckingInterval: 1000,
1081
+ }, (request, response) => incomingRequest(task, request, response));
802
1082
  task.server = server;
803
- await new Promise((resolveListen, rejectListen) => {
804
- const failed = error => { server.off("listening", ready); rejectListen(error); };
805
- const ready = () => { server.off("error", failed); resolveListen(); };
806
- server.once("error", failed);
807
- server.once("listening", ready);
808
- server.listen({port: portValue, host});
1083
+ server.maxConnections = maxServeSocketsPerServer;
1084
+ server.maxRequestsPerSocket = 1000;
1085
+ server.on("connection", socket => {
1086
+ if (task.stopping || task.sockets.size >= maxServeSocketsPerServer || activeServeSockets >= maxServeSockets) {
1087
+ socket.destroy();
1088
+ return;
1089
+ }
1090
+ task.sockets.add(socket);
1091
+ activeServeSockets += 1;
1092
+ socket.setNoDelay(true);
1093
+ socket.setKeepAlive(true, serveKeepAliveTimeoutMilliseconds);
1094
+ socket.once("close", () => {
1095
+ if (!task.sockets.delete(socket)) return;
1096
+ activeServeSockets -= 1;
1097
+ if (activeServeSockets < 0) activeServeSockets = 0;
1098
+ });
809
1099
  });
810
- const address = server.address();
811
- if (!address || typeof address === "string") throw new Error("velar/serve could not determine the bound port");
812
1100
  servers.set(handle, task);
813
- return {handle, port: address.port};
1101
+ try {
1102
+ await new Promise((resolveListen, rejectListen) => {
1103
+ const failed = error => { server.off("listening", ready); rejectListen(error); };
1104
+ const ready = () => { server.off("error", failed); resolveListen(); };
1105
+ server.once("error", failed);
1106
+ server.once("listening", ready);
1107
+ server.listen({port: portValue, host});
1108
+ });
1109
+ const address = server.address();
1110
+ if (!address || typeof address === "string") throw new Error("velar/serve could not determine the bound port");
1111
+ server.on("error", error => {
1112
+ if (task.stopping) return;
1113
+ const failure = errorRecord(error);
1114
+ try { port.postMessage({kind: "event", event: "serve.error", value: {token, message: failure.message}}); }
1115
+ catch {}
1116
+ });
1117
+ return {handle, port: address.port};
1118
+ } catch (error) {
1119
+ if (servers.get(handle) === task) servers.delete(handle);
1120
+ try { server.close(); } catch {}
1121
+ throw error;
1122
+ }
814
1123
  }
815
1124
 
816
1125
  async function stopServer(args) {
817
- if (args.length !== 1) throw new TypeError("serve.stop arguments are invalid");
1126
+ if (args.length < 1 || args.length > 2) throw new TypeError("serve.stop arguments are invalid");
818
1127
  const handle = integer(args[0], 1, Number.MAX_SAFE_INTEGER, "Node serve server handle");
1128
+ const grace = args.length === 2 ? integer(args[1], 1, 120_000, "serve stop grace") : serveShutdownTimeoutMilliseconds;
819
1129
  const task = servers.get(handle);
820
1130
  if (!task) return null;
821
- await new Promise((resolveStop, rejectStop) => {
822
- task.server.close(error => error ? rejectStop(error) : resolveStop(null));
823
- task.server.closeIdleConnections?.();
824
- });
825
- servers.delete(handle);
826
- return null;
1131
+ if (task.stopPromise !== null) return await task.stopPromise;
1132
+ task.stopping = true;
1133
+ for (const request of requests.values()) if (request.server === handle && !request.completed) {
1134
+ request.response.shouldKeepAlive = false;
1135
+ if (!request.response.headersSent) request.response.setHeader("Connection", "close");
1136
+ cancelRequest(request, "server_stopping");
1137
+ }
1138
+ const pending = (async () => {
1139
+ await new Promise((resolveStop, rejectStop) => {
1140
+ let settled = false;
1141
+ let forceTimer = null;
1142
+ let finalTimer = null;
1143
+ const finish = action => {
1144
+ if (settled) return;
1145
+ settled = true;
1146
+ if (forceTimer !== null) clearTimeout(forceTimer);
1147
+ if (finalTimer !== null) clearTimeout(finalTimer);
1148
+ action();
1149
+ };
1150
+ task.server.close(error => error ? finish(() => rejectStop(error)) : finish(() => resolveStop(null)));
1151
+ task.server.closeIdleConnections?.();
1152
+ forceTimer = setTimeout(() => {
1153
+ try { task.server.closeAllConnections?.(); } catch {}
1154
+ for (const socket of task.sockets) try { socket.destroy(); } catch {}
1155
+ finalTimer = setTimeout(() => finish(() => rejectStop(new Error("Node serve transport did not stop within its graceful shutdown deadline"))), 1000);
1156
+ finalTimer.unref?.();
1157
+ }, grace);
1158
+ forceTimer.unref?.();
1159
+ });
1160
+ if (servers.get(handle) === task) servers.delete(handle);
1161
+ return null;
1162
+ })();
1163
+ task.stopPromise = pending;
1164
+ try { return await pending; }
1165
+ catch (error) { if (task.stopPromise === pending) { task.stopPromise = null; task.stopping = false; } throw error; }
827
1166
  }
828
1167
 
829
1168
  async function dispatch(operation, args) {
@@ -980,77 +1319,129 @@ async function dispatch(operation, args) {
980
1319
  const task = requestHandle(args[0]);
981
1320
  return withRequest(task, async () => {
982
1321
  const maximum = integer(args[1], 1, maxServeBodyBytes, "Request body maxBytes");
983
- const body = await bodyOf(task);
984
- return body.bytes > maximum ? {text: null, bytes: body.bytes, tooLarge: true} : {text: body.text, bytes: body.bytes, tooLarge: false};
1322
+ return bodyOf(task, maximum);
985
1323
  });
986
1324
  }
987
- if (operation === "serve.respond") {
988
- if (args.length !== 6) throw new TypeError("serve.respond arguments are invalid");
1325
+ if (operation === "serve.bodyBytes") {
1326
+ if (args.length !== 2) throw new TypeError("serve.bodyBytes arguments are invalid");
989
1327
  const task = requestHandle(args[0]);
990
1328
  return withRequest(task, async () => {
991
- const status = integer(args[1], 100, 599, "ServeResponse.status");
1329
+ const maximum = integer(args[1], 1, maxServeBodyBytes, "Request body maxBytes");
1330
+ return bodyBytesOf(task, maximum);
1331
+ });
1332
+ }
1333
+ if (operation === "serve.readFile") {
1334
+ if (args.length !== 3) throw new TypeError("serve.readFile arguments are invalid");
1335
+ return testStaticFile(args[0], args[1], args[2]);
1336
+ }
1337
+ if (operation === "serve.respond") {
1338
+ if (args.length !== 8) throw new TypeError("serve.respond arguments are invalid");
1339
+ const task = requestHandle(args[0]);
1340
+ return withTerminalResponse(task, async () => {
1341
+ const status = integer(args[1], 200, 599, "ServeResponse.status");
992
1342
  const headers = headerPairs(args[2]);
993
1343
  const kind = args[3];
994
1344
  const body = args[4];
995
1345
  const contentType = args[5];
1346
+ const compression = args[6];
1347
+ const cookies = cookieValues(args[7]);
996
1348
  if (kind !== "json" && kind !== "text" || typeof body !== "string" || Buffer.byteLength(body, "utf8") > maxServeBodyBytes) {
997
1349
  throw new TypeError("ServeResponse body is invalid");
998
1350
  }
999
1351
  if (contentType !== null && (typeof contentType !== "string" || contentType.length === 0 || contentType.length > 1024 || /[\0\r\n]/u.test(contentType))) {
1000
1352
  throw new TypeError("ServeResponse.contentType must be bounded single-line text");
1001
1353
  }
1002
- reserveServeBytes(task, Buffer.byteLength(body, "utf8"));
1354
+ if (compression !== null && compression !== "gzip" && compression !== "br") throw new TypeError("ServeResponse compression is invalid");
1355
+ const suppressBody = responseHasNoBody(task, status);
1356
+ let output = body;
1357
+ if (!suppressBody && compression !== null) {
1358
+ const inputBytes = Buffer.byteLength(body, "utf8");
1359
+ reserveTransientServeBytes(inputBytes * 2);
1360
+ try { output = await (compression === "br" ? brotliCompress(body) : gzip(body)); }
1361
+ finally { releaseTransientServeBytes(inputBytes * 2); }
1362
+ if (output.byteLength > maxServeBodyBytes) throw new RangeError("Compressed ServeResponse exceeds 16 MiB");
1363
+ }
1364
+ if (!suppressBody) reserveServeBytes(task, typeof output === "string" ? Buffer.byteLength(output, "utf8") : output.byteLength);
1003
1365
  task.response.statusCode = status;
1004
- setHeaders(task.response, headers);
1005
- if (!task.response.hasHeader("Content-Type")) {
1366
+ setHeaders(task.response, headers, cookies);
1367
+ if (!task.response.hasHeader("Content-Type") && (task.request.method === "HEAD" || !suppressBody)) {
1006
1368
  task.response.setHeader("Content-Type", kind === "json" ? "application/json; charset=utf-8" : contentType ?? "text/plain; charset=utf-8");
1007
1369
  }
1008
- task.response.end(body);
1370
+ if (compression !== null) {
1371
+ task.response.setHeader("Content-Encoding", compression);
1372
+ if (!task.response.hasHeader("Vary")) task.response.setHeader("Vary", "Accept-Encoding");
1373
+ }
1374
+ await endServeResponse(task, suppressBody ? undefined : output);
1009
1375
  completeRequest(task);
1010
1376
  return null;
1011
1377
  });
1012
1378
  }
1013
1379
  if (operation === "serve.respondFile") {
1014
- if (args.length !== 4) throw new TypeError("serve.respondFile arguments are invalid");
1380
+ if (args.length !== 6) throw new TypeError("serve.respondFile arguments are invalid");
1015
1381
  const task = requestHandle(args[0]);
1016
- return withRequest(task, async () => {
1382
+ return withTerminalResponse(task, async () => {
1017
1383
  try {
1018
- const file = await staticFile(task, args[1], args[2], args[3]);
1019
- task.response.statusCode = 200;
1384
+ const file = await staticFile(args[1], args[2], args[3]);
1385
+ setHeaders(task.response, headerPairs(args[4]), args[5]);
1020
1386
  task.response.setHeader("Content-Type", file.contentType);
1021
- task.response.setHeader("Content-Length", file.data.byteLength);
1022
- task.response.end(file.data);
1387
+ task.response.setHeader("Accept-Ranges", "bytes");
1388
+ const etag = staticEtag(file.metadata);
1389
+ task.response.setHeader("ETag", etag);
1390
+ task.response.setHeader("Last-Modified", file.metadata.mtime.toUTCString());
1391
+ if (staticNotModified(task.request, file.metadata, etag)) {
1392
+ task.response.statusCode = 304;
1393
+ await endServeResponse(task);
1394
+ } else {
1395
+ const range = staticRange(task.request, file.metadata, etag);
1396
+ if (range === false) {
1397
+ task.response.statusCode = 416;
1398
+ task.response.setHeader("Content-Range", "bytes */" + file.metadata.size);
1399
+ await endServeResponse(task, task.request.method === "HEAD" ? undefined : "Range not satisfiable");
1400
+ } else {
1401
+ const start = range === null ? 0 : range.start;
1402
+ const end = range === null ? file.metadata.size - 1 : range.end;
1403
+ task.response.statusCode = range === null ? 200 : 206;
1404
+ task.response.setHeader("Content-Length", Math.max(0, end - start + 1));
1405
+ if (range !== null) task.response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + file.metadata.size);
1406
+ if (task.request.method !== "HEAD") await writeStaticRange(task, file, start, end);
1407
+ await endServeResponse(task);
1408
+ }
1409
+ }
1023
1410
  } catch (error) {
1024
1411
  if (!(error instanceof StaticNotFound) && !missing(error) && error?.code !== "EISDIR") throw error;
1025
1412
  task.response.statusCode = 404;
1026
1413
  task.response.setHeader("Content-Type", "text/plain; charset=utf-8");
1027
- task.response.end("Not found");
1414
+ await endServeResponse(task, "Not found");
1028
1415
  }
1029
1416
  completeRequest(task);
1030
1417
  return null;
1031
1418
  });
1032
1419
  }
1033
1420
  if (operation === "serve.streamStart") {
1034
- if (args.length !== 3) throw new TypeError("serve.streamStart arguments are invalid");
1421
+ if (args.length !== 4) throw new TypeError("serve.streamStart arguments are invalid");
1035
1422
  const task = requestHandle(args[0]);
1423
+ const status = integer(args[1], 200, 599, "ServeResponse.status");
1424
+ const headers = headerPairs(args[2]);
1036
1425
  return withRequest(task, async () => {
1037
- if (task.streaming) throw new Error("ServeResponse stream has already started");
1038
- task.response.statusCode = integer(args[1], 100, 599, "ServeResponse.status");
1039
- setHeaders(task.response, args[2]);
1040
- task.streaming = true;
1426
+ if (task.responseMode !== "idle") throw new Error("Node serve request already owns a response operation");
1427
+ task.responseMode = "streaming";
1428
+ task.response.statusCode = status;
1429
+ setHeaders(task.response, headers, args[3]);
1430
+ task.suppressBody = responseHasNoBody(task, task.response.statusCode);
1041
1431
  return null;
1042
1432
  });
1043
1433
  }
1044
1434
  if (operation === "serve.streamWrite") {
1045
1435
  if (args.length !== 2) throw new TypeError("serve.streamWrite arguments are invalid");
1046
1436
  const task = requestHandle(args[0]);
1047
- return withRequest(task, async () => {
1437
+ return withStreamWrite(task, async () => {
1048
1438
  const chunk = args[1];
1049
- if (!task.streaming || typeof chunk !== "string") throw new TypeError("ServeResponse.stream chunks must be strings");
1439
+ if (typeof chunk !== "string") throw new TypeError("ServeResponse.stream chunks must be strings");
1050
1440
  const bytes = Buffer.byteLength(chunk, "utf8");
1051
1441
  if (bytes > maxServeStreamChunkBytes || task.streamBytes + bytes > maxServeStreamBytes) {
1052
1442
  throw new RangeError("ServeResponse.stream exceeded its bounded output");
1053
1443
  }
1444
+ if (task.suppressBody) return null;
1054
1445
  if (task.response.destroyed || task.response.writableEnded) throw new Error("ServeResponse.stream client connection is closed");
1055
1446
  reserveTransientServeBytes(bytes);
1056
1447
  task.streamBytes += bytes;
@@ -1081,8 +1472,10 @@ async function dispatch(operation, args) {
1081
1472
  if (args.length !== 1) throw new TypeError("serve.streamEnd arguments are invalid");
1082
1473
  const task = requestHandle(args[0]);
1083
1474
  return withRequest(task, async () => {
1084
- if (!task.streaming) throw new Error("ServeResponse stream has not started");
1085
- task.response.end();
1475
+ if (task.responseMode !== "streaming") throw new Error("ServeResponse stream has not started");
1476
+ if (task.writeActive) throw new Error("ServeResponse stream still has an active write");
1477
+ task.responseMode = "terminal";
1478
+ await endServeResponse(task);
1086
1479
  completeRequest(task);
1087
1480
  return null;
1088
1481
  });
@@ -1090,7 +1483,7 @@ async function dispatch(operation, args) {
1090
1483
  if (operation === "serve.fail") {
1091
1484
  if (args.length !== 1) throw new TypeError("serve.fail arguments are invalid");
1092
1485
  const task = requestHandle(args[0]);
1093
- return withRequest(task, async () => { opaqueFailure(task); return null; });
1486
+ return withRequest(task, async () => { task.responseMode = "terminal"; opaqueFailure(task); return null; });
1094
1487
  }
1095
1488
  throw new TypeError("Unknown Node host operation");
1096
1489
  }
@@ -1131,7 +1524,16 @@ port.on("message", value => {
1131
1524
  }
1132
1525
  return dispatch(value.operation, value.args);
1133
1526
  }).then(
1134
- result => port.postMessage({kind: "response", id, ok: true, value: result, error: null}),
1527
+ result => {
1528
+ const message = {kind: "response", id, ok: true, value: result, error: null};
1529
+ if ((value?.operation === "serve.bodyBytes" || value?.operation === "serve.readFile") && result?.data instanceof Uint8Array && result.data.byteLength > 0) {
1530
+ let data = result.data;
1531
+ if (data.byteOffset !== 0 || data.buffer.byteLength !== data.byteLength) data = new Uint8Array(data);
1532
+ result.data = data;
1533
+ try { port.postMessage(message, [data.buffer]); }
1534
+ finally { if (value.operation === "serve.readFile") releaseTransientServeBytes(data.byteLength); }
1535
+ } else port.postMessage(message);
1536
+ },
1135
1537
  error => port.postMessage({kind: "response", id, ok: false, value: null, error: errorRecord(error)}),
1136
1538
  );
1137
1539
  });