@wxxb789/anc 0.0.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/astro.config.mjs +255 -0
  4. package/bin/anc.mjs +798 -0
  5. package/package.json +93 -0
  6. package/public/_headers +45 -0
  7. package/public/favicon.ico +0 -0
  8. package/public/favicon.svg +9 -0
  9. package/public/fonts/Temml.woff2 +0 -0
  10. package/scripts/build-snapshot.js +69 -0
  11. package/scripts/build-wasm.js +67 -0
  12. package/scripts/build-workspace.js +50 -0
  13. package/scripts/copy-snapshot.js +48 -0
  14. package/scripts/copy-wasm.js +57 -0
  15. package/scripts/emit-redirects.js +64 -0
  16. package/scripts/generate-corpus.js +421 -0
  17. package/scripts/init-repository.js +249 -0
  18. package/scripts/load-config.js +975 -0
  19. package/scripts/markdown-to-artifact.js +1286 -0
  20. package/scripts/preview-site.js +364 -0
  21. package/scripts/publish-set-review.js +157 -0
  22. package/scripts/resolve-links.js +640 -0
  23. package/scripts/run-pagefind.js +125 -0
  24. package/scripts/scan-residue.js +1016 -0
  25. package/scripts/scan-secrets.js +268 -0
  26. package/scripts/seed-gitignore.js +410 -0
  27. package/scripts/snapshot-rows.js +205 -0
  28. package/scripts/validate-content.js +95 -0
  29. package/scripts/vendor-provenance.js +307 -0
  30. package/scripts/verify-output-inventory.js +307 -0
  31. package/scripts/write-report.js +351 -0
  32. package/scripts/write-snapshot.js +136 -0
  33. package/src/components/CollectionExplorer.astro +149 -0
  34. package/src/components/FacetIndex.astro +45 -0
  35. package/src/components/LinkedNotes.astro +94 -0
  36. package/src/components/NoteGraph.astro +434 -0
  37. package/src/components/NoteList.astro +54 -0
  38. package/src/components/SiteMetadata.astro +143 -0
  39. package/src/components/TableOfContents.astro +69 -0
  40. package/src/components/TagBrowser.astro +70 -0
  41. package/src/layouts/Layout.astro +305 -0
  42. package/src/lib/artifact-source.js +72 -0
  43. package/src/lib/build-bindings.js +11 -0
  44. package/src/lib/collection-navigation.js +145 -0
  45. package/src/lib/content.js +92 -0
  46. package/src/lib/diagram-mode.js +18 -0
  47. package/src/lib/graph-layout.js +219 -0
  48. package/src/lib/graph-selection.js +84 -0
  49. package/src/lib/graph.js +42 -0
  50. package/src/lib/link-resolution.js +389 -0
  51. package/src/lib/markdown.js +1469 -0
  52. package/src/lib/math-mode.js +31 -0
  53. package/src/lib/math-output.js +131 -0
  54. package/src/lib/math.js +165 -0
  55. package/src/lib/mermaid-environment.js +475 -0
  56. package/src/lib/mermaid-render.js +769 -0
  57. package/src/lib/preview-model.js +96 -0
  58. package/src/lib/relations.js +242 -0
  59. package/src/lib/rendered-marker.js +55 -0
  60. package/src/lib/route-path.js +192 -0
  61. package/src/lib/routes.js +342 -0
  62. package/src/lib/schema.js +578 -0
  63. package/src/lib/site.js +578 -0
  64. package/src/lib/snapshot-contract.js +216 -0
  65. package/src/lib/snapshot-operations.js +147 -0
  66. package/src/lib/snapshot-queries.js +216 -0
  67. package/src/lib/snapshot-reader.js +210 -0
  68. package/src/lib/snapshot.js +233 -0
  69. package/src/lib/sqlite-warning.js +22 -0
  70. package/src/lib/sqlite.js +14 -0
  71. package/src/lib/tag-browser-model.js +18 -0
  72. package/src/lib/translations.js +533 -0
  73. package/src/lib/wasm-asset.js +47 -0
  74. package/src/lib/worker-protocol.js +93 -0
  75. package/src/pages/404.astro +35 -0
  76. package/src/pages/about.astro +100 -0
  77. package/src/pages/collections/[slug].astro +36 -0
  78. package/src/pages/collections/index.astro +33 -0
  79. package/src/pages/graph.astro +64 -0
  80. package/src/pages/index.astro +33 -0
  81. package/src/pages/notes/[slug].astro +509 -0
  82. package/src/pages/privacy.astro +121 -0
  83. package/src/pages/private.astro +59 -0
  84. package/src/pages/recent.astro +33 -0
  85. package/src/pages/robots.txt.js +15 -0
  86. package/src/pages/rss.xml.js +21 -0
  87. package/src/pages/sitemap.xml.js +20 -0
  88. package/src/pages/tags/[tag].astro +56 -0
  89. package/src/pages/tags/index.astro +37 -0
  90. package/src/scripts/code-copy.js +45 -0
  91. package/src/scripts/diagram-disabled.js +16 -0
  92. package/src/scripts/diagram.js +322 -0
  93. package/src/scripts/graph-client.js +222 -0
  94. package/src/scripts/graph-draw.js +166 -0
  95. package/src/scripts/link-preview.js +334 -0
  96. package/src/scripts/math-disabled.js +18 -0
  97. package/src/scripts/math.js +133 -0
  98. package/src/scripts/preferences.js +123 -0
  99. package/src/scripts/search-dialog.js +651 -0
  100. package/src/scripts/snapshot-client.js +157 -0
  101. package/src/scripts/snapshot-worker.js +341 -0
  102. package/src/scripts/tag-browser.js +153 -0
  103. package/src/scripts/theme-init.js +35 -0
  104. package/src/scripts/toc-active.js +38 -0
  105. package/src/styles/code.css +152 -0
  106. package/src/styles/diagram.css +149 -0
  107. package/src/styles/global.css +1739 -0
  108. package/src/styles/graph.css +317 -0
  109. package/src/styles/math.css +376 -0
  110. package/src/styles/tokens.css +171 -0
  111. package/tsconfig.json +25 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Main-thread client for the lazy snapshot Worker.
