@fuzdev/fuz_app 0.81.0 → 0.83.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.
@@ -1 +1 @@
1
- {"version":3,"file":"fact_store.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAKN,KAAK,QAAQ,EACb,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAC,MAAM,gCAAgC,CAAC;AAYxF,qDAAqD;AACrD,eAAO,MAAM,+BAA+B,QAAc,CAAC;AAE3D,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IACnC,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IACnE,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CAClD;AAED,oDAAoD;AACpD,eAAO,MAAM,sBAAsB,QAAO,mBAkBxC,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,qBAAa,WAAY,YAAW,SAAS;;gBAMhC,OAAO,EAAE,eAAe;IAOpC;;;;OAIG;IACG,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBzE;;;;;OAKG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqBrF;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA4B/C,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrC,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWlD,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAIxD;;;;;;;;;;;;;;;;;;OAkBG;IACG,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,GAAG,IAAI,CAAC;CAGzF"}
1
+ {"version":3,"file":"fact_store.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAKN,KAAK,QAAQ,EACb,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EACX,QAAQ,EACR,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,MAAM,gCAAgC,CAAC;AAWxC,OAAO,EAIN,KAAK,mBAAmB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,qDAAqD;AACrD,eAAO,MAAM,+BAA+B,QAAc,CAAC;AAE3D,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IACnC,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IACnE,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CAClD;AAED,oDAAoD;AACpD,eAAO,MAAM,sBAAsB,QAAO,mBAkBxC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,mBAAmB,CAAC;IACzB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,qBAAa,WAAY,YAAW,SAAS;;gBAQhC,OAAO,EAAE,eAAe;IAapC;;;;;;;OAOG;IACG,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;IA6BzE;;;;;;;;;;;;;;;;OAgBG;IACG,UAAU,CACf,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,gBAAgB,CAAC;IA6B5B;;;;;OAKG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqBrF;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA4B/C,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrC,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWlD,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAIxD;;;;;;;;;;;;;;;;;;OAkBG;IACG,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,GAAG,IAAI,CAAC;CAGzF"}
@@ -14,19 +14,21 @@
14
14
  * - mismatched external bytes return `null` + log warning (treat as
15
15
  * unavailable; GC / repair is a separate concern)
16
16
  *
17
- * Embedded vs referenced split: callers route by size. `put` rejects
18
- * `bytes.length > embedded_threshold` so oversized content takes the
19
- * `put_ref` path explicitly. Auto-split inside `put` is a future option.
20
- *
21
- * Wired with a filesystem `file:`-URL fetcher (`create_file_fact_fetcher`)
22
- * at server assembly: bytes threshold embed via `put`, larger bytes go
23
- * through atomic temp+rename onto disk then `put_ref('file:<shard>/<rest>',
24
- * size)` for verified registration.
17
+ * Embedded vs disk split: writes route by size. Bytes `<= embedded_threshold`
18
+ * land in the PG `bytes` column; larger bytes go to the disk CAS at
19
+ * `<facts_dir>/<shard>/<rest>` (`db/fact_disk_storage.ts`) and the row records a
20
+ * `file:<shard>/<rest>` `external_url`. `put` takes fully-buffered bytes;
21
+ * `put_stream` is the bounded-memory streaming twin (hash BLAKE3 + SHA-256 in
22
+ * one pass, spill past the threshold, enforce `max_bytes` / `ENOSPC`). Both need
23
+ * `disk_root` + `fs` (the `runtime/*Deps`) configured for the over-threshold
24
+ * path; without them, an oversize `put` throws and the caller must `put_ref`
25
+ * against an externally-managed URL (federation / stub-fetcher tests).
25
26
  *
26
27
  * @module
27
28
  */
28
29
  import { fact_hash_bytes, fact_hash_stream, fact_hash_verify, fact_hash_extract_refs, } from '@fuzdev/fuz_util/fact_hash.js';
29
30
  import { query_delete_fact, query_get_fact, query_get_fact_meta, query_get_fact_refs, query_has_fact, query_put_fact, query_put_fact_refs, } from './fact_queries.js';
31
+ import { create_disk_fact_fetcher, stream_fact_to_disk, write_fact_bytes_to_disk, } from './fact_disk_storage.js';
30
32
  /** Default embedded-vs-referenced cutoff (1 MiB). */
31
33
  export const FACT_EMBEDDED_THRESHOLD_DEFAULT = 1024 * 1024;
32
34
  /** Default fetcher backed by `globalThis.fetch`. */
