@aletheia-ios/tools 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +36 -2
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1061,7 +1061,10 @@ const TRAVERSAL = /^(\.\.[/\\])+/;
1061
1061
  /** How long after the last change to wait before rebuilding, so an editor's save burst is one build. */
1062
1062
  const DEBOUNCE_MS = 300;
1063
1063
  const OK = 200;
1064
+ const NOT_MODIFIED = 304;
1064
1065
  const NOT_FOUND = 404;
1066
+ /** Base 16, so the validator stays short. */
1067
+ const HEX = 16;
1065
1068
  /**
1066
1069
  * Packs and indexes the whole repository, reporting failures instead of throwing.
1067
1070
  *
@@ -1103,7 +1106,27 @@ async function resolveFile(dist, url) {
1103
1106
  if (!(existsSync(file) && (await stat(file)).isFile())) return null;
1104
1107
  return file;
1105
1108
  }
1106
- /** A static server over `dist/` that sends `cache-control: no-store` so the app never caches a dev build. */
1109
+ /**
1110
+ * A validator for the file as it is right now, from its size and modification time.
1111
+ *
1112
+ * Every rebuild rewrites the file, so the value changes and a conditional request gets the new
1113
+ * bytes. Between rebuilds it is stable, which is the only case that answers 304.
1114
+ */
1115
+ function etagOf(size, mtimeMs) {
1116
+ return `"${size.toString(HEX)}-${Math.trunc(mtimeMs).toString(HEX)}"`;
1117
+ }
1118
+ /** Whether the request already holds this version. A client may offer several. */
1119
+ function offers(header, etag) {
1120
+ if (header === void 0) return false;
1121
+ return header.split(",").some((candidate) => candidate.trim() === etag);
1122
+ }
1123
+ /**
1124
+ * A static server over `dist/`.
1125
+ *
1126
+ * Sends `cache-control: no-store` so nothing holds on to a dev build, alongside an `ETag` so a
1127
+ * client that asks can still be told nothing changed. The two are not in tension: `no-store`
1128
+ * governs whether a copy may be kept, the validator governs whether a fetch has to transfer.
1129
+ */
1107
1130
  function fileServer(dist) {
1108
1131
  return createServer(async (request, response) => {
1109
1132
  const file = await resolveFile(dist, request.url ?? "/");
@@ -1111,9 +1134,20 @@ function fileServer(dist) {
1111
1134
  response.writeHead(NOT_FOUND).end();
1112
1135
  return;
1113
1136
  }
1137
+ const { size, mtimeMs } = await stat(file);
1138
+ const etag = etagOf(size, mtimeMs);
1139
+ if (offers(request.headers["if-none-match"], etag)) {
1140
+ response.writeHead(NOT_MODIFIED, {
1141
+ etag,
1142
+ "cache-control": "no-store"
1143
+ }).end();
1144
+ return;
1145
+ }
1114
1146
  response.writeHead(OK, {
1115
1147
  "content-type": TYPES[extname(file)] ?? "application/octet-stream",
1116
- "cache-control": "no-store"
1148
+ "cache-control": "no-store",
1149
+ "content-length": size,
1150
+ etag
1117
1151
  });
1118
1152
  createReadStream(file).pipe(response);
1119
1153
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aletheia-ios/tools",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "The aletheia CLI: build, check, pack, index and serve source packages",
5
5
  "license": "MIT",
6
6
  "repository": {