@smeltjs/core 0.1.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.
- package/LICENSE +202 -0
- package/README.md +55 -0
- package/THIRD-PARTY.md +140 -0
- package/dist/apply.d.ts +145 -0
- package/dist/apply.d.ts.map +1 -0
- package/dist/apply.js +244 -0
- package/dist/apply.js.map +1 -0
- package/dist/cache/prefix.d.ts +164 -0
- package/dist/cache/prefix.d.ts.map +1 -0
- package/dist/cache/prefix.js +361 -0
- package/dist/cache/prefix.js.map +1 -0
- package/dist/cli/args.d.ts +71 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +308 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +161 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/config.d.ts +73 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +144 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/init.d.ts +56 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +449 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/report.d.ts +44 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +113 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/resolve.d.ts +103 -0
- package/dist/cli/resolve.d.ts.map +1 -0
- package/dist/cli/resolve.js +106 -0
- package/dist/cli/resolve.js.map +1 -0
- package/dist/cli/run.d.ts +99 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +351 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/detect.d.ts +16 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +18 -0
- package/dist/detect.js.map +1 -0
- package/dist/errors.d.ts +88 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +107 -0
- package/dist/errors.js.map +1 -0
- package/dist/hash.d.ts +13 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +16 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/lang/bash.d.ts +3 -0
- package/dist/lang/bash.d.ts.map +1 -0
- package/dist/lang/bash.js +28 -0
- package/dist/lang/bash.js.map +1 -0
- package/dist/lang/c.d.ts +3 -0
- package/dist/lang/c.d.ts.map +1 -0
- package/dist/lang/c.js +39 -0
- package/dist/lang/c.js.map +1 -0
- package/dist/lang/c_sharp.d.ts +3 -0
- package/dist/lang/c_sharp.d.ts.map +1 -0
- package/dist/lang/c_sharp.js +37 -0
- package/dist/lang/c_sharp.js.map +1 -0
- package/dist/lang/cpp.d.ts +3 -0
- package/dist/lang/cpp.d.ts.map +1 -0
- package/dist/lang/cpp.js +46 -0
- package/dist/lang/cpp.js.map +1 -0
- package/dist/lang/go.d.ts +3 -0
- package/dist/lang/go.d.ts.map +1 -0
- package/dist/lang/go.js +31 -0
- package/dist/lang/go.js.map +1 -0
- package/dist/lang/java.d.ts +3 -0
- package/dist/lang/java.d.ts.map +1 -0
- package/dist/lang/java.js +34 -0
- package/dist/lang/java.js.map +1 -0
- package/dist/lang/javascript.d.ts +3 -0
- package/dist/lang/javascript.d.ts.map +1 -0
- package/dist/lang/javascript.js +28 -0
- package/dist/lang/javascript.js.map +1 -0
- package/dist/lang/kotlin.d.ts +3 -0
- package/dist/lang/kotlin.d.ts.map +1 -0
- package/dist/lang/kotlin.js +37 -0
- package/dist/lang/kotlin.js.map +1 -0
- package/dist/lang/php.d.ts +3 -0
- package/dist/lang/php.d.ts.map +1 -0
- package/dist/lang/php.js +37 -0
- package/dist/lang/php.js.map +1 -0
- package/dist/lang/profile.d.ts +130 -0
- package/dist/lang/profile.d.ts.map +1 -0
- package/dist/lang/profile.js +2 -0
- package/dist/lang/profile.js.map +1 -0
- package/dist/lang/python.d.ts +3 -0
- package/dist/lang/python.d.ts.map +1 -0
- package/dist/lang/python.js +33 -0
- package/dist/lang/python.js.map +1 -0
- package/dist/lang/registry.d.ts +31 -0
- package/dist/lang/registry.d.ts.map +1 -0
- package/dist/lang/registry.js +89 -0
- package/dist/lang/registry.js.map +1 -0
- package/dist/lang/ruby.d.ts +3 -0
- package/dist/lang/ruby.d.ts.map +1 -0
- package/dist/lang/ruby.js +47 -0
- package/dist/lang/ruby.js.map +1 -0
- package/dist/lang/rust.d.ts +3 -0
- package/dist/lang/rust.d.ts.map +1 -0
- package/dist/lang/rust.js +45 -0
- package/dist/lang/rust.js.map +1 -0
- package/dist/lang/swift.d.ts +3 -0
- package/dist/lang/swift.d.ts.map +1 -0
- package/dist/lang/swift.js +34 -0
- package/dist/lang/swift.js.map +1 -0
- package/dist/lang/tsx.d.ts +4 -0
- package/dist/lang/tsx.d.ts.map +1 -0
- package/dist/lang/tsx.js +11 -0
- package/dist/lang/tsx.js.map +1 -0
- package/dist/lang/typescript.d.ts +18 -0
- package/dist/lang/typescript.d.ts.map +1 -0
- package/dist/lang/typescript.js +61 -0
- package/dist/lang/typescript.js.map +1 -0
- package/dist/net/policy.d.ts +49 -0
- package/dist/net/policy.d.ts.map +1 -0
- package/dist/net/policy.js +120 -0
- package/dist/net/policy.js.map +1 -0
- package/dist/plan/grammar.d.ts +38 -0
- package/dist/plan/grammar.d.ts.map +1 -0
- package/dist/plan/grammar.js +85 -0
- package/dist/plan/grammar.js.map +1 -0
- package/dist/plan/lexical.d.ts +51 -0
- package/dist/plan/lexical.d.ts.map +1 -0
- package/dist/plan/lexical.js +167 -0
- package/dist/plan/lexical.js.map +1 -0
- package/dist/plan/planners.d.ts +35 -0
- package/dist/plan/planners.d.ts.map +1 -0
- package/dist/plan/planners.js +26 -0
- package/dist/plan/planners.js.map +1 -0
- package/dist/plan/structural.d.ts +68 -0
- package/dist/plan/structural.d.ts.map +1 -0
- package/dist/plan/structural.js +458 -0
- package/dist/plan/structural.js.map +1 -0
- package/dist/repomap/cache.d.ts +53 -0
- package/dist/repomap/cache.d.ts.map +1 -0
- package/dist/repomap/cache.js +139 -0
- package/dist/repomap/cache.js.map +1 -0
- package/dist/repomap/map.d.ts +161 -0
- package/dist/repomap/map.d.ts.map +1 -0
- package/dist/repomap/map.js +302 -0
- package/dist/repomap/map.js.map +1 -0
- package/dist/repomap/rank.d.ts +52 -0
- package/dist/repomap/rank.d.ts.map +1 -0
- package/dist/repomap/rank.js +155 -0
- package/dist/repomap/rank.js.map +1 -0
- package/dist/repomap/tags.d.ts +47 -0
- package/dist/repomap/tags.d.ts.map +1 -0
- package/dist/repomap/tags.js +101 -0
- package/dist/repomap/tags.js.map +1 -0
- package/dist/retrieve.d.ts +14 -0
- package/dist/retrieve.d.ts.map +1 -0
- package/dist/retrieve.js +44 -0
- package/dist/retrieve.js.map +1 -0
- package/dist/stages.d.ts +22 -0
- package/dist/stages.d.ts.map +1 -0
- package/dist/stages.js +33 -0
- package/dist/stages.js.map +1 -0
- package/dist/stats.d.ts +40 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +37 -0
- package/dist/stats.js.map +1 -0
- package/dist/store-dir.d.ts +98 -0
- package/dist/store-dir.d.ts.map +1 -0
- package/dist/store-dir.js +379 -0
- package/dist/store-dir.js.map +1 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +56 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +298 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +68 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { RawRetrieveCounters } from './stats.ts';
|
|
2
|
+
import type { ElisionStore, RetrieveStats } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The format marker every store directory carries, and the one version this code
|
|
5
|
+
* understands. A future layout is a new version, refused loudly by old code — never a
|
|
6
|
+
* quiet reinterpretation of someone's stored bytes.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DIRECTORY_STORE_FORMAT = "smelt-elision-store";
|
|
9
|
+
export declare const DIRECTORY_STORE_VERSION = 1;
|
|
10
|
+
/** See {@link MemoryElisionStoreOptions} in `store.ts` — same escape hatch, same reason. */
|
|
11
|
+
export interface DirectoryElisionStoreOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Override the hash function, so the collision branch — unreachable with sha256 —
|
|
14
|
+
* can be tested. Production has no reason to pass this.
|
|
15
|
+
*/
|
|
16
|
+
readonly hash?: (content: string) => string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A persistent {@link ElisionStore} over a content-addressed directory. `node:fs` only —
|
|
20
|
+
* no SQLite, no new dependency, nothing that phones home. Elisions put here outlive the
|
|
21
|
+
* process, so a long-lived agent session can `retrieve()` across restarts.
|
|
22
|
+
*
|
|
23
|
+
* ## Storage layout
|
|
24
|
+
*
|
|
25
|
+
* ```text
|
|
26
|
+
* <root>/
|
|
27
|
+
* format.json { "format": "smelt-elision-store", "version": 1 } — refused if unknown
|
|
28
|
+
* blobs/<hash> one file per elision: the exact UTF-8 bytes, named by their content hash
|
|
29
|
+
* tmp/ staging for atomic writes; never read, safe to sweep
|
|
30
|
+
* retrievals.log append-only journal: `hit "<hash>"` | `miss "<hash>"` | `corrupt "<hash>"`
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* **Nothing lives in memory.** Every read — `stats()` included — comes off the disk, so
|
|
34
|
+
* two instances over the same directory (two processes, or one process before and after
|
|
35
|
+
* a restart) always agree. `stats()` is a scan; elision counts per session are small and
|
|
36
|
+
* retrieval is the model asking for material back, which is rare by design.
|
|
37
|
+
*
|
|
38
|
+
* ## Durability
|
|
39
|
+
*
|
|
40
|
+
* - **Writes are crash-safe.** A blob is written to `tmp/`, `fsync`ed, then `link(2)`ed
|
|
41
|
+
* into `blobs/` — an atomic, no-clobber publish. A torn write dies in `tmp/`, where
|
|
42
|
+
* nothing looks; a name in `blobs/` always refers to a fully written file.
|
|
43
|
+
* - **Reads verify.** `retrieve()` and `peek()` re-hash the bytes and refuse a mismatch
|
|
44
|
+
* with {@link StoreCorruptionError} — a damaged blob is never handed back as a
|
|
45
|
+
* retrieval, and "we hold damaged bytes" is distinct from {@link UnknownHashError}'s
|
|
46
|
+
* "never existed". The guard in `test/guards/persistent-store.test.ts` watches this.
|
|
47
|
+
* - **Counters survive a restart.** Every `retrieve()` appends one `fsync`ed line to
|
|
48
|
+
* `retrievals.log`, and `stats()` is a fold over it — so `expansionRate` stays
|
|
49
|
+
* meaningful across a whole session, not just one process. A crash in the middle of
|
|
50
|
+
* an append can tear at most that one line; a torn tail is skipped, costing at most
|
|
51
|
+
* the single count that was being written when the process died.
|
|
52
|
+
* - **Concurrent writers are safe.** `link(2)` refuses to clobber, so two processes
|
|
53
|
+
* putting at once race to publish and the loser verifies byte-for-byte agreement with
|
|
54
|
+
* the winner — identical content dedupes, different content under one hash is a
|
|
55
|
+
* {@link HashCollisionError}. Journal appends use `O_APPEND`. Tested with two real
|
|
56
|
+
* processes in `test/store-dir.test.ts`.
|
|
57
|
+
*
|
|
58
|
+
* ## No eviction
|
|
59
|
+
*
|
|
60
|
+
* Same rule as {@link MemoryElisionStore}: no cap, no LRU, no `clear()`. A store that
|
|
61
|
+
* can forget turns Law 3 into "reversible, usually". Elided text is smaller than the
|
|
62
|
+
* session that produced it; if disk pressure ever forces a cap, retrieval of an evicted
|
|
63
|
+
* hash must throw a distinct "evicted" error — never {@link UnknownHashError} — so the
|
|
64
|
+
* model can tell "we lost it" from "never existed". Today there is no such error because
|
|
65
|
+
* there is no such cap.
|
|
66
|
+
*
|
|
67
|
+
* ## Two deliberate choices around the edges
|
|
68
|
+
*
|
|
69
|
+
* - **The root is resolved to an absolute path at construction.** Every later path is
|
|
70
|
+
* joined from that, so a `process.chdir()` after construction cannot silently
|
|
71
|
+
* re-target the store — the bytes a relative-rooted store put before a chdir would
|
|
72
|
+
* otherwise be unreachable after it, which reads exactly like data loss.
|
|
73
|
+
* - **A failed journal append never withholds intact bytes.** `retrieve()`'s order of
|
|
74
|
+
* business is: read, verify, count, return. When the *count* cannot be written (a
|
|
75
|
+
* read-only journal, a full disk), the bytes are still returned — they are verified
|
|
76
|
+
* and the caller asked for them; refusing would turn a bookkeeping failure into
|
|
77
|
+
* Law 3 breaking. The failure is surfaced distinctly instead: a
|
|
78
|
+
* `process.emitWarning` with name `SmeltCounterWriteFailure`, so "your retrieval
|
|
79
|
+
* worked" and "your counters just went quiet" stay two separate facts. The same
|
|
80
|
+
* applies to the `miss`/`corrupt` journal lines: the store's own error for the
|
|
81
|
+
* lookup still wins over the journal's I/O error.
|
|
82
|
+
*/
|
|
83
|
+
export declare class DirectoryElisionStore implements ElisionStore {
|
|
84
|
+
#private;
|
|
85
|
+
constructor(root: string, options?: DirectoryElisionStoreOptions);
|
|
86
|
+
put(content: string): string;
|
|
87
|
+
peek(hash: string): string | undefined;
|
|
88
|
+
retrieve(hash: string): string;
|
|
89
|
+
has(hash: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The five directly-observed counts, every one read off the disk — a scan of
|
|
92
|
+
* `blobs/` plus a fold over `retrievals.log`. See {@link RawRetrieveCounters}; the
|
|
93
|
+
* derived half of the stats comes from the shared `retrieveStats()`, never here.
|
|
94
|
+
*/
|
|
95
|
+
rawCounters(): RawRetrieveCounters;
|
|
96
|
+
stats(): RetrieveStats;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=store-dir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-dir.d.ts","sourceRoot":"","sources":["../src/store-dir.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAC5D,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAYzC,4FAA4F;AAC5F,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,qBAAa,qBAAsB,YAAW,YAAY;;IAMxD,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,4BAAiC,EAiBnE;IAED,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoC3B;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrC;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAY7B;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzB;IAED;;;;OAIG;IACH,WAAW,IAAI,mBAAmB,CAqBjC;IAED,KAAK,IAAI,aAAa,CAErB;CA6IF"}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { closeSync, fsyncSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync, writeSync, } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { HashCollisionError, SmeltError, StoreCorruptionError, StoreFormatError, UnknownHashError, } from './errors.js';
|
|
6
|
+
import { contentHash } from './hash.js';
|
|
7
|
+
import { retrieveStats } from './stats.js';
|
|
8
|
+
/**
|
|
9
|
+
* The format marker every store directory carries, and the one version this code
|
|
10
|
+
* understands. A future layout is a new version, refused loudly by old code — never a
|
|
11
|
+
* quiet reinterpretation of someone's stored bytes.
|
|
12
|
+
*/
|
|
13
|
+
export const DIRECTORY_STORE_FORMAT = 'smelt-elision-store';
|
|
14
|
+
export const DIRECTORY_STORE_VERSION = 1;
|
|
15
|
+
/**
|
|
16
|
+
* What a storage key may look like. `contentHash` produces 16 lowercase hex characters;
|
|
17
|
+
* the pattern is wider so an injected test hash still works, and strict enough that a
|
|
18
|
+
* key can never traverse out of `blobs/` or collide with `format.json`.
|
|
19
|
+
*/
|
|
20
|
+
const KEY_PATTERN = /^[0-9a-f]{4,128}$/;
|
|
21
|
+
/** One journal line: a kind, a space, and the hash as a JSON string literal. */
|
|
22
|
+
const LOG_LINE = /^(hit|miss|corrupt) ("(?:[^"\\]|\\.)*")$/;
|
|
23
|
+
/**
|
|
24
|
+
* A persistent {@link ElisionStore} over a content-addressed directory. `node:fs` only —
|
|
25
|
+
* no SQLite, no new dependency, nothing that phones home. Elisions put here outlive the
|
|
26
|
+
* process, so a long-lived agent session can `retrieve()` across restarts.
|
|
27
|
+
*
|
|
28
|
+
* ## Storage layout
|
|
29
|
+
*
|
|
30
|
+
* ```text
|
|
31
|
+
* <root>/
|
|
32
|
+
* format.json { "format": "smelt-elision-store", "version": 1 } — refused if unknown
|
|
33
|
+
* blobs/<hash> one file per elision: the exact UTF-8 bytes, named by their content hash
|
|
34
|
+
* tmp/ staging for atomic writes; never read, safe to sweep
|
|
35
|
+
* retrievals.log append-only journal: `hit "<hash>"` | `miss "<hash>"` | `corrupt "<hash>"`
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* **Nothing lives in memory.** Every read — `stats()` included — comes off the disk, so
|
|
39
|
+
* two instances over the same directory (two processes, or one process before and after
|
|
40
|
+
* a restart) always agree. `stats()` is a scan; elision counts per session are small and
|
|
41
|
+
* retrieval is the model asking for material back, which is rare by design.
|
|
42
|
+
*
|
|
43
|
+
* ## Durability
|
|
44
|
+
*
|
|
45
|
+
* - **Writes are crash-safe.** A blob is written to `tmp/`, `fsync`ed, then `link(2)`ed
|
|
46
|
+
* into `blobs/` — an atomic, no-clobber publish. A torn write dies in `tmp/`, where
|
|
47
|
+
* nothing looks; a name in `blobs/` always refers to a fully written file.
|
|
48
|
+
* - **Reads verify.** `retrieve()` and `peek()` re-hash the bytes and refuse a mismatch
|
|
49
|
+
* with {@link StoreCorruptionError} — a damaged blob is never handed back as a
|
|
50
|
+
* retrieval, and "we hold damaged bytes" is distinct from {@link UnknownHashError}'s
|
|
51
|
+
* "never existed". The guard in `test/guards/persistent-store.test.ts` watches this.
|
|
52
|
+
* - **Counters survive a restart.** Every `retrieve()` appends one `fsync`ed line to
|
|
53
|
+
* `retrievals.log`, and `stats()` is a fold over it — so `expansionRate` stays
|
|
54
|
+
* meaningful across a whole session, not just one process. A crash in the middle of
|
|
55
|
+
* an append can tear at most that one line; a torn tail is skipped, costing at most
|
|
56
|
+
* the single count that was being written when the process died.
|
|
57
|
+
* - **Concurrent writers are safe.** `link(2)` refuses to clobber, so two processes
|
|
58
|
+
* putting at once race to publish and the loser verifies byte-for-byte agreement with
|
|
59
|
+
* the winner — identical content dedupes, different content under one hash is a
|
|
60
|
+
* {@link HashCollisionError}. Journal appends use `O_APPEND`. Tested with two real
|
|
61
|
+
* processes in `test/store-dir.test.ts`.
|
|
62
|
+
*
|
|
63
|
+
* ## No eviction
|
|
64
|
+
*
|
|
65
|
+
* Same rule as {@link MemoryElisionStore}: no cap, no LRU, no `clear()`. A store that
|
|
66
|
+
* can forget turns Law 3 into "reversible, usually". Elided text is smaller than the
|
|
67
|
+
* session that produced it; if disk pressure ever forces a cap, retrieval of an evicted
|
|
68
|
+
* hash must throw a distinct "evicted" error — never {@link UnknownHashError} — so the
|
|
69
|
+
* model can tell "we lost it" from "never existed". Today there is no such error because
|
|
70
|
+
* there is no such cap.
|
|
71
|
+
*
|
|
72
|
+
* ## Two deliberate choices around the edges
|
|
73
|
+
*
|
|
74
|
+
* - **The root is resolved to an absolute path at construction.** Every later path is
|
|
75
|
+
* joined from that, so a `process.chdir()` after construction cannot silently
|
|
76
|
+
* re-target the store — the bytes a relative-rooted store put before a chdir would
|
|
77
|
+
* otherwise be unreachable after it, which reads exactly like data loss.
|
|
78
|
+
* - **A failed journal append never withholds intact bytes.** `retrieve()`'s order of
|
|
79
|
+
* business is: read, verify, count, return. When the *count* cannot be written (a
|
|
80
|
+
* read-only journal, a full disk), the bytes are still returned — they are verified
|
|
81
|
+
* and the caller asked for them; refusing would turn a bookkeeping failure into
|
|
82
|
+
* Law 3 breaking. The failure is surfaced distinctly instead: a
|
|
83
|
+
* `process.emitWarning` with name `SmeltCounterWriteFailure`, so "your retrieval
|
|
84
|
+
* worked" and "your counters just went quiet" stay two separate facts. The same
|
|
85
|
+
* applies to the `miss`/`corrupt` journal lines: the store's own error for the
|
|
86
|
+
* lookup still wins over the journal's I/O error.
|
|
87
|
+
*/
|
|
88
|
+
export class DirectoryElisionStore {
|
|
89
|
+
#blobsDir;
|
|
90
|
+
#tmpDir;
|
|
91
|
+
#logPath;
|
|
92
|
+
#hash;
|
|
93
|
+
constructor(root, options = {}) {
|
|
94
|
+
this.#hash = options.hash ?? contentHash;
|
|
95
|
+
// Resolve NOW, against the working directory the caller constructed with — a
|
|
96
|
+
// later chdir must never re-point an already-constructed store. See the class doc.
|
|
97
|
+
const absoluteRoot = resolve(root);
|
|
98
|
+
this.#blobsDir = join(absoluteRoot, 'blobs');
|
|
99
|
+
this.#tmpDir = join(absoluteRoot, 'tmp');
|
|
100
|
+
this.#logPath = join(absoluteRoot, 'retrievals.log');
|
|
101
|
+
const markerPath = join(absoluteRoot, 'format.json');
|
|
102
|
+
// Validate before mutating: a directory carrying a marker this code does not
|
|
103
|
+
// understand is refused with the directory exactly as it was found — no blobs/,
|
|
104
|
+
// no tmp/, no staged temp file created inside someone else's layout.
|
|
105
|
+
const existing = this.#readMarker(markerPath);
|
|
106
|
+
if (existing !== undefined)
|
|
107
|
+
this.#verifyMarker(markerPath, existing);
|
|
108
|
+
mkdirSync(this.#blobsDir, { recursive: true });
|
|
109
|
+
mkdirSync(this.#tmpDir, { recursive: true });
|
|
110
|
+
this.#claimFormat(markerPath);
|
|
111
|
+
}
|
|
112
|
+
put(content) {
|
|
113
|
+
const hash = this.#hash(content);
|
|
114
|
+
if (!KEY_PATTERN.test(hash)) {
|
|
115
|
+
throw new SmeltError(`smelt: hash "${hash}" is not usable as a storage key — it must match ` +
|
|
116
|
+
`${String(KEY_PATTERN)} so it can name a file inside blobs/ and nothing else.`);
|
|
117
|
+
}
|
|
118
|
+
const existing = this.#readBlob(hash);
|
|
119
|
+
if (existing !== undefined) {
|
|
120
|
+
// Verify the stored bytes before comparing: a damaged blob is corruption, not a
|
|
121
|
+
// collision. Only intact bytes that still differ earn HashCollisionError.
|
|
122
|
+
if (this.#hash(existing) !== hash)
|
|
123
|
+
throw new StoreCorruptionError(hash);
|
|
124
|
+
if (existing !== content)
|
|
125
|
+
throw new HashCollisionError(hash);
|
|
126
|
+
return hash;
|
|
127
|
+
}
|
|
128
|
+
const tmpPath = this.#writeTemp(content);
|
|
129
|
+
try {
|
|
130
|
+
// link(2) is the atomic, no-clobber publish: it fails with EEXIST rather than
|
|
131
|
+
// overwrite, so a concurrent writer can never silently replace someone's bytes.
|
|
132
|
+
linkSync(tmpPath, join(this.#blobsDir, hash));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error.code !== 'EEXIST')
|
|
136
|
+
throw error;
|
|
137
|
+
// Another writer published this hash between our existence check and our link.
|
|
138
|
+
// Same bytes: idempotent put, done. Damaged or vanished bytes: corruption — the
|
|
139
|
+
// store was torn or edited outside smelt. Intact different bytes: a collision.
|
|
140
|
+
const winner = this.#readBlob(hash);
|
|
141
|
+
if (winner === undefined || this.#hash(winner) !== hash) {
|
|
142
|
+
throw new StoreCorruptionError(hash);
|
|
143
|
+
}
|
|
144
|
+
if (winner !== content)
|
|
145
|
+
throw new HashCollisionError(hash);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
unlinkSync(tmpPath);
|
|
149
|
+
}
|
|
150
|
+
fsyncDirBestEffort(this.#blobsDir);
|
|
151
|
+
return hash;
|
|
152
|
+
}
|
|
153
|
+
peek(hash) {
|
|
154
|
+
const content = this.#readBlob(hash);
|
|
155
|
+
if (content === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
if (this.#hash(content) !== hash)
|
|
158
|
+
throw new StoreCorruptionError(hash);
|
|
159
|
+
return content;
|
|
160
|
+
}
|
|
161
|
+
retrieve(hash) {
|
|
162
|
+
const content = this.#readBlob(hash);
|
|
163
|
+
if (content === undefined) {
|
|
164
|
+
this.#appendLogCounting('miss', hash);
|
|
165
|
+
throw new UnknownHashError(hash);
|
|
166
|
+
}
|
|
167
|
+
if (this.#hash(content) !== hash) {
|
|
168
|
+
this.#appendLogCounting('corrupt', hash);
|
|
169
|
+
throw new StoreCorruptionError(hash);
|
|
170
|
+
}
|
|
171
|
+
this.#appendLogCounting('hit', hash);
|
|
172
|
+
return content;
|
|
173
|
+
}
|
|
174
|
+
has(hash) {
|
|
175
|
+
return this.#readBlob(hash) !== undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The five directly-observed counts, every one read off the disk — a scan of
|
|
179
|
+
* `blobs/` plus a fold over `retrievals.log`. See {@link RawRetrieveCounters}; the
|
|
180
|
+
* derived half of the stats comes from the shared `retrieveStats()`, never here.
|
|
181
|
+
*/
|
|
182
|
+
rawCounters() {
|
|
183
|
+
let elisionsStored = 0;
|
|
184
|
+
let bytesStored = 0;
|
|
185
|
+
for (const entry of readdirSync(this.#blobsDir)) {
|
|
186
|
+
if (!KEY_PATTERN.test(entry))
|
|
187
|
+
continue; // `.DS_Store` and friends are not blobs
|
|
188
|
+
elisionsStored += 1;
|
|
189
|
+
bytesStored += statSync(join(this.#blobsDir, entry)).size;
|
|
190
|
+
}
|
|
191
|
+
let retrieveCalls = 0;
|
|
192
|
+
let misses = 0;
|
|
193
|
+
const hits = new Set();
|
|
194
|
+
for (const line of this.#readLog().split('\n')) {
|
|
195
|
+
const match = LOG_LINE.exec(line);
|
|
196
|
+
if (match === null)
|
|
197
|
+
continue; // a torn tail from a crash mid-append, or blank
|
|
198
|
+
retrieveCalls += 1;
|
|
199
|
+
if (match[1] === 'miss')
|
|
200
|
+
misses += 1;
|
|
201
|
+
else if (match[1] === 'hit')
|
|
202
|
+
hits.add(JSON.parse(match[2]));
|
|
203
|
+
}
|
|
204
|
+
return { elisionsStored, bytesStored, retrieveCalls, uniqueRetrieved: hits.size, misses };
|
|
205
|
+
}
|
|
206
|
+
stats() {
|
|
207
|
+
return retrieveStats(this.rawCounters());
|
|
208
|
+
}
|
|
209
|
+
/** The blob's exact content, or `undefined` when no such blob is stored. */
|
|
210
|
+
#readBlob(hash) {
|
|
211
|
+
if (!KEY_PATTERN.test(hash))
|
|
212
|
+
return undefined; // never a path component
|
|
213
|
+
try {
|
|
214
|
+
return readFileSync(join(this.#blobsDir, hash), 'utf8');
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error.code === 'ENOENT')
|
|
218
|
+
return undefined;
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Write content to a unique file in `tmp/`, fsynced, and return its path. */
|
|
223
|
+
#writeTemp(content) {
|
|
224
|
+
const tmpPath = join(this.#tmpDir, `${String(process.pid)}-${randomBytes(8).toString('hex')}`);
|
|
225
|
+
const fd = openSync(tmpPath, 'wx');
|
|
226
|
+
try {
|
|
227
|
+
// writeSync may write fewer bytes than asked; loop, or a short write would be
|
|
228
|
+
// fsynced and published under the full content's hash as a torn blob.
|
|
229
|
+
const bytes = Buffer.from(content, 'utf8');
|
|
230
|
+
let written = 0;
|
|
231
|
+
while (written < bytes.length) {
|
|
232
|
+
written += writeSync(fd, bytes, written);
|
|
233
|
+
}
|
|
234
|
+
fsyncSync(fd);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
closeSync(fd);
|
|
238
|
+
}
|
|
239
|
+
return tmpPath;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* A journal append on the `retrieve()` path — counting, not custody. A failure here
|
|
243
|
+
* must never decide whether the caller gets its verified bytes (or its true error),
|
|
244
|
+
* so it is caught and surfaced as a distinct `process.emitWarning` — see the class
|
|
245
|
+
* doc, and the read-only-journal case in `test/store-dir.test.ts`.
|
|
246
|
+
*/
|
|
247
|
+
#appendLogCounting(kind, hash) {
|
|
248
|
+
try {
|
|
249
|
+
this.#appendLog(kind, hash);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
process.emitWarning(`smelt: could not journal a "${kind}" for hash "${hash}" in ${this.#logPath} ` +
|
|
253
|
+
`(${error instanceof Error ? error.message : String(error)}). The retrieval ` +
|
|
254
|
+
`itself is unaffected, but this count is lost — retrieveCalls and ` +
|
|
255
|
+
`expansionRate now UNDER-report until the journal is writable again.`, 'SmeltCounterWriteFailure');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* One durable journal line. The hash is JSON-encoded because `retrieve()` takes it
|
|
260
|
+
* from the model verbatim — a hash containing a newline must not forge a second line.
|
|
261
|
+
* The record starts with its own newline so a torn tail from an earlier crash — a
|
|
262
|
+
* partial record with no trailing newline — can never bleed into this one: the tear
|
|
263
|
+
* stays on its own line and is skipped by `stats()`, as blank lines are.
|
|
264
|
+
*/
|
|
265
|
+
#appendLog(kind, hash) {
|
|
266
|
+
const fd = openSync(this.#logPath, 'a');
|
|
267
|
+
try {
|
|
268
|
+
const record = Buffer.from(`\n${kind} ${JSON.stringify(hash)}\n`, 'utf8');
|
|
269
|
+
let written = 0;
|
|
270
|
+
while (written < record.length) {
|
|
271
|
+
written += writeSync(fd, record, written);
|
|
272
|
+
}
|
|
273
|
+
fsyncSync(fd);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
closeSync(fd);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
#readLog() {
|
|
280
|
+
try {
|
|
281
|
+
return readFileSync(this.#logPath, 'utf8');
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (error.code === 'ENOENT')
|
|
285
|
+
return '';
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Write the format marker if this directory has none, or verify the one it has.
|
|
291
|
+
* Creation is atomic (write to `tmp/`, then `link`), so a concurrent creator never
|
|
292
|
+
* observes a half-written marker. The constructor pre-verified any pre-existing
|
|
293
|
+
* marker; the verify here catches only a concurrent creator's claim.
|
|
294
|
+
*/
|
|
295
|
+
#claimFormat(markerPath) {
|
|
296
|
+
const claim = () => {
|
|
297
|
+
const body = `${JSON.stringify({
|
|
298
|
+
format: DIRECTORY_STORE_FORMAT,
|
|
299
|
+
version: DIRECTORY_STORE_VERSION,
|
|
300
|
+
})}\n`;
|
|
301
|
+
const tmpPath = this.#writeTemp(body);
|
|
302
|
+
try {
|
|
303
|
+
linkSync(tmpPath, markerPath);
|
|
304
|
+
return undefined; // claimed by us; nothing to verify
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error.code !== 'EEXIST')
|
|
308
|
+
throw error;
|
|
309
|
+
return readFileSync(markerPath, 'utf8');
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
unlinkSync(tmpPath);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const existing = claim();
|
|
316
|
+
if (existing === undefined)
|
|
317
|
+
return;
|
|
318
|
+
this.#verifyMarker(markerPath, existing);
|
|
319
|
+
}
|
|
320
|
+
/** The marker's body, or `undefined` when the directory carries none. */
|
|
321
|
+
#readMarker(markerPath) {
|
|
322
|
+
try {
|
|
323
|
+
return readFileSync(markerPath, 'utf8');
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error.code === 'ENOENT')
|
|
327
|
+
return undefined;
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** Refuse a marker this version of smelt does not understand. */
|
|
332
|
+
#verifyMarker(markerPath, existing) {
|
|
333
|
+
let parsed;
|
|
334
|
+
try {
|
|
335
|
+
parsed = JSON.parse(existing);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
throw new StoreFormatError(`smelt: "${markerPath}" is not parseable JSON, so this directory cannot be ` +
|
|
339
|
+
`trusted as an elision store. Refusing to read or write it.`);
|
|
340
|
+
}
|
|
341
|
+
if (parsed.format !== DIRECTORY_STORE_FORMAT || parsed.version !== DIRECTORY_STORE_VERSION) {
|
|
342
|
+
throw new StoreFormatError(`smelt: "${markerPath}" declares format ${JSON.stringify(parsed.format)} ` +
|
|
343
|
+
`version ${JSON.stringify(parsed.version)}; this code understands ` +
|
|
344
|
+
`"${DIRECTORY_STORE_FORMAT}" version ${String(DIRECTORY_STORE_VERSION)}. ` +
|
|
345
|
+
`Refusing to reinterpret someone else's layout.`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Flush the directory entry after a publish, so the *name* survives a crash as well as
|
|
351
|
+
* the bytes. Where the platform refuses to fsync a directory (Windows does), the publish
|
|
352
|
+
* is still atomic — only the durability of the directory entry falls back to the OS's
|
|
353
|
+
* own schedule. Only that refusal is swallowed: a real I/O failure (`EIO`) propagates,
|
|
354
|
+
* because "the disk could not flush" must never be reported as a successful put.
|
|
355
|
+
*/
|
|
356
|
+
function fsyncDirBestEffort(path) {
|
|
357
|
+
let fd;
|
|
358
|
+
try {
|
|
359
|
+
fd = openSync(path, 'r');
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return; // the platform refuses to even open a directory for reading (Windows)
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
fsyncSync(fd);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
const code = error.code;
|
|
369
|
+
// EINVAL/ENOTSUP/EPERM/EBADF: the platform refuses to fsync a directory — see the
|
|
370
|
+
// doc comment. Anything else (EIO above all) is a genuine write failure.
|
|
371
|
+
if (code !== 'EINVAL' && code !== 'ENOTSUP' && code !== 'EPERM' && code !== 'EBADF') {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
closeSync(fd);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=store-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-dir.js","sourceRoot":"","sources":["../src/store-dir.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RawRetrieveCounters } from './stats.ts';
|
|
2
|
+
import type { ElisionStore, RetrieveStats } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The default store: in-process, content-addressed, no eviction.
|
|
5
|
+
*
|
|
6
|
+
* In-process is the right default because it is the only one with no failure modes to
|
|
7
|
+
* explain. A consumer that needs elisions to survive a process restart (a long-lived
|
|
8
|
+
* agent session, say) hands `createSmelter()` a {@link DirectoryElisionStore} — or
|
|
9
|
+
* implements {@link ElisionStore} over storage of its own; the interface is five
|
|
10
|
+
* methods wide for exactly that reason.
|
|
11
|
+
*
|
|
12
|
+
* There is no `clear()` and no LRU. A store that can forget turns Law 3 into
|
|
13
|
+
* "reversible, usually", and a `retrieve()` that fails after an eviction is
|
|
14
|
+
* indistinguishable to the model from a hallucinated hash.
|
|
15
|
+
*/
|
|
16
|
+
export interface MemoryElisionStoreOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Override the hash function. The only reason this exists: the collision branch in
|
|
19
|
+
* {@link MemoryElisionStore.put} is unreachable with sha256, and an untestable branch
|
|
20
|
+
* is a branch nobody knows works. A test injects a colliding hash and watches it
|
|
21
|
+
* throw. Production has no reason to pass this.
|
|
22
|
+
*/
|
|
23
|
+
readonly hash?: (content: string) => string;
|
|
24
|
+
}
|
|
25
|
+
export declare class MemoryElisionStore implements ElisionStore {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(options?: MemoryElisionStoreOptions);
|
|
28
|
+
put(content: string): string;
|
|
29
|
+
peek(hash: string): string | undefined;
|
|
30
|
+
retrieve(hash: string): string;
|
|
31
|
+
has(hash: string): boolean;
|
|
32
|
+
/** The five directly-observed counts. See {@link RawRetrieveCounters} — no derivation here. */
|
|
33
|
+
rawCounters(): RawRetrieveCounters;
|
|
34
|
+
stats(): RetrieveStats;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC7C;AAED,qBAAa,kBAAmB,YAAW,YAAY;;IAQrD,YAAY,OAAO,GAAE,yBAA8B,EAElD;IAED,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAU3B;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAErC;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS7B;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzB;IAED,+FAA+F;IAC/F,WAAW,IAAI,mBAAmB,CAQjC;IAED,KAAK,IAAI,aAAa,CAErB;CACF"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HashCollisionError, UnknownHashError } from './errors.js';
|
|
2
|
+
import { contentHash } from './hash.js';
|
|
3
|
+
import { retrieveStats } from './stats.js';
|
|
4
|
+
export class MemoryElisionStore {
|
|
5
|
+
#blobs = new Map();
|
|
6
|
+
#hash;
|
|
7
|
+
#bytesStored = 0;
|
|
8
|
+
#retrieveCalls = 0;
|
|
9
|
+
#misses = 0;
|
|
10
|
+
#retrievedHashes = new Set();
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.#hash = options.hash ?? contentHash;
|
|
13
|
+
}
|
|
14
|
+
put(content) {
|
|
15
|
+
const hash = this.#hash(content);
|
|
16
|
+
const existing = this.#blobs.get(hash);
|
|
17
|
+
if (existing !== undefined) {
|
|
18
|
+
if (existing !== content)
|
|
19
|
+
throw new HashCollisionError(hash);
|
|
20
|
+
return hash;
|
|
21
|
+
}
|
|
22
|
+
this.#blobs.set(hash, content);
|
|
23
|
+
this.#bytesStored += Buffer.byteLength(content, 'utf8');
|
|
24
|
+
return hash;
|
|
25
|
+
}
|
|
26
|
+
peek(hash) {
|
|
27
|
+
return this.#blobs.get(hash);
|
|
28
|
+
}
|
|
29
|
+
retrieve(hash) {
|
|
30
|
+
this.#retrieveCalls += 1;
|
|
31
|
+
const content = this.#blobs.get(hash);
|
|
32
|
+
if (content === undefined) {
|
|
33
|
+
this.#misses += 1;
|
|
34
|
+
throw new UnknownHashError(hash);
|
|
35
|
+
}
|
|
36
|
+
this.#retrievedHashes.add(hash);
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
has(hash) {
|
|
40
|
+
return this.#blobs.has(hash);
|
|
41
|
+
}
|
|
42
|
+
/** The five directly-observed counts. See {@link RawRetrieveCounters} — no derivation here. */
|
|
43
|
+
rawCounters() {
|
|
44
|
+
return {
|
|
45
|
+
elisionsStored: this.#blobs.size,
|
|
46
|
+
bytesStored: this.#bytesStored,
|
|
47
|
+
retrieveCalls: this.#retrieveCalls,
|
|
48
|
+
uniqueRetrieved: this.#retrievedHashes.size,
|
|
49
|
+
misses: this.#misses,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
stats() {
|
|
53
|
+
return retrieveStats(this.rawCounters());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA2B3C,MAAM,OAAO,kBAAkB;IACpB,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACnC,KAAK,CAA8B;IAC5C,YAAY,GAAG,CAAC,CAAC;IACjB,cAAc,GAAG,CAAC,CAAC;IACnB,OAAO,GAAG,CAAC,CAAC;IACH,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAE9C,YAAY,OAAO,GAA8B,EAAE;QACjD,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,OAAe;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,QAAQ,KAAK,OAAO;gBAAE,MAAM,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC;YAC7D,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/B,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,QAAQ,CAAC,IAAY;QACnB,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;YAClB,MAAM,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,+FAA+F;IAC/F,WAAW;QACT,OAAO;YACL,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;YAChC,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,aAAa,EAAE,IAAI,CAAC,cAAc;YAClC,eAAe,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI;YAC3C,MAAM,EAAE,IAAI,CAAC,OAAO;SACrB,CAAC;IACJ,CAAC;IAED,KAAK;QACH,OAAO,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3C,CAAC;CACF"}
|