3
+ *
4
+ * Ordinary reading must download zero SQLite assets, so this module creates the
5
+ * Worker only on first explicit intent and shares one instance for previews,
6
+ * tag browsing, and graph exploration. It owns request ids, deadlines, and the
7
+ * pending bound: a reply from a torn-down Worker is discarded rather than
8
+ * attached to the current UI.
9
+ */
10
+ import { WORKER_LIMITS, } from "../lib/worker-protocol.js";
11
+ export class SnapshotClientError extends Error {
12
+ code;
13
+ constructor(code) {
14
+ super(code);
15
+ this.name = 'SnapshotClientError';
16
+ this.code = code;
17
+ }
18
+ }
19
+ let worker;
20
+ let nextId = 1;
21
+ let initialized = false;
22
+ const pending = new Map();
23
+ function failAll(code) {
24
+ for (const entry of pending.values()) {
25
+ clearTimeout(entry.timer);
26
+ entry.reject(new SnapshotClientError(code));
27
+ }
28
+ pending.clear();
29
+ }
30
+ function reset(code) {
31
+ worker?.terminate();
32
+ worker = undefined;
33
+ initialized = false;
34
+ failAll(code);
35
+ }
36
+ function ensureWorker() {
37
+ if (worker !== undefined)
38
+ return worker;
39
+ const created = new Worker(new URL('./snapshot-worker.js', import.meta.url), { type: 'module' });
40
+ created.addEventListener('message', (event) => {
41
+ const reply = event.data;
42
+ const entry = pending.get(reply.id);
43
+ if (entry === undefined)
44
+ return;
45
+ const elapsed = performance.now() - entry.started;
46
+ pending.delete(reply.id);
47
+ clearTimeout(entry.timer);
48
+ if (reply.ok) {
49
+ initialized = true;
50
+ // Observability seam for the benchmark harness, armed explicitly by the
51
+ // measurer: the operation, its dispatch-to-validated-result time, and the
52
+ // Worker's own operation time (its queries plus selection). Goal 0005
53
+ // requires the two recorded separately for goal 0008. No corpus data is
54
+ // carried, and an ordinary reader dispatches nothing. An armed request
55
+ // also gets the reply's measurement-only fields when a measured Worker
56
+ // sent them; they are added to the detail only when present, so a reply
57
+ // without them keeps the shape this seam always had.
58
+ if (typeof window !== 'undefined' && window.__snapshotMeasurement === true) {
59
+ const detail = {
60
+ type: reply.result.type,
61
+ ms: elapsed,
62
+ operationMs: reply.operationMs,
63
+ };
64
+ if (reply.sqlMs !== undefined)
65
+ detail.sqlMs = reply.sqlMs;
66
+ if (reply.phases !== undefined)
67
+ detail.phases = reply.phases;
68
+ document.dispatchEvent(new CustomEvent('snapshot-result', { detail }));
69
+ }
70
+ entry.resolve(reply.result);
71
+ }
72
+ else {
73
+ entry.reject(new SnapshotClientError(reply.code));
74
+ }
75
+ });
76
+ const crashed = () => reset('terminated');
77
+ created.addEventListener('error', crashed);
78
+ created.addEventListener('messageerror', crashed);
79
+ worker = created;
80
+ return created;
81
+ }
82
+ /**
83
+ * Release the Worker and settle what it owed.
84
+ *
85
+ * A document's snapshot cannot change in place — its binding is compiled into
86
+ * the page — so the lifecycle contract's "snapshot change" case arrives as a
87
+ * new document with a new binding, and teardown is the only in-place
88
+ * transition. Registering it on `pagehide` releases the Worker (and its
89
+ * deserialized database) with the document, without an idle timer that would
90
+ * make a later preview cold.
91
+ */
92
+ export function dispose() {
93
+ reset('cancelled');
94
+ }
95
+ // `typeof window` keeps this module loadable in Node for the client gate; a
96
+ // browser is the only environment where the listener has anything to release.
97
+ if (typeof window !== 'undefined')
98
+ window.addEventListener('pagehide', () => dispose());
99
+ /**
100
+ * Send one named operation.
101
+ *
102
+ * @throws {SnapshotClientError} with a small code on failure, a bounded reject
103
+ * when too many requests are already pending, and on a deadline.
104
+ */
105
+ export function request(message) {
106
+ // The bound applies at every stage: a ready Worker must not accumulate an
107
+ // unbounded queue of closures and timers just because initialization finished.
108
+ if (pending.size >= WORKER_LIMITS.maxPendingRequests) {
109
+ return Promise.reject(new SnapshotClientError('busy'));
110
+ }
111
+ let instance;
112
+ try {
113
+ instance = ensureWorker();
114
+ }
115
+ catch {
116
+ // A Worker the document may not construct (CSP refusal, unsupported engine)
117
+ // rejects this request instead of throwing past an awaiting caller; the
118
+ // client's init state stays clean, so the next intent tries again.
119
+ return Promise.reject(new SnapshotClientError('terminated'));
120
+ }
121
+ const deadline = initialized ? WORKER_LIMITS.requestDeadlineMs : WORKER_LIMITS.startupDeadlineMs;
122
+ return new Promise((resolve, reject) => {
123
+ const id = message.id;
124
+ const timer = setTimeout(() => {
125
+ // A cancel message cannot interrupt synchronous SQL already running in the
126
+ // Worker, so the only bounded stop is terminating it.
127
+ reset('timeout');
128
+ }, deadline);
129
+ pending.set(id, { resolve, reject, timer, started: performance.now() });
130
+ // Arming is read at dispatch, and the added flag is written on a copy: the
131
+ // function's callers construct their message inline, but a caller that
132
+ // holds its object must get it back unmutated. Unarmed, the identical
133
+ // object posted before this seam existed is what goes out.
134
+ const armed = typeof window !== 'undefined' && window.__snapshotMeasurement === true;
135
+ instance.postMessage(armed ? { ...message, measure: true } : message);
136
+ });
137
+ }
138
+ /** Mint the next request id. */
139
+ function nextRequestId() {
140
+ return nextId++;
141
+ }
142
+ /** Preview one published note. */
143
+ export function requestPreview(slug) {
144
+ return request({ id: nextRequestId(), type: 'preview', slug });
145
+ }
146
+ /** One page of a tag's members. */
147
+ export function requestByTag(tagKey, cursor, pageSize) {
148
+ return request({ id: nextRequestId(), type: 'byTag', tagKey, cursor, pageSize });
149
+ }
150
+ /** The local neighbourhood of one note. */
151
+ export function requestLocalGraph(slug) {
152
+ return request({ id: nextRequestId(), type: 'localGraph', slug });
153
+ }
154
+ /** The global graph, optionally filtered to one tag. */
155
+ export function requestGlobalGraph(tagKey) {
156
+ return request({ id: nextRequestId(), type: 'globalGraph', tagKey });
157
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * The lazy, read-only snapshot Worker.
3
+ *
4
+ * Owns the snapshot download, digest verification, WASM initialization,
5
+ * read-only import, schema validation, and query execution. The main thread
6
+ * never learns the database URL and never sends SQL: the binding is compiled
7
+ * into this module, and requests are one of the named operations.
8
+ *
9
+ * Lifecycle (`docs/core-design/build-and-runtime.md`): one init promise per
10
+ * document/snapshot; a failure clears it so a later explicit intent can retry;
11
+ * every failure falls back to the already-rendered static page.
12
+ */
13
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
14
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
15
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
16
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
17
+ });
18
+ }
19
+ return path;
20
+ };
21
+ import "../lib/build-bindings.js";
22
+ import { assertSnapshotRows } from "../lib/snapshot-contract.js";
23
+ import { edgePage, globalGraph, localGraph, preview, tagPage } from "../lib/snapshot-operations.js";
24
+ import { isSnapshotMessage, requestPageSize, WORKER_LIMITS, } from "../lib/worker-protocol.js";
25
+ /**
26
+ * The instant this module begins evaluating, before any load work.
27
+ *
28
+ * `load`'s `totalMs` is measured from here, so a measured reply reports the
29
+ * Worker's whole cold path rather than only the piece after `load` is called.
30
+ * That matters for goal 0008: the module's own evaluation and the shared
31
+ * initialization promise both precede the first reply, and a number that began
32
+ * at the fetch would silently omit them.
33
+ */
34
+ const moduleStarted = performance.now();
35
+ /** Minimal Worker global, avoiding a webworker/DOM lib declaration clash. */
36
+ const scope = self;
37
+ const SQLITE_MAGIC = [0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00];
38
+ /** A typed failure the reply maps back to a small error code. */
39
+ function fault(code) {
40
+ const error = new Error(code);
41
+ error.code = code;
42
+ throw error;
43
+ }
44
+ function codeOf(error) {
45
+ const code = error?.code;
46
+ return typeof code === 'string' ? code : 'sql';
47
+ }
48
+ /**
49
+ * Fetch with a running decoded-byte cap, cancelled before the cap is exceeded.
50
+ *
51
+ * The optional `signal` lets `load` abort the sibling download when the other
52
+ * one fails, so a failed initialization cannot leave a response draining toward
53
+ * its cap behind the failure. Exported for
54
+ * `tests/snapshot-fetch-bounded.test.ts`, which drives it against a server whose
55
+ * `Content-Length` disagrees with the body; `load` is the only production
56
+ * caller.
57
+ */
58
+ export async function fetchBounded(url, limit, signal) {
59
+ let response;
60
+ try {
61
+ response = await fetch(url, { credentials: 'same-origin', redirect: 'error', signal });
62
+ }
63
+ catch {
64
+ return fault('fetch');
65
+ }
66
+ if (!response.ok) {
67
+ // Release the connection rather than leaving the body undrained behind the
68
+ // failure the caller is about to see.
69
+ if (response.body !== null)
70
+ await response.body.cancel().catch(() => { });
71
+ fault('fetch');
72
+ }
73
+ const reader = response.body?.getReader();
74
+ if (reader === undefined) {
75
+ const buffer = new Uint8Array(await response.arrayBuffer());
76
+ if (buffer.byteLength > limit)
77
+ fault('integrity');
78
+ return buffer;
79
+ }
80
+ const chunks = [];
81
+ let total = 0;
82
+ for (;;) {
83
+ const { done, value } = await reader.read();
84
+ if (done)
85
+ break;
86
+ if (value === undefined)
87
+ continue;
88
+ total += value.byteLength;
89
+ if (total > limit) {
90
+ await reader.cancel();
91
+ fault('integrity');
92
+ }
93
+ chunks.push(value);
94
+ }
95
+ const bytes = new Uint8Array(total);
96
+ let offset = 0;
97
+ for (const chunk of chunks) {
98
+ bytes.set(chunk, offset);
99
+ offset += chunk.byteLength;
100
+ }
101
+ return bytes;
102
+ }
103
+ async function sha256(bytes) {
104
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
105
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
106
+ }
107
+ /**
108
+ * The WASM linear memory's capacity, or `null` when it is not reachable.
109
+ *
110
+ * The pinned package types `sqlite3.config.memory` as a `WebAssembly.Memory`
111
+ * (the Emscripten heap, the same object as the untyped `wasm.memory`), so the
112
+ * size is its `buffer.byteLength`: capacity after import, not bytes in use and
113
+ * not the decoded DB size. A build that stops exposing either location yields
114
+ * `null` — the phase field is documented as unknown — rather than a fabricated
115
+ * number, and the `instanceof` keeps a foreign stand-in from being read as one.
116
+ */
117
+ function wasmMemorySize(sqlite3) {
118
+ const memory = sqlite3.config?.memory ?? sqlite3.wasm.memory;
119
+ return memory instanceof WebAssembly.Memory ? memory.buffer.byteLength : null;
120
+ }
121
+ /**
122
+ * Load the pinned runtime from its same-origin entry.
123
+ *
124
+ * The URL is read from a variable so Vite leaves the import for the browser
125
+ * (`@vite-ignore`): bundling the package inlined its Emscripten glue into this
126
+ * chunk and made the residue scan read the library's strings as ANC findings.
127
+ */
128
+ async function initSqlite(bytes, wasmUrl) {
129
+ const moduleUrl = __ANC_WASM_MODULE_URL__;
130
+ const module = (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ moduleUrl, true)));
131
+ const init = module.default;
132
+ return init({ wasmBinary: bytes.buffer, locateFile: () => wasmUrl });
133
+ }
134
+ /**
135
+ * Deserialize the snapshot bytes and validate the imported schema.
136
+ *
137
+ * Exported for `tests/snapshot-worker-init.test.ts`, which drives it with a
138
+ * fake `sqlite3` and counts open handles; `load` is the only production caller.
139
+ *
140
+ * The `oo1.DB` handle owns a FREEONCLOSE copy of the bytes — up to
141
+ * `WORKER_LIMITS.maxSnapshotBytes` — and `database()` clears a failed loading
142
+ * promise so a later intent retries here, so every failure after the handle
143
+ * exists closes it before rethrowing. Without the close each retry would hold
144
+ * another handle and another copy; `docs/core-design/build-and-runtime.md`
145
+ * requires the release on failing initialization.
146
+ */
147
+ export function importSnapshot(sqlite3, databaseBytes) {
148
+ const database = new sqlite3.oo1.DB();
149
+ try {
150
+ const pointer = sqlite3.wasm.allocFromTypedArray(databaseBytes);
151
+ const result = sqlite3.capi.sqlite3_deserialize(database.pointer, 'main', pointer, databaseBytes.byteLength, databaseBytes.byteLength, sqlite3.capi.SQLITE_DESERIALIZE_READONLY | sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE);
152
+ // On failure with FREEONCLOSE, SQLite has already freed the buffer, so only
153
+ // the handle is left for the close below.
154
+ if (result !== 0)
155
+ fault('format');
156
+ database.exec('PRAGMA query_only = ON');
157
+ if (database.selectValue('PRAGMA query_only') !== 1)
158
+ fault('format');
159
+ // A deserialize sizes its page list from the buffer length, so a file whose
160
+ // last page is partial still imports — and `PRAGMA integrity_check` calls it
161
+ // `ok`. The producer's snapshot is exactly its pages; any other byte length
162
+ // is truncated or padded, and the import refuses it. The page size comes
163
+ // from the file's own header (offset 16, big-endian; 1 means 65536) rather
164
+ // than `PRAGMA page_size`, which on the pinned WASM build reports the
165
+ // connection's compiled-in default for a freshly deserialized database.
166
+ // This is the second line: `load` already fails truncated data on the
167
+ // digest before import.
168
+ const headerPageSize = (databaseBytes[16] << 8) | databaseBytes[17];
169
+ const pageSize = headerPageSize === 1 ? 65536 : headerPageSize;
170
+ let pageCount;
171
+ try {
172
+ pageCount = Number(database.selectValue('PRAGMA page_count'));
173
+ }
174
+ catch {
175
+ // A buffer too corrupt to answer `page_count` is refused as a format
176
+ // fault rather than escaping as a raw driver error.
177
+ fault('format');
178
+ }
179
+ if (!Number.isInteger(pageCount) || pageSize * pageCount !== databaseBytes.byteLength)
180
+ fault('format');
181
+ try {
182
+ assertSnapshotRows((sql) => database.selectObjects(sql));
183
+ }
184
+ catch {
185
+ fault('schema');
186
+ }
187
+ return database;
188
+ }
189
+ catch (error) {
190
+ // Best effort: a close error must not replace the failure being diagnosed.
191
+ try {
192
+ database.close();
193
+ }
194
+ catch {
195
+ // The original error is the diagnosis.
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+ async function load() {
201
+ const snapshotBinding = __ANC_SNAPSHOT_BINDING__;
202
+ const wasmBinding = __ANC_WASM_BINDING__;
203
+ if (snapshotBinding === null || wasmBinding === null)
204
+ fault('not-ready');
205
+ // Neither download depends on the other, and both are on the cold path. One
206
+ // controller bounds the pair: whichever fails first settles the `Promise.all`
207
+ // and aborts the sibling, so a failed load cannot leave a response draining
208
+ // toward its cap behind the failure.
209
+ const controller = new AbortController();
210
+ const fetchStarted = performance.now();
211
+ const [databaseBytes, wasmBytes] = await Promise.all([
212
+ fetchBounded(snapshotBinding.url, WORKER_LIMITS.maxSnapshotBytes, controller.signal),
213
+ fetchBounded(wasmBinding.url, WORKER_LIMITS.maxWasmBytes, controller.signal),
214
+ ]).finally(() => controller.abort());
215
+ const fetchMs = performance.now() - fetchStarted;
216
+ const digestStarted = performance.now();
217
+ const [databaseDigest, wasmDigest] = await Promise.all([sha256(databaseBytes), sha256(wasmBytes)]);
218
+ const digestMs = performance.now() - digestStarted;
219
+ if (databaseDigest !== snapshotBinding.digest)
220
+ fault('integrity');
221
+ if (databaseBytes.length < 100)
222
+ fault('header');
223
+ for (const [index, byte] of SQLITE_MAGIC.entries()) {
224
+ if (databaseBytes[index] !== byte)
225
+ fault('header');
226
+ }
227
+ // Bytes 18/19 are the file-format write/read versions: 1 is rollback journal,
228
+ // 2 is WAL, which cannot be deserialized as a standalone snapshot.
229
+ if (databaseBytes[18] !== 1 || databaseBytes[19] !== 1)
230
+ fault('format');
231
+ if (wasmDigest !== wasmBinding.digest)
232
+ fault('integrity');
233
+ let sqlite3;
234
+ const wasmInitStarted = performance.now();
235
+ try {
236
+ sqlite3 = await initSqlite(wasmBytes, wasmBinding.url);
237
+ }
238
+ catch {
239
+ return fault('wasm');
240
+ }
241
+ const wasmInitMs = performance.now() - wasmInitStarted;
242
+ // The schema validation inside `importSnapshot` is part of this phase: goal
243
+ // 0008's startup decomposition is fetch/hash/init/import, and `totalMs` is
244
+ // the whole module-to-ready span that also covers the header checks above.
245
+ const importStarted = performance.now();
246
+ const database = importSnapshot(sqlite3, databaseBytes);
247
+ const importMs = performance.now() - importStarted;
248
+ return {
249
+ db: { select: (sql, params) => database.selectObjects(sql, params ? [...params] : undefined) },
250
+ phases: {
251
+ totalMs: performance.now() - moduleStarted,
252
+ fetchMs,
253
+ digestMs,
254
+ wasmInitMs,
255
+ importMs,
256
+ wasmMemoryBytes: wasmMemorySize(sqlite3),
257
+ },
258
+ };
259
+ }
260
+ let loading;
261
+ /** One shared initialization promise; a failure clears it so intent can retry. */
262
+ function database() {
263
+ loading ??= load().catch((error) => {
264
+ loading = undefined;
265
+ throw error;
266
+ });
267
+ return loading;
268
+ }
269
+ function run(db, message) {
270
+ switch (message.type) {
271
+ case 'preview':
272
+ return { type: 'preview', preview: preview(db, message.slug) };
273
+ case 'outgoing':
274
+ return { type: 'outgoing', ...edgePage(db, 'outgoing', message.slug, message.cursor, requestPageSize(message)) };
275
+ case 'backlinks':
276
+ return { type: 'backlinks', ...edgePage(db, 'backlinks', message.slug, message.cursor, requestPageSize(message)) };
277
+ case 'byTag':
278
+ return { type: 'byTag', page: tagPage(db, message.tagKey, message.cursor, requestPageSize(message)) };
279
+ case 'localGraph':
280
+ return { type: 'localGraph', graph: localGraph(db, message.slug) };
281
+ case 'globalGraph':
282
+ return { type: 'globalGraph', graph: globalGraph(db, message.tagKey ?? undefined) };
283
+ }
284
+ }
285
+ function reply(message) {
286
+ scope.postMessage(message);
287
+ }
288
+ async function handle(value) {
289
+ if (!isSnapshotMessage(value)) {
290
+ const id = value?.id;
291
+ if (typeof id === 'number')
292
+ reply({ id, ok: false, code: 'bad-request' });
293
+ return;
294
+ }
295
+ try {
296
+ const loaded = await database();
297
+ // The measured request gets a wrapping adapter that sums every `select`
298
+ // span; an unmeasured request runs against the same adapter as before, so
299
+ // its reply shape and behavior are unchanged. The wrapper never changes
300
+ // what is executed or returned: `select` delegates straight through.
301
+ let sqlMs = 0;
302
+ const db = value.measure === true
303
+ ? {
304
+ select(sql, params) {
305
+ const selectStarted = performance.now();
306
+ try {
307
+ return loaded.db.select(sql, params);
308
+ }
309
+ finally {
310
+ sqlMs += performance.now() - selectStarted;
311
+ }
312
+ },
313
+ }
314
+ : loaded.db;
315
+ // The measurement goal needs the operation's SQL/Worker time separated from
316
+ // main-thread dispatch and rendering time, so a successful reply carries the
317
+ // span around `run()` alone: the shared initialization promise is awaited
318
+ // first so a cold start's download, hashing, WASM init, import, and schema
319
+ // validation can never land in a reply's duration. The span still covers
320
+ // the operation's selection and induced-edge work beside its queries; that
321
+ // is the worker-side half goal 0005 records for goal 0008. A request that
322
+ // asked to measure additionally gets `sqlMs`, the subset of that span spent
323
+ // inside `select` calls, and `phases`, the decomposition of the cold start
324
+ // that preceded it.
325
+ const started = performance.now();
326
+ const result = run(db, value);
327
+ const operationMs = performance.now() - started;
328
+ if (value.measure === true) {
329
+ reply({ id: value.id, ok: true, result, operationMs, sqlMs, phases: loaded.phases });
330
+ }
331
+ else {
332
+ reply({ id: value.id, ok: true, result, operationMs });
333
+ }
334
+ }
335
+ catch (error) {
336
+ reply({ id: value.id, ok: false, code: codeOf(error) });
337
+ }
338
+ }
339
+ scope.addEventListener('message', (event) => {
340
+ void handle(event.data);
341
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Reader-facing tag browsing over the shared lazy snapshot runtime.
3
+ *
4
+ * `byTag` is a named operation with cursor pagination; this is its reader-facing
5
+ * consumer. It is mounted on `/tags/` (a tag chooser) and `/tags/<tag>/` (one
6
+ * fixed tag), and every chrome string arrives as a `data-` attribute the build
7
+ * wrote, so no locale table ships to the browser and no tag is normalized here —
8
+ * the key is sent exactly as the static route spells it.
9
+ *
10
+ * The static `NoteList`/`FacetIndex` above it remains the complete no-JS route;
11
+ * this region is hidden until the script runs and unhides the static list on any
12
+ * failure.
13
+ */
14
+ import { noteRoute } from "../lib/route-path.js";
15
+ import { classifyTagPage, TAG_PAGE_SIZE } from "../lib/tag-browser-model.js";
16
+ import { partLanguage, TAG_BROWSE_DATASET } from "../lib/translations.js";
17
+ import { requestByTag } from "./snapshot-client.js";
18
+ const root = document.querySelector('#tag-browser');
19
+ if (root)
20
+ install(root);
21
+ function install(root) {
22
+ const mode = root.dataset['mode'] === 'switch' ? 'switch' : 'fixed';
23
+ const pageSize = Number(root.dataset['pageSize']) || TAG_PAGE_SIZE;
24
+ const staticList = document.querySelector('#tag-static-list');
25
+ const chooser = root.querySelector('#tag-browser-select');
26
+ const start = root.querySelector('#tag-browse-start');
27
+ const more = root.querySelector('#tag-browse-more');
28
+ const results = root.querySelector('#tag-browser-results');
29
+ const status = root.querySelector('#tag-browser-status');
30
+ const current = root.querySelector('#tag-browser-current');
31
+ const string = (key) => root.dataset[key] ?? '';
32
+ let generation = 0;
33
+ let tagKey = mode === 'fixed' ? (root.dataset['tagKey'] ?? '') : '';
34
+ let cursor = null;
35
+ root.hidden = false;
36
+ const reset = () => {
37
+ results.replaceChildren();
38
+ if (more !== null)
39
+ more.hidden = true;
40
+ current.hidden = true;
41
+ status.textContent = '';
42
+ };
43
+ async function load(after, forGeneration) {
44
+ if (tagKey === '') {
45
+ reset();
46
+ return;
47
+ }
48
+ // One continuation may be in flight at a time. `#tag-browse-more` is the only
49
+ // way to dispatch one, and a second click before the first reply lands would
50
+ // send the same cursor again — both replies pass the generation guard below
51
+ // and append the same page. Marked busy synchronously, before the first
52
+ // await, so a double click cannot get two handlers in; the cursor advances
53
+ // only when a reply lands, and the `finally` clears the mark for the next
54
+ // explicit continuation.
55
+ //
56
+ // `aria-disabled` plus the click handler's own check, **not** `disabled`.
57
+ // Disabling the button that holds focus drops focus to `<body>`, so a
58
+ // keyboard reader who pressed Enter on "Load more" was thrown back to the
59
+ // top of the tab order on every page. `tests/tag-browser.test.ts` asserts
60
+ // `document.activeElement` across a continuation.
61
+ if (more !== null)
62
+ more.setAttribute('aria-disabled', 'true');
63
+ status.textContent = string(TAG_BROWSE_DATASET.loading);
64
+ let page;
65
+ try {
66
+ page = (await requestByTag(tagKey, after, pageSize)).page;
67
+ }
68
+ catch {
69
+ if (forGeneration !== generation)
70
+ return;
71
+ // The static list is never removed before page 1 renders, so a failure
72
+ // leaves the complete no-JS list on screen.
73
+ if (staticList !== null)
74
+ staticList.hidden = false;
75
+ status.textContent = string(TAG_BROWSE_DATASET.failed);
76
+ return;
77
+ }
78
+ finally {
79
+ if (more !== null)
80
+ more.removeAttribute('aria-disabled');
81
+ }
82
+ if (forGeneration !== generation)
83
+ return; // a stale tag's reply cannot replace this one
84
+ const state = classifyTagPage(page);
85
+ if (state.kind === 'unknown') {
86
+ if (more !== null)
87
+ more.hidden = true;
88
+ status.textContent = string(TAG_BROWSE_DATASET.unknown);
89
+ return;
90
+ }
91
+ if (state.kind === 'empty') {
92
+ if (more !== null)
93
+ more.hidden = true;
94
+ status.textContent = string(TAG_BROWSE_DATASET.empty);
95
+ return;
96
+ }
97
+ // `partLanguage` compares case-insensitively, so the page's raw tag is fine.
98
+ const language = document.documentElement.lang || 'en';
99
+ let firstAppended;
100
+ for (const note of state.notes) {
101
+ const item = document.createElement('li');
102
+ const link = document.createElement('a');
103
+ link.href = noteRoute(note.slug);
104
+ link.textContent = note.title;
105
+ // The static tag list marks a foreign title through `partLanguage`;
106
+ // calling it here keeps the enhanced list from drifting from it.
107
+ const lang = partLanguage(note.language, language);
108
+ if (lang !== undefined)
109
+ link.lang = lang;
110
+ item.append(link);
111
+ results.append(item);
112
+ firstAppended ??= link;
113
+ }
114
+ if (page.known) {
115
+ current.textContent = page.tag.label;
116
+ current.hidden = false;
117
+ }
118
+ cursor = state.nextCursor;
119
+ if (cursor === null) {
120
+ // Hiding the focused button would drop focus to `<body>` just as above,
121
+ // so a reader who reached the last page from "Load more" continues at
122
+ // the first result that page added.
123
+ if (more !== null && document.activeElement === more)
124
+ firstAppended?.focus();
125
+ if (more !== null)
126
+ more.hidden = true;
127
+ status.textContent = string(TAG_BROWSE_DATASET.exhausted);
128
+ }
129
+ else {
130
+ if (more !== null)
131
+ more.hidden = false;
132
+ status.textContent = '';
133
+ }
134
+ }
135
+ /** Explicit intent: choosing a tag or pressing Start. */
136
+ function select(nextKey) {
137
+ const forGeneration = ++generation; // invalidates any in-flight reply
138
+ tagKey = nextKey;
139
+ cursor = null;
140
+ reset();
141
+ if (staticList !== null)
142
+ staticList.hidden = true; // only after a selection is made
143
+ void load(null, forGeneration);
144
+ }
145
+ start?.addEventListener('click', () => select(mode === 'fixed' ? (root.dataset['tagKey'] ?? '') : ''));
146
+ chooser?.addEventListener('change', () => select(chooser.value));
147
+ more?.addEventListener('click', () => {
148
+ if (more.getAttribute('aria-disabled') === 'true')
149
+ return; // a continuation is in flight
150
+ const forGeneration = generation;
151
+ void load(cursor, forGeneration);
152
+ });
153
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Applies the stored display preferences before first paint.
3
+ *
4
+ * Kept a classic script with no imports or exports so `Layout.astro` can load
5
+ * it render-blocking; see the comment there for why that matters. Ships to
6
+ * every visitor in the critical path, so it stays short.
7
+ */
8
+ (function () {
9
+ var root = document.documentElement;
10
+
11
+ /** Blocked storage throws on access, not on read. */
12
+ function stored(key) {
13
+ try {
14
+ return localStorage.getItem(key);
15
+ } catch (error) {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ // No attribute is the default: CSS then follows `prefers-color-scheme`
21
+ // through `light-dark()`. These express only an explicit override.
22
+ //
23
+ // The `publish:` prefix is duplicated from `preferences.ts`, which this file
24
+ // cannot import; `tests/design-tokens.test.ts` asserts the two agree. It names
25
+ // what wrote the value rather than this project, so a stranger's visitor does
26
+ // not carry the tool's name in their own browser storage.
27
+ var theme = stored('publish:theme');
28
+ if (theme === 'light' || theme === 'dark') root.dataset.theme = theme;
29
+
30
+ if (stored('publish:reader') === 'on') root.dataset.reader = 'on';
31
+
32
+ // Reveals the controls that only work with scripting; without this they stay
33
+ // hidden rather than rendering as buttons that do nothing.
34
+ root.dataset.js = 'on';
35
+ })();