@@ -56,28 +58,49 @@ export const create_default_fetcher = () => ({
56
58
  export class PgFactStore {
57
59
  #deps;
58
60
  #embedded_threshold;
61
+ #disk_root;
62
+ #fs;
59
63
  #fetcher;
60
64
  #log;
61
65
  constructor(options) {
62
66
  this.#deps = options.deps;
63
67
  this.#embedded_threshold = options.embedded_threshold ?? FACT_EMBEDDED_THRESHOLD_DEFAULT;
64
- this.#fetcher = options.fetcher ?? create_default_fetcher();
68
+ this.#disk_root = options.disk_root;
69
+ this.#fs = options.fs;
70
+ this.#fetcher =
71
+ options.fetcher ??
72
+ (options.disk_root !== undefined && options.fs !== undefined
73
+ ? create_disk_fact_fetcher(options.fs, options.disk_root)
74
+ : create_default_fetcher());
65
75
  this.#log = options.log;
66
76
  }
67
77
  /**
68
- * Store small bytes embedded in PG. Rejects oversized content so the
69
- * caller routes it through `put_ref` explicitly — implicit splitting
70
- * hides the size decision from the caller.
78
+ * Store fully-buffered bytes, routing by size: `<= embedded_threshold` into
79
+ * the PG `bytes` column; larger into the disk CAS (when `disk_root` + `fs`
80
+ * are configured) at `<facts_dir>/<shard>/<rest>` with a `file:` URL. Oversize
81
+ * without a disk root throws so the caller routes it through `put_ref`
82
+ * explicitly. Idempotent — `ON CONFLICT DO NOTHING` + content-addressed disk
83
+ * filenames make a re-write a no-op.
71
84
  */
72
85
  async put(bytes, options) {
86
+ const hash = fact_hash_bytes(bytes);
87
+ let row_bytes;
88
+ let row_external_url;
73
89
  if (bytes.length > this.#embedded_threshold) {
74
- throw new Error(`fact bytes exceed embedded threshold (${bytes.length} > ${this.#embedded_threshold}); use put_ref for external storage`);
90
+ if (this.#disk_root === undefined || this.#fs === undefined) {
91
+ throw new Error(`fact bytes exceed embedded threshold (${bytes.length} > ${this.#embedded_threshold}); configure disk_root or use put_ref for external storage`);
92
+ }
93
+ row_bytes = null;
94
+ row_external_url = await write_fact_bytes_to_disk(this.#fs, this.#disk_root, hash, bytes);
95
+ }
96
+ else {
97
+ row_bytes = bytes;
98
+ row_external_url = null;
75
99
  }
76
- const hash = fact_hash_bytes(bytes);
77
100
  const inserted = await query_put_fact(this.#deps, {
78
101
  hash,
79
- bytes,
80
- external_url: null,
102
+ bytes: row_bytes,
103
+ external_url: row_external_url,
81
104
  content_type: options?.content_type ?? null,
82
105
  size: bytes.length,
83
106
  });
@@ -86,6 +109,42 @@ export class PgFactStore {
86
109
  }
87
110
  return hash;
88
111
  }
112
+ /**
113
+ * Stream bytes into the store with bounded memory, returning the finalized
114
+ * digests + size. Delegates the byte path to `stream_fact_to_disk` (hash
115
+ * BLAKE3 + SHA-256 in one pass, buffer to the embedded threshold, spill to the
116
+ * disk CAS), then inserts the `fact` row by placement — embedded bytes go to
117
+ * the PG `bytes` column, disk-spilled bytes record the `file:` `external_url`.
118
+ * The cap is enforced mid-stream (`PayloadTooLargeError`); a disk-full mid-
119
+ * stream throws `StorageFullError`.
120
+ *
121
+ * Refs: explicit `options.refs` are recorded; JSON auto-extraction is NOT
122
+ * attempted (it would need a buffered re-read, defeating the bounded-memory
123
+ * contract) — streamed uploads are opaque blobs.
124
+ *
125
+ * Requires `fs` (and, for the over-threshold spill, `disk_root`) to be
126
+ * configured. The streaming twin of `put`; mirrors the Rust
127
+ * `FactStore::put_stream`.
128
+ */
129
+ async put_stream(stream, max_bytes, options) {
130
+ if (this.#fs === undefined) {
131
+ throw new Error('PgFactStore.put_stream requires `fs` (FactDiskStorageDeps) to be configured');
132
+ }
133
+ const streamed = await stream_fact_to_disk(this.#fs, this.#disk_root, stream, max_bytes, this.#embedded_threshold);
134
+ const row_bytes = streamed.placement.kind === 'embedded' ? streamed.placement.bytes : null;
135
+ const row_external_url = streamed.placement.kind === 'disk' ? streamed.placement.external_url : null;
136
+ const inserted = await query_put_fact(this.#deps, {
137
+ hash: streamed.hash,
138
+ bytes: row_bytes,
139
+ external_url: row_external_url,
140
+ content_type: options?.content_type ?? null,
141
+ size: streamed.size,
142
+ });
143
+ if (inserted && options?.refs && options.refs.length > 0) {
144
+ await query_put_fact_refs(this.#deps, streamed.hash, options.refs);
145
+ }
146
+ return { hash: streamed.hash, sha256: streamed.sha256, size: streamed.size };
147
+ }
89
148
  /**
90
149
  * Stream-hash external content and record `(hash, external_url, size)`.
91
150
  * Throws when the streamed byte count disagrees with the caller's
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Typed errors thrown by `PgFactStore.put_stream` so a file-store route can
3
+ * map them to the canonical wire responses.
4
+ *
5
+ * The Rust twin uses `FactError::PayloadTooLarge` / `::StorageFull` (`fuz_fact`);
6
+ * these TS classes carry the same two cases so the upload handler can branch
7
+ * identically and return the same status + body shape (`413` / `507`).
8
+ *
9
+ * @module
10
+ */
11
+ /**
12
+ * The streamed upload exceeded the byte cap. Thrown by `put_stream` when its
13
+ * mid-stream counter passes `max_bytes` — the backstop for a chunked or
14
+ * mis-declared `Content-Length` that the cheap header pre-check can't catch.
15
+ * A consumer route maps this to `413`.
16
+ */
17
+ export declare class PayloadTooLargeError extends Error {
18
+ /** Bytes read before the cap tripped (may exceed `max_bytes` by one chunk). */
19
+ readonly bytes_read: number;
20
+ readonly max_bytes: number;
21
+ constructor(bytes_read: number, max_bytes: number);
22
+ }
23
+ /**
24
+ * The disk filled mid-stream (`ENOSPC`). Thrown by `put_stream` when the
25
+ * temp-file write fails for lack of space — the real disk-full guarantee that
26
+ * a best-effort free-space preflight can't promise (chunked uploads, TOCTOU
27
+ * races). A consumer route maps this to `507`.
28
+ */
29
+ export declare class StorageFullError extends Error {
30
+ constructor(cause?: unknown);
31
+ }
32
+ /**
33
+ * Whether a thrown value is a Node filesystem `ENOSPC` (no space left on
34
+ * device). Used by the streaming disk write to translate the raw FS error
35
+ * into a `StorageFullError`.
36
+ */
37
+ export declare const is_enospc_error: (err: unknown) => boolean;
38
+ //# sourceMappingURL=fact_store_errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact_store_errors.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_store_errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC9C,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBACf,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAMjD;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,KAAK,CAAC,EAAE,OAAO;CAI3B;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,OAAO,KAAG,OAIH,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Typed errors thrown by `PgFactStore.put_stream` so a file-store route can
3
+ * map them to the canonical wire responses.
4
+ *
5
+ * The Rust twin uses `FactError::PayloadTooLarge` / `::StorageFull` (`fuz_fact`);
6
+ * these TS classes carry the same two cases so the upload handler can branch
7
+ * identically and return the same status + body shape (`413` / `507`).
8
+ *
9
+ * @module
10
+ */
11
+ /**
12
+ * The streamed upload exceeded the byte cap. Thrown by `put_stream` when its
13
+ * mid-stream counter passes `max_bytes` — the backstop for a chunked or
14
+ * mis-declared `Content-Length` that the cheap header pre-check can't catch.
15
+ * A consumer route maps this to `413`.
16
+ */
17
+ export class PayloadTooLargeError extends Error {
18
+ /** Bytes read before the cap tripped (may exceed `max_bytes` by one chunk). */
19
+ bytes_read;
20
+ max_bytes;
21
+ constructor(bytes_read, max_bytes) {
22
+ super(`payload too large: read ${bytes_read} bytes, exceeds ${max_bytes} byte limit`);
23
+ this.name = 'PayloadTooLargeError';
24
+ this.bytes_read = bytes_read;
25
+ this.max_bytes = max_bytes;
26
+ }
27
+ }
28
+ /**
29
+ * The disk filled mid-stream (`ENOSPC`). Thrown by `put_stream` when the
30
+ * temp-file write fails for lack of space — the real disk-full guarantee that
31
+ * a best-effort free-space preflight can't promise (chunked uploads, TOCTOU
32
+ * races). A consumer route maps this to `507`.
33
+ */
34
+ export class StorageFullError extends Error {
35
+ constructor(cause) {
36
+ super('storage_full', cause === undefined ? undefined : { cause });
37
+ this.name = 'StorageFullError';
38
+ }
39
+ }
40
+ /**
41
+ * Whether a thrown value is a Node filesystem `ENOSPC` (no space left on
42
+ * device). Used by the streaming disk write to translate the raw FS error
43
+ * into a `StorageFullError`.
44
+ */
45
+ export const is_enospc_error = (err) => typeof err === 'object' &&
46
+ err !== null &&
47
+ 'code' in err &&
48
+ err.code === 'ENOSPC';
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Canonical filesystem-fact URL shape.
2
+ * Canonical filesystem-fact URL shape + on-disk layout.
3
3
  *
4
- * `external_url` on the generic `facts` row is `string | null` because the
4
+ * `external_url` on the generic `fact` row is `string | null` because the
5
5
  * `FactStore` interface stays federation-friendly (future
6
6
  * `https://...` / `s3://...` shapes). Filesystem-minted URLs are exactly
7
7
  * `file:<shard>/<rest>` where `<shard>` is the first 2 hex chars of the
@@ -9,10 +9,11 @@
9
9
  * `<facts_dir>/<shard>/<rest>` after the writer atomically temp+renames
10
10
  * them in.
11
11
  *
12
- * Centralizing the regex keeps the shape in one place: the `serve_fact_route`
13
- * defense-in-depth check and the `file_fact_fetcher` resolver both validate
14
- * against it, so federation work that loosens or extends the shape (e.g.,
15
- * admitting `https://`) updates one place, not several.
12
+ * Centralizing the regex + the `fact_disk_path` split keeps the shape in one
13
+ * place: `PgFactStore`'s disk CAS (`db/fact_disk_storage.ts`), the
14
+ * `serve_fact_route` defense-in-depth check, and the `file_fact_fetcher`
15
+ * resolver all derive the layout here, so the write path and the read path
16
+ * can't drift. The TS twin of the Rust `fact_disk_path` (`fuz_fact`).
16
17
  *
17
18
  * Defense-in-depth: a `..` segment can't match (`.` isn't in `[0-9a-f]`),
18
19
  * neither can absolute paths, query strings, or any non-hex character.
@@ -21,6 +22,7 @@
21
22
  *
22
23
  * @module
23
24
  */
25
+ import { type FactHash } from '@fuzdev/fuz_util/fact_hash.js';
24
26
  import { z } from 'zod';
25
27
  /** Anchored, capture-group form: `^file:(<shard>)/(<rest>)$`. */
26
28
  export declare const FILE_FACT_URL_PATTERN: RegExp;
@@ -33,6 +35,17 @@ export declare const FileFactUrl: z.core.$ZodBranded<z.ZodString, "FileFactUrl",
33
35
  export type FileFactUrl = z.infer<typeof FileFactUrl>;
34
36
  /** Type guard. Useful when discriminating a `string | null` column. */
35
37
  export declare const is_file_fact_url: (s: string) => s is FileFactUrl;
38
+ /**
39
+ * Split a `FactHash` into its on-disk `<shard>/<rest>` parts — the first 2
40
+ * hex chars of the digest (shard subdir) + the remaining 62. The single
41
+ * source of truth for the disk layout, so the write path (`put` /
42
+ * `put_stream`) and the URL minted into the `fact` row can't disagree.
43
+ * Mirrors the Rust `fact_disk_path` in `fuz_fact`.
44
+ */
45
+ export declare const fact_disk_path: (hash: FactHash) => {
46
+ shard: string;
47
+ rest: string;
48
+ };
36
49
  /**
37
50
  * Validate a string against the canonical shape. Returns the branded URL
38
51
  * plus its parsed parts, or `null` on shape mismatch — callers decide
@@ -45,9 +58,9 @@ export declare const parse_file_fact_url: (url: string) => {
45
58
  } | null;
46
59
  /**
47
60
  * Construct a canonical `file:<shard>/<rest>` URL. The writer side
48
- * (`server/fact_write.ts`) assembles the shape from a freshly-computed
49
- * hash; this helper centralizes the literal so a future shape change is
50
- * a single edit.
61
+ * (`db/fact_disk_storage.ts`) assembles the shape from a freshly-computed
62
+ * hash via `fact_disk_path`; this helper centralizes the literal so a
63
+ * future shape change is a single edit.
51
64
  */
52
65
  export declare const mint_file_fact_url: (shard: string, rest: string) => FileFactUrl;
53
66
  //# sourceMappingURL=file_fact_url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file_fact_url.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/file_fact_url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAmB,KAAK,QAAQ,EAAC,MAAM,+BAA+B,CAAC;AAC9E,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,QAAyC,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,WAAW,uDAA+D,CAAC;AACxF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,KAAG,CAAC,IAAI,WAA4C,CAAC;AAE/F;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,QAAQ,KAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAG3E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,MAAM,KACT;IAAC,GAAG,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAAG,IAIpD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,WAC1B,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Canonical filesystem-fact URL shape.
2
+ * Canonical filesystem-fact URL shape + on-disk layout.
3
3
  *
4
- * `external_url` on the generic `facts` row is `string | null` because the
4
+ * `external_url` on the generic `fact` row is `string | null` because the
5
5
  * `FactStore` interface stays federation-friendly (future
6
6
  * `https://...` / `s3://...` shapes). Filesystem-minted URLs are exactly
7
7
  * `file:<shard>/<rest>` where `<shard>` is the first 2 hex chars of the
@@ -9,10 +9,11 @@
9
9
  * `<facts_dir>/<shard>/<rest>` after the writer atomically temp+renames
10
10
  * them in.
11
11
  *
12
- * Centralizing the regex keeps the shape in one place: the `serve_fact_route`
13
- * defense-in-depth check and the `file_fact_fetcher` resolver both validate
14
- * against it, so federation work that loosens or extends the shape (e.g.,
15
- * admitting `https://`) updates one place, not several.
12
+ * Centralizing the regex + the `fact_disk_path` split keeps the shape in one
13
+ * place: `PgFactStore`'s disk CAS (`db/fact_disk_storage.ts`), the
14
+ * `serve_fact_route` defense-in-depth check, and the `file_fact_fetcher`
15
+ * resolver all derive the layout here, so the write path and the read path
16
+ * can't drift. The TS twin of the Rust `fact_disk_path` (`fuz_fact`).
16
17
  *
17
18
  * Defense-in-depth: a `..` segment can't match (`.` isn't in `[0-9a-f]`),
18
19
  * neither can absolute paths, query strings, or any non-hex character.
@@ -21,6 +22,7 @@
21
22
  *
22
23
  * @module
23
24
  */
25
+ import { FACT_HASH_PREFIX } from '@fuzdev/fuz_util/fact_hash.js';
24
26
  import { z } from 'zod';
25
27
  /** Anchored, capture-group form: `^file:(<shard>)/(<rest>)$`. */
26
28
  export const FILE_FACT_URL_PATTERN = /^file:([0-9a-f]{2})\/([0-9a-f]{62})$/;
@@ -32,6 +34,17 @@ export const FILE_FACT_URL_PATTERN = /^file:([0-9a-f]{2})\/([0-9a-f]{62})$/;
32
34
  export const FileFactUrl = z.string().regex(FILE_FACT_URL_PATTERN).brand('FileFactUrl');
33
35
  /** Type guard. Useful when discriminating a `string | null` column. */
34
36
  export const is_file_fact_url = (s) => FILE_FACT_URL_PATTERN.test(s);
37
+ /**
38
+ * Split a `FactHash` into its on-disk `<shard>/<rest>` parts — the first 2
39
+ * hex chars of the digest (shard subdir) + the remaining 62. The single
40
+ * source of truth for the disk layout, so the write path (`put` /
41
+ * `put_stream`) and the URL minted into the `fact` row can't disagree.
42
+ * Mirrors the Rust `fact_disk_path` in `fuz_fact`.
43
+ */
44
+ export const fact_disk_path = (hash) => {
45
+ const hex = hash.slice(FACT_HASH_PREFIX.length);
46
+ return { shard: hex.slice(0, 2), rest: hex.slice(2) };
47
+ };
35
48
  /**
36
49
  * Validate a string against the canonical shape. Returns the branded URL
37
50
  * plus its parsed parts, or `null` on shape mismatch — callers decide
@@ -45,8 +58,8 @@ export const parse_file_fact_url = (url) => {
45
58
  };
46
59
  /**
47
60
  * Construct a canonical `file:<shard>/<rest>` URL. The writer side
48
- * (`server/fact_write.ts`) assembles the shape from a freshly-computed
49
- * hash; this helper centralizes the literal so a future shape change is
50
- * a single edit.
61
+ * (`db/fact_disk_storage.ts`) assembles the shape from a freshly-computed
62
+ * hash via `fact_disk_path`; this helper centralizes the literal so a
63
+ * future shape change is a single edit.
51
64
  */
52
65
  export const mint_file_fact_url = (shard, rest) => `file:${shard}/${rest}`;
@@ -1 +1 @@
1
- {"version":3,"file":"deno.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deno.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AA0DtE;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,WAmIhE,CAAC"}
1
+ {"version":3,"file":"deno.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deno.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AA6DtE;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,WAgJhE,CAAC"}
@@ -29,7 +29,12 @@ export const create_deno_runtime = (args) => ({
29
29
  stat: async (path) => {
30
30
  try {
31
31
  const s = await Deno.stat(path);
32
- return { is_file: s.isFile, is_directory: s.isDirectory, size: s.size };
32
+ return {
33
+ is_file: s.isFile,
34
+ is_directory: s.isDirectory,
35
+ size: s.size,
36
+ mtime_ms: s.mtime?.getTime(),
37
+ };
33
38
  }
34
39
  catch {
35
40
  return null;
@@ -75,6 +80,15 @@ export const create_deno_runtime = (args) => ({
75
80
  write_text_file: (path, content) => Deno.writeTextFile(path, content),
76
81
  write_file: (path, data) => Deno.writeFile(path, data),
77
82
  rename: (old_path, new_path) => Deno.rename(old_path, new_path),
83
+ fsync: async (path) => {
84
+ const file = await Deno.open(path, { read: true });
85
+ try {
86
+ await file.sync();
87
+ }
88
+ finally {
89
+ file.close();
90
+ }
91
+ },
78
92
  remove: (path, options) => Deno.remove(path, options),
79
93
  // === HTTP ===
80
94
  fetch: globalThis.fetch,
@@ -24,6 +24,15 @@ export interface StatResult {
24
24
  * `Content-Length`) read it from a real runtime, where it is always present.
25
25
  */
26
26
  size?: number;
27
+ /**
28
+ * Last-modification time in epoch milliseconds, when the runtime reports it.
29
+ * Populated by `create_node_runtime` / `create_deno_runtime`;
30
+ * `create_mock_runtime` omits it (so a mock-backed sweep treats every temp as
31
+ * unknown-age and never reaps). Optional so loose test stubs that only assert
32
+ * `is_file` / `is_directory` don't have to supply it. The orphan-temp sweep
33
+ * (`db/fact_disk_storage.ts`) reads it to age out stale `.tmp` spill files.
34
+ */
35
+ mtime_ms?: number;
27
36
  }
28
37
  /**
29
38
  * Result of executing a command.
@@ -105,6 +114,18 @@ export interface FsWriteDeps {
105
114
  write_file: (path: string, data: Uint8Array) => Promise<void>;
106
115
  /** Rename (move) a file. */
107
116
  rename: (old_path: string, new_path: string) => Promise<void>;
117
+ /**
118
+ * Flush a file's data to stable storage (fsync). Call on a temp file after
119
+ * writing it and *before* `rename`-ing it into place when the renamed path is
120
+ * later served without re-verification — otherwise a host crash after the
121
+ * rename can surface a torn/zero file as authentic content. The fact disk CAS
122
+ * (`db/fact_disk_storage.ts`) is the one such path; it twins the Rust
123
+ * `fuz_fact` §fsync posture (data-sync before rename; the parent-dir fsync
124
+ * stays deliberately waived — a lost dirent is regenerable under content
125
+ * addressing). Real runtimes open the path, fsync, and close;
126
+ * `create_mock_runtime` no-ops (it models no durability).
127
+ */
128
+ fsync: (path: string) => Promise<void>;
108
129
  }
109
130
  /**
110
131
  * Streaming file I/O — read a file as a byte stream, or write a byte stream to
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,yCAAyC;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC9C,mCAAmC;IACnC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,+DAA+D;IAC/D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,8DAA8D;IAC9D,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,+DAA+D;IAC/D,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC3F,8FAA8F;IAC9F,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;CAClD;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,0BAA0B;IAC1B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,4BAA4B;IAC5B,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,6BAA6B;IAC7B,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,4BAA4B;IAC5B,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IACxE;;;;;OAKG;IACH,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,kCAAkC;IAClC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;;;OAOG;IACH,WAAW,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,EACnB,OAAO,CAAC,EAAE,iBAAiB,KACvB,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,yDAAyD;IACzD,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,6BAA6B;IAC7B,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,6BAA6B;IAC7B,YAAY,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,6CAA6C;IAC7C,UAAU,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3D;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,oCAAoC;IACpC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAChB,SACC,OAAO,EACP,UAAU,EACV,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,SAAS,EACT,YAAY,EACZ,WAAW,EACX,OAAO;IACR,qCAAqC;IACrC,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACrC,qCAAqC;IACrC,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,qFAAqF;IACrF,mBAAmB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3E"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"mock.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/mock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAAc,aAAa,EAAE,iBAAiB,EAAC,MAAM,WAAW,CAAC;AAIzF;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,+CAA+C;IAC/C,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,mCAAmC;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,wCAAwC;IACxC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAC,CAAC,CAAC;IACtF,sCAAsC;IACtC,qBAAqB,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAC,CAAC,CAAC;IACjE,8BAA8B;IAC9B,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,4CAA4C;IAC5C,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjD,yCAAyC;IACzC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,4BAA4B;IAC5B,WAAW,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,WAAW,CAAA;KAAC,CAAC,CAAC;IACxE,wDAAwD;IACxD,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAM,KAAK,CAAC,MAAM,CAAM,KAAG,WAoR9D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,KAAG,IAazD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,KAAG,IAEpE,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM;CAKxB"}
1
+ {"version":3,"file":"mock.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/mock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAAc,aAAa,EAAE,iBAAiB,EAAC,MAAM,WAAW,CAAC;AAIzF;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,+CAA+C;IAC/C,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,mCAAmC;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,wCAAwC;IACxC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAC,CAAC,CAAC;IACtF,sCAAsC;IACtC,qBAAqB,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAC,CAAC,CAAC;IACjE,8BAA8B;IAC9B,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,4CAA4C;IAC5C,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjD,yCAAyC;IACzC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,4BAA4B;IAC5B,WAAW,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,WAAW,CAAA;KAAC,CAAC,CAAC;IACxE,wDAAwD;IACxD,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAM,KAAK,CAAC,MAAM,CAAM,KAAG,WAuR9D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,KAAG,IAazD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,KAAG,IAEpE,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM;CAKxB"}
@@ -222,6 +222,9 @@ export const create_mock_runtime = (args = []) => {
222
222
  }
223
223
  mock_fs_bytes.set(path, merged);
224
224
  },
225
+ // The mock models no real disk, so durability is a no-op (mirrors how it
226
+ // omits `mtime_ms`). The fact disk CAS sweep + fsync stay deps-based.
227
+ fsync: async () => { },
225
228
  rename: async (old_path, new_path) => {
226
229
  const content = mock_fs.get(old_path);
227
230
  if (content !== undefined) {
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,OAAM,aAAa,CAAC,MAAM,CAAyB,KACjD,WAmLD,CAAC"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,OAAM,aAAa,CAAC,MAAM,CAAyB,KACjD,WAkMD,CAAC"}
@@ -34,7 +34,12 @@ export const create_node_runtime = (args = process.argv.slice(2)) => ({
34
34
  stat: async (path) => {
35
35
  try {
36
36
  const s = await stat(path);
37
- return { is_file: s.isFile(), is_directory: s.isDirectory(), size: s.size };
37
+ return {
38
+ is_file: s.isFile(),
39
+ is_directory: s.isDirectory(),
40
+ size: s.size,
41
+ mtime_ms: s.mtimeMs,
42
+ };
38
43
  }
39
44
  catch {
40
45
  return null;
@@ -82,6 +87,17 @@ export const create_node_runtime = (args = process.argv.slice(2)) => ({
82
87
  write_text_file: (path, content) => writeFile(path, content, 'utf-8'),
83
88
  write_file: (path, data) => writeFile(path, data),
84
89
  rename: (old_path, new_path) => rename(old_path, new_path),
90
+ fsync: async (path) => {
91
+ // fsync flushes the inode's dirty pages regardless of the fd's open mode,
92
+ // so a read handle is enough (and needs no write permission).
93
+ const handle = await open(path, 'r');
94
+ try {
95
+ await handle.sync();
96
+ }
97
+ finally {
98
+ await handle.close();
99
+ }
100
+ },
85
101
  remove: (path, options) => rm(path, options),
86
102
  // === HTTP ===
87
103
  fetch: globalThis.fetch,
@@ -14,7 +14,7 @@ import { randomBytes } from 'node:crypto';
14
14
  import { writeFile, rename, mkdir, unlink } from 'node:fs/promises';
15
15
  import { join } from 'node:path';
16
16
  import { FACT_HASH_PREFIX, fact_hash_bytes } from '@fuzdev/fuz_util/fact_hash.js';
17
- import { mint_file_fact_url } from './file_fact_url.js';
17
+ import { mint_file_fact_url } from '../db/file_fact_url.js';
18
18
  /**
19
19
  * Write `bytes` as a fact, choosing embedded (PG) vs external (disk +
20
20
  * `put_ref`) based on `embedded_threshold`. Returns the canonical
@@ -27,7 +27,7 @@ import { readFile } from 'node:fs/promises';
27
27
  import { createReadStream } from 'node:fs';
28
28
  import { Readable } from 'node:stream';
29
29
  import { join } from 'node:path';
30
- import { parse_file_fact_url } from './file_fact_url.js';
30
+ import { parse_file_fact_url } from '../db/file_fact_url.js';
31
31
  /**
32
32
  * Build a `FactExternalFetcher` that resolves `file:` URLs against the
33
33
  * filesystem. Throws on a malformed URL before touching the disk so
@@ -89,7 +89,7 @@ import { query_get_fact, query_get_fact_meta } from '../db/fact_queries.js';
89
89
  import { query_cell_get } from '../db/cell_queries.js';
90
90
  import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
91
91
  import { can_view_cell } from '../auth/cell_authorize.js';
92
- import { parse_file_fact_url } from './file_fact_url.js';
92
+ import { parse_file_fact_url } from '../db/file_fact_url.js';
93
93
  /** `Cache-Control` for fact responses — 5 min revocation window. */
94
94
  const CACHE_CONTROL = 'private, max-age=300';
95
95
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.81.0",
3
+ "version": "0.83.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -28,16 +28,17 @@
28
28
  "peerDependencies": {
29
29
  "@electric-sql/pglite": ">=0.4",
30
30
  "@fuzdev/blake3_wasm": ">=0.1.0",
31
- "@fuzdev/fuz_util": ">=0.53.4",
31
+ "@fuzdev/fuz_util": ">=0.65.1",
32
32
  "@hono/node-server": ">=1",
33
33
  "@hono/node-ws": ">=1",
34
34
  "@node-rs/argon2": ">=2",
35
35
  "@sveltejs/kit": "^2",
36
+ "esm-env": "^1",
36
37
  "hono": ">=4",
37
38
  "pg": ">=8",
38
39
  "svelte": "^5",
39
40
  "ws": ">=8",
40
- "zod": ">=4"
41
+ "zod": "^4"
41
42
  },
42
43
  "peerDependenciesMeta": {
43
44
  "@electric-sql/pglite": {
@@ -52,6 +53,9 @@
52
53
  "@node-rs/argon2": {
53
54
  "optional": true
54
55
  },
56
+ "esm-env": {
57
+ "optional": true
58
+ },
55
59
  "hono": {
56
60
  "optional": true
57
61
  },
@@ -65,19 +69,18 @@
65
69
  "devDependencies": {
66
70
  "@electric-sql/pglite": "^0.4.5",
67
71
  "@fuzdev/blake3_wasm": "^0.1.1",
68
- "@fuzdev/fuz_code": "^0.45.1",
69
- "@fuzdev/fuz_css": "^0.61.1",
70
- "@fuzdev/fuz_ui": "^0.197.0",
71
- "@fuzdev/fuz_util": "^0.63.0",
72
- "@fuzdev/gro": "^0.200.0",
72
+ "@fuzdev/fuz_code": "^0.46.0",
73
+ "@fuzdev/fuz_css": "^0.62.0",
74
+ "@fuzdev/fuz_ui": "^0.199.0",
75
+ "@fuzdev/fuz_util": "^0.65.1",
76
+ "@fuzdev/gro": "^0.204.0",
73
77
  "@hono/node-server": "^1.19.14",
74
78
  "@hono/node-ws": "^1.3.1",
75
- "@jridgewell/trace-mapping": "^0.3.31",
76
79
  "@node-rs/argon2": "^2.0.2",
77
80
  "@ryanatkn/eslint-config": "^0.12.1",
78
81
  "@sveltejs/acorn-typescript": "^1.0.9",
79
82
  "@sveltejs/adapter-static": "^3.0.10",
80
- "@sveltejs/kit": "^2.61.1",
83
+ "@sveltejs/kit": "^2.63.0",
81
84
  "@sveltejs/package": "^2.5.7",
82
85
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
83
86
  "@types/estree": "^1.0.8",
@@ -93,9 +96,9 @@
93
96
  "pg": "^8.20.0",
94
97
  "prettier": "^3.7.4",
95
98
  "prettier-plugin-svelte": "^3.5.1",
96
- "svelte": "^5.56.0",
97
- "svelte-check": "^4.4.5",
98
- "svelte-docinfo": "^0.2.1",
99
+ "svelte": "^5.56.2",
100
+ "svelte-check": "^4.6.0",
101
+ "svelte-docinfo": "^0.4.1",
99
102
  "svelte2tsx": "^0.7.52",
100
103
  "tslib": "^2.8.1",
101
104
  "typescript": "^5.9.3",
@@ -1 +0,0 @@
1
- {"version":3,"file":"file_fact_url.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/file_fact_url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,QAAyC,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,WAAW,uDAA+D,CAAC;AACxF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,KAAG,CAAC,IAAI,WAA4C,CAAC;AAE/F;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,MAAM,KACT;IAAC,GAAG,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAAG,IAIpD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,WAC1B,CAAC"}