tina4-nodejs 3.13.74 → 3.13.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.74)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.75)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.13.74 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.75 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.74",
3
+ "version": "3.13.75",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync, statSync, type Stats } from "node:fs";
2
2
  import { join, extname } from "node:path";
3
3
  import type { Tina4Request, Tina4Response } from "./types.js";
4
4
 
@@ -55,6 +55,29 @@ export function tryServeStatic(
55
55
  const ext = extname(filePath);
56
56
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
57
57
 
58
+ // Cheap validators from the file's size + mtime — no hashing needed. A weak
59
+ // ETag (W/) is correct here: two representations with the same size+mtime
60
+ // are treated as equivalent for caching. Last-Modified is second-resolution
61
+ // per HTTP.
62
+ const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
63
+ const lastModified = stat.mtime.toUTCString();
64
+
65
+ // A static asset MAY be cached but MUST be revalidated before use, so a
66
+ // redeployed file reaches the browser on the next load without a manual hard
67
+ // refresh; an unchanged asset costs a cheap 304, not a re-download. These
68
+ // ride on both the 200 and the 304 so the client always has fresh
69
+ // validators. Parity with the Python master static handler.
70
+ res.raw.setHeader("Cache-Control", "no-cache, must-revalidate");
71
+ res.raw.setHeader("ETag", etag);
72
+ res.raw.setHeader("Last-Modified", lastModified);
73
+
74
+ // Conditional request → 304 Not Modified with no body.
75
+ if (isNotModified(req, etag, stat)) {
76
+ res.raw.statusCode = 304;
77
+ res.raw.end();
78
+ return true;
79
+ }
80
+
58
81
  res.raw.setHeader("Content-Type", contentType);
59
82
  res.raw.setHeader("Content-Length", stat.size);
60
83
  res.raw.end(readFileSync(filePath));
@@ -63,3 +86,53 @@ export function tryServeStatic(
63
86
 
64
87
  return false;
65
88
  }
89
+
90
+ /**
91
+ * Read a conditional-request header off the incoming request. Works with the
92
+ * real `IncomingMessage` (`req.headers`, already lower-cased by Node) AND with
93
+ * the hand-rolled request objects used in unit tests. Header names must be
94
+ * lower-case. Returns "" when absent.
95
+ */
96
+ function conditionalHeader(req: Tina4Request, name: string): string {
97
+ const headers = (req as { headers?: Record<string, string | string[] | undefined> }).headers;
98
+ const value = headers?.[name];
99
+ if (Array.isArray(value)) return value[0] ?? "";
100
+ return value ?? "";
101
+ }
102
+
103
+ /**
104
+ * Answer whether the cached representation the client already holds is still
105
+ * current. `If-None-Match` wins over `If-Modified-Since` (RFC 7232 §3.3): when
106
+ * the client sends an entity tag we compare against that alone.
107
+ */
108
+ function isNotModified(req: Tina4Request, etag: string, stat: Stats): boolean {
109
+ const ifNoneMatch = conditionalHeader(req, "if-none-match");
110
+ if (ifNoneMatch) {
111
+ return etagMatches(ifNoneMatch, etag);
112
+ }
113
+ const ifModifiedSince = conditionalHeader(req, "if-modified-since");
114
+ if (ifModifiedSince) {
115
+ const since = Date.parse(ifModifiedSince);
116
+ if (!Number.isNaN(since)) {
117
+ // Last-Modified is second-resolution; floor the mtime so sub-second
118
+ // precision never reports a spurious "modified".
119
+ const modified = Math.floor(stat.mtimeMs / 1000) * 1000;
120
+ return modified <= since;
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+
126
+ /**
127
+ * Weak comparison of an `If-None-Match` value against our ETag. The header may
128
+ * be `*`, a single tag, or a comma-separated list; the `W/` weak prefix is
129
+ * stripped on both sides before comparing (RFC 7232 §2.3.2 weak comparison).
130
+ */
131
+ function etagMatches(ifNoneMatch: string, etag: string): boolean {
132
+ const strip = (tag: string) => tag.trim().replace(/^W\//, "");
133
+ const target = strip(etag);
134
+ return ifNoneMatch.split(",").some((candidate) => {
135
+ const trimmed = candidate.trim();
136
+ return trimmed === "*" || strip(trimmed) === target;
137
+ });
138
+ }