@bejibun/storage 0.1.1 → 0.1.11

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 (40) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/benchmarks/README.md +46 -0
  3. package/benchmarks/package.json +12 -0
  4. package/benchmarks/scripts/coldstart-baseline.mjs +9 -0
  5. package/benchmarks/scripts/coldstart-optimized.mjs +9 -0
  6. package/benchmarks/scripts/coldstart.mjs +93 -0
  7. package/benchmarks/scripts/readme-writer.mjs +35 -0
  8. package/benchmarks/scripts/table-format.mjs +123 -0
  9. package/benchmarks/scripts/throughput-baseline.mjs +68 -0
  10. package/benchmarks/scripts/throughput-optimized.mjs +68 -0
  11. package/benchmarks/scripts/throughput.mjs +83 -0
  12. package/builders/StorageBuilder.d.ts +117 -0
  13. package/builders/StorageBuilder.js +156 -29
  14. package/builders/storage/StorageLocalBuilder.d.ts +89 -0
  15. package/builders/storage/StorageLocalBuilder.js +109 -21
  16. package/builders/storage/StorageS3Builder.d.ts +90 -0
  17. package/builders/storage/StorageS3Builder.js +107 -18
  18. package/config/storage.d.ts +3 -0
  19. package/config/storage.js +8 -0
  20. package/configure.js +5 -0
  21. package/enums/StorageDiskDriverEnum.d.ts +5 -0
  22. package/enums/StorageDiskDriverEnum.js +5 -0
  23. package/enums/index.d.ts +4 -1
  24. package/enums/index.js +4 -1
  25. package/exceptions/StorageException.d.ts +10 -0
  26. package/exceptions/StorageException.js +10 -0
  27. package/exceptions/index.d.ts +4 -1
  28. package/exceptions/index.js +4 -1
  29. package/facades/Storage.d.ts +87 -0
  30. package/facades/Storage.js +87 -0
  31. package/facades/index.d.ts +4 -1
  32. package/facades/index.js +4 -1
  33. package/index.d.ts +4 -0
  34. package/index.js +4 -0
  35. package/package.json +10 -9
  36. package/tests/integration/storage.integration.test.ts +145 -0
  37. package/tests/unit/storage.test.ts +177 -0
  38. package/tsconfig.json +2 -1
  39. package/types/index.d.ts +4 -1
  40. package/types/storage.d.ts +24 -24
package/CHANGELOG.md CHANGED
@@ -3,6 +3,51 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  ---
5
5
 
6
+ ## [v0.1.11](https://github.com/Bejibun-Framework/bejibun-storage/compare/v0.1.1...v0.1.11) - 2026-09-04
7
+
8
+ ### 🩹 Fixes
9
+ - Fixed `StorageBuilder.missing` which used a double negation (`!(await driver.missing(...))`) and therefore returned `exists` instead of the correct "missing" result
10
+
11
+ ### 📖 Changes
12
+ #### Performance
13
+ - `StorageBuilder` now loads and caches the storage config once via a lazy `loadConfig()` (module-level `cachedConfig`), instead of reading `storage.ts` from disk with `fs.existsSync` + `require()` on every `new StorageBuilder()`
14
+ - Replaced the `@bejibun/utils` `defineValue`/`isEmpty` calls with native nullish coalescing (`??`) and truthiness checks in `StorageBuilder`, `StorageLocalBuilder`, and `StorageS3Builder`
15
+ - `StorageLocalBuilder` caches the resolved `root` once (validated in the constructor) and resolves paths through a single helper instead of re-validating on every operation
16
+ - `StorageS3Builder` validates the required S3 fields once in the constructor and reuses the plain config, instead of re-running the validation getter on every access
17
+
18
+ #### Docs
19
+ - Added missing `@returns` annotations (with descriptions) to every `Storage` facade method, matching the cache and limiter facade style; aligned the `@param`/`@returns` JSDoc format with the sibling packages
20
+ - Re-exported the `StorageException` classes from the package root (`@/exceptions/index`) for consistency with the cache and limiter entry points
21
+ - Standardized `StorageBuilder` config handling to the same lazy `loadConfig()` pattern used by `CacheBuilder`
22
+
23
+ ### 🧪 Tests
24
+ - Added unit test suite (17 tests across 1 file in `tests/unit`) covering `StorageBuilder` input validation (empty path/content/source/destination rejections across `exists`, `missing`, `metadata`, `size`, `mimeType`, `lastModified`, `get`, `put`, `copy`, `move`, `delete`) and the `Storage` facade delegation, with silenced logger output
25
+ - Added an integration test suite (7 tests across 1 file in `tests/integration`) exercising the real local disk driver end to end against a temporary directory: `put`/`exists`/`missing`/`size`/`get`, `metadata`/`lastModified`, `copy`, `move`, and `delete`
26
+ - Added `test` (unit) and `test:integration` scripts and added `tests` to tsconfig `exclude` so compiled output never lands in `tests/`
27
+
28
+ ### ⚡ Benchmarks
29
+ - Added benchmark suite comparing baseline (`@bejibun/storage@0.1.1`) vs the optimized build, covering `construction`, `exists`, `get`, and `delete` hot paths against a temporary local disk; full results are written to `benchmarks/README.md` between the `BENCHMARK` markers
30
+ - Also added a cold-start suite spawning 30 fresh OS processes per variant and measuring full process time and import time
31
+ - Throughput results on the local disk backend: `construction` **26.64x** (40.0 vs 1.5ms, ~13.3M ops/s), `exists` 1.85x (117.2 vs 63.2ms), `get` 3.50x (74.7 vs 21.3ms), `delete` ~1.03x (3896.7 vs 3775.0ms, I/O bound); cold start ~1.02x
32
+
33
+ ### 📦 Dependencies
34
+
35
+ - Bumped [`@bejibun/app`](https://github.com/Bejibun-Framework/bejibun-app) from `^0.1.25` to `^0.1.26`
36
+ - Bumped [`@bejibun/logger`](https://github.com/Bejibun-Framework/bejibun-logger) from `^0.1.23` to `^0.2.1`
37
+ - Bumped [`@bejibun/utils`](https://github.com/Bejibun-Framework/bejibun-utils) from `^0.1.29` to `^0.1.30`
38
+ - Bumped `@types/bun` (devDependency) from `^1.3.14` to `^1.4.0`
39
+ - Bumped `eslint` (devDependency) from `^10.8.1` to `^10.9.1`
40
+ - Bumped `globals` (devDependency) from `^17.11.0` to `^17.12.0`
41
+ - Bumped `tsc-alias` (devDependency) from `^1.9.2` to `^1.9.4`
42
+ - Bumped `typescript-eslint` (devDependency) from `^8.67.0` to `^8.69.0`
43
+
44
+ ### ❤️Contributors
45
+ - Havea Crenata ([@crenata](https://github.com/crenata))
46
+
47
+ **Full Changelog**: https://github.com/Bejibun-Framework/bejibun-storage/blob/master/CHANGELOG.md
48
+
49
+ ---
50
+
6
51
  ## [v0.1.1](https://github.com/Bejibun-Framework/bejibun-storage/compare/v0.1.0...v0.1.1) - 2026-08-20
7
52
 
8
53
  ### 🩹 Fixes
@@ -0,0 +1,46 @@
1
+ # Benchmarks
2
+
3
+ Speed comparison: baseline (previously published npm release `@bejibun/storage@0.1.1`) vs the optimized `@bejibun/storage` in this repo.
4
+
5
+ ## Running
6
+
7
+ ```bash
8
+ # Run all benchmarks (installs baseline from npm first)
9
+ bun run bench
10
+
11
+ # Or run individually (after install-deps)
12
+ bun run install-deps
13
+ bun run coldstart
14
+ bun run throughput
15
+ ```
16
+
17
+ ## Cold Start
18
+
19
+ Measures package import time by spawning fresh OS processes. Two metrics:
20
+
21
+ - **Full process time** — spawn → exit (includes Bun boot time)
22
+ - **Import** — measured inside the process, isolates the package's own import cost
23
+
24
+ <!-- BENCHMARK:COLDSTART:START -->
25
+
26
+ | | baseline | optimized | speedup |
27
+ | --------------------------- | -------- | --------- | --------- |
28
+ | Full process (spawn → exit) | 23.1ms | 21.5ms | **1.07x** |
29
+ | Import | 15.3ms | 14.1ms | **1.09x** |
30
+
31
+ <!-- BENCHMARK:COLDSTART:END -->
32
+
33
+ ## Throughput
34
+
35
+ The hot paths touched on every storage operation. `construction` covers `new StorageBuilder()` plus config resolution (no disk I/O). `exists`, `get`, and `delete` run the full facade against a temporary local disk. 20,000 calls each, median of 9 runs.
36
+
37
+ <!-- BENCHMARK:THROUGHPUT:START -->
38
+
39
+ | Method | baseline (0.1.1) | optimized | speedup | baseline ops/s | optimized ops/s |
40
+ | -------------- | ---------------- | --------- | ---------- | -------------- | --------------- |
41
+ | `construction` | 35.0ms | 1.5ms | **22.90x** | 571,510/s | 13,086,855/s |
42
+ | `exists` | 88.7ms | 52.9ms | **1.68x** | 225,468/s | 377,841/s |
43
+ | `get` | 54.4ms | 15.3ms | **3.54x** | 367,796/s | 1,303,802/s |
44
+ | `delete` | 3320.7ms | 3226.3ms | **1.03x** | 6,023/s | 6,199/s |
45
+
46
+ <!-- BENCHMARK:THROUGHPUT:END -->
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@bejibun/storage-benchmarks",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Speed comparison: baseline (previously published npm release) vs the optimized @bejibun/storage in this repo. Run with `bun run`.",
6
+ "scripts": {
7
+ "install-deps": "bun install --cwd .. --no-save \"@bejibun-baseline/storage@npm:@bejibun/storage@0.1.1\"",
8
+ "coldstart": "bun run scripts/coldstart.mjs && prettier --write . --log-level=silent",
9
+ "throughput": "bun run scripts/throughput.mjs && prettier --write . --log-level=silent",
10
+ "bench": "bun run install-deps && bun run coldstart && bun run throughput"
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ const realLog = console.log;
2
+ console.log = () => {};
3
+
4
+ const t0 = performance.now();
5
+ await import("@bejibun-baseline/storage");
6
+ const t1 = performance.now();
7
+
8
+ console.log = realLog;
9
+ process.stderr.write(String(t1 - t0));
@@ -0,0 +1,9 @@
1
+ const realLog = console.log;
2
+ console.log = () => {};
3
+
4
+ const t0 = performance.now();
5
+ await import("../../index.js");
6
+ const t1 = performance.now();
7
+
8
+ console.log = realLog;
9
+ process.stderr.write(String(t1 - t0));
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cold-start benchmark.
3
+ *
4
+ * Spawns a brand new OS process per trial for each variant (baseline vs optimized) and
5
+ * measures the full process time (spawn -> exit) and the import time of the package.
6
+ *
7
+ * Run: bun run scripts/coldstart.mjs
8
+ */
9
+ import {spawnSync} from "node:child_process";
10
+ import {fileURLToPath} from "node:url";
11
+ import path from "node:path";
12
+ import {printTable} from "./table-format.mjs";
13
+ import {updateReadmeSection} from "./readme-writer.mjs";
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const TRIALS = 30;
17
+ const runtime = process.execPath;
18
+
19
+ function runTrials(scriptPath) {
20
+ const times = [];
21
+ for (let i = 0; i < TRIALS; i++) {
22
+ const t0 = performance.now();
23
+ const res = spawnSync(runtime, [scriptPath], {encoding: "utf8"});
24
+ const wallTime = performance.now() - t0;
25
+ if (res.status !== 0) {
26
+ console.error("Benchmark failed:", res.stderr);
27
+ process.exit(1);
28
+ }
29
+ const internalTime = parseFloat(res.stderr.trim());
30
+ times.push({wallTime, internalTime});
31
+ }
32
+ return times;
33
+ }
34
+
35
+ function stats(arr) {
36
+ const sorted = [...arr].sort((a, b) => a - b);
37
+ const sum = arr.reduce((a, b) => a + b, 0);
38
+ return {
39
+ min: sorted[0],
40
+ median: sorted[Math.floor(sorted.length / 2)],
41
+ mean: sum / arr.length
42
+ };
43
+ }
44
+
45
+ const baseline = runTrials(path.join(__dirname, "coldstart-baseline.mjs"));
46
+ const optimized = runTrials(path.join(__dirname, "coldstart-optimized.mjs"));
47
+
48
+ const baseWall = stats(baseline.map((t) => t.wallTime));
49
+ const optWall = stats(optimized.map((t) => t.wallTime));
50
+ const baseInt = stats(baseline.map((t) => t.internalTime));
51
+ const optInt = stats(optimized.map((t) => t.internalTime));
52
+
53
+ function fmt(ms) {
54
+ return ms < 1 ? `${(ms * 1000).toFixed(0)}\u00B5s` : `${ms.toFixed(1)}ms`;
55
+ }
56
+
57
+ function sp(b, o) {
58
+ const r = b / o;
59
+ return r >= 1.05 ? `${r.toFixed(2)}x` : r <= 0.95 ? `${r.toFixed(2)}x` : "~1.0x";
60
+ }
61
+
62
+ printTable({
63
+ title: "COLD START BENCHMARK",
64
+ subtitle: `${TRIALS} fresh process spawns per variant`,
65
+ headers: ["Metric", "Baseline (0.1.1)", "Optimized", "Speedup"],
66
+ rows: [
67
+ {
68
+ cells: [
69
+ "Full process (spawn \u2192 exit)",
70
+ fmt(baseWall.median),
71
+ fmt(optWall.median),
72
+ sp(baseWall.median, optWall.median)
73
+ ]
74
+ },
75
+ {
76
+ cells: [
77
+ "Import",
78
+ fmt(baseInt.median),
79
+ fmt(optInt.median),
80
+ sp(baseInt.median, optInt.median)
81
+ ]
82
+ }
83
+ ]
84
+ });
85
+
86
+ const table = [
87
+ "| | baseline | optimized | speedup |",
88
+ "|---|---|---|---|",
89
+ `| Full process (spawn → exit) | ${baseWall.median.toFixed(1)}ms | ${optWall.median.toFixed(1)}ms | **${(baseWall.median / optWall.median).toFixed(2)}x** |`,
90
+ `| Import | ${baseInt.median.toFixed(1)}ms | ${optInt.median.toFixed(1)}ms | **${(baseInt.median / optInt.median).toFixed(2)}x** |`
91
+ ].join("\n");
92
+
93
+ updateReadmeSection("COLDSTART", table);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Writes benchmark output directly into ../README.md between a pair of HTML comment
3
+ * markers, so results never have to be hand-copied. Everything outside the markers
4
+ * (headings, explanatory prose) is left untouched.
5
+ *
6
+ * <!-- BENCHMARK:NAME:START -->
7
+ * ...replaced on every run...
8
+ * <!-- BENCHMARK:NAME:END -->
9
+ */
10
+ import {readFileSync, writeFileSync} from "node:fs";
11
+ import {fileURLToPath} from "node:url";
12
+ import path from "node:path";
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const README_PATH = path.join(__dirname, "..", "README.md");
16
+
17
+ export function updateReadmeSection(marker, markdown) {
18
+ const start = `<!-- BENCHMARK:${marker}:START -->`;
19
+ const end = `<!-- BENCHMARK:${marker}:END -->`;
20
+
21
+ const readme = readFileSync(README_PATH, "utf8");
22
+ const startIdx = readme.indexOf(start);
23
+ const endIdx = readme.indexOf(end);
24
+
25
+ if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
26
+ console.warn(
27
+ `README markers for "${marker}" not found in ${README_PATH} -- skipping README update.`
28
+ );
29
+ return;
30
+ }
31
+
32
+ const before = readme.slice(0, startIdx + start.length);
33
+ const after = readme.slice(endIdx);
34
+ writeFileSync(README_PATH, `${before}\n${markdown}\n${after}`);
35
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Simple bordered console table for benchmark output.
3
+ *
4
+ * Uses box-drawing characters for a clean, readable look.
5
+ * Supports optional section headers between rows.
6
+ */
7
+
8
+ const C = {
9
+ reset: "\x1b[0m",
10
+ bold: "\x1b[1m",
11
+ dim: "\x1b[2m",
12
+ green: "\x1b[32m",
13
+ yellow: "\x1b[33m",
14
+ cyan: "\x1b[36m",
15
+ white: "\x1b[37m"
16
+ };
17
+
18
+ const B = {
19
+ TL: "\u250C",
20
+ TR: "\u2510",
21
+ BL: "\u2514",
22
+ BR: "\u2518",
23
+ H: "\u2500",
24
+ V: "\u2502",
25
+ TD: "\u252C",
26
+ TU: "\u2534",
27
+ TR2: "\u251C",
28
+ TL2: "\u2524",
29
+ X: "\u253C"
30
+ };
31
+
32
+ function line(widths, left, mid, right) {
33
+ return left + widths.map((w) => B.H.repeat(w + 2)).join(mid) + right;
34
+ }
35
+
36
+ function p(str, n, align) {
37
+ str = String(str);
38
+ return align === "left" ? str.padEnd(n) : str.padStart(n);
39
+ }
40
+
41
+ function displayValue(val, isSpeedup) {
42
+ if (val === "---") return {text: val, color: C.dim};
43
+ if (val === "new") return {text: val, color: C.cyan};
44
+ if (isSpeedup) {
45
+ const num = parseFloat(val);
46
+ if (num >= 1.2) return {text: val, color: C.bold + C.green};
47
+ if (num >= 1.05) return {text: val, color: C.green};
48
+ if (num < 1.0) return {text: val, color: C.yellow};
49
+ }
50
+ return {text: val, color: null};
51
+ }
52
+
53
+ /**
54
+ * Print a bordered table to the console.
55
+ *
56
+ * @param {object} opts
57
+ * @param {string} opts.title
58
+ * @param {string} opts.subtitle
59
+ * @param {string[]} opts.headers
60
+ * @param {Array<{cells: string[], group?: string}>} opts.rows
61
+ */
62
+ export function printTable({title, subtitle, headers, rows}) {
63
+ const widths = headers.map((h) => h.length);
64
+
65
+ // Calculate column widths
66
+ for (const r of rows) {
67
+ if (!r.cells) continue;
68
+ r.cells.forEach((c, i) => {
69
+ widths[i] = Math.max(widths[i], String(c).length);
70
+ });
71
+ }
72
+
73
+ const totalWidth = widths.reduce((a, w) => a + w + 2, 0) + widths.length - 1 + 2;
74
+ const lineInner = totalWidth - 4;
75
+
76
+ const hr = line(widths, B.TL, B.TD, B.TR);
77
+ const hrSep = line(widths, B.TR2, B.X, B.TL2);
78
+ const hrBot = line(widths, B.BL, B.TU, B.BR);
79
+
80
+ console.log();
81
+ console.log(hr);
82
+ console.log(`${B.V} ${C.bold + C.white}${title.padEnd(lineInner)}${C.reset} ${B.V}`);
83
+ if (subtitle) {
84
+ console.log(`${B.V} ${C.dim}${subtitle.padEnd(lineInner)}${C.reset} ${B.V}`);
85
+ }
86
+ console.log(hrSep);
87
+
88
+ // Header
89
+ const hdr = headers
90
+ .map((h, i) => {
91
+ const pad = i === 0 ? p(h, widths[i], "left") : p(h, widths[i], "right");
92
+ return ` ${C.bold + C.cyan}${pad}${C.reset} `;
93
+ })
94
+ .join(`${B.V}`);
95
+ console.log(`${B.V}${hdr}${B.V}`);
96
+ console.log(hrSep);
97
+
98
+ // Rows
99
+ let lastGroup = null;
100
+ for (const r of rows) {
101
+ if (r.group !== undefined && r.group !== lastGroup) {
102
+ if (lastGroup !== null) console.log(hrSep);
103
+ console.log(`${B.V} ${C.bold + C.white}${r.group.padEnd(lineInner)}${C.reset} ${B.V}`);
104
+ console.log(hrSep);
105
+ lastGroup = r.group;
106
+ }
107
+
108
+ if (!r.cells) continue;
109
+ const cells = r.cells
110
+ .map((c, i) => {
111
+ const isSpeedup = i === 3;
112
+ const d = displayValue(c, isSpeedup);
113
+ const align = i === 0 ? "left" : "right";
114
+ const padStr = p(d.text, widths[i], align);
115
+ return d.color ? ` ${d.color}${padStr}${C.reset} ` : ` ${padStr} `;
116
+ })
117
+ .join(`${B.V}`);
118
+ console.log(`${B.V}${cells}${B.V}`);
119
+ }
120
+
121
+ console.log(hrBot);
122
+ console.log();
123
+ }
@@ -0,0 +1,68 @@
1
+ console.log = () => {};
2
+ console.error = () => {};
3
+
4
+ const {default: Storage} = await import("@bejibun-baseline/storage");
5
+ const {default: StorageBuilder} = await import("@bejibun-baseline/storage/builders/StorageBuilder");
6
+
7
+ const ITERATIONS = 20_000;
8
+ const WARMUP = 500;
9
+ const root = `${process.env.TMPDIR ?? "/tmp"}/bejibun-storage-bench-baseline`;
10
+ const disk = {driver: "local", root};
11
+
12
+ await Bun.write(`${root}/.keep`, "");
13
+
14
+ const buildMs = measureConstructor();
15
+ const existsMs = await measureExists();
16
+ const getMs = await measureGet();
17
+ const deleteMs = await measureDelete();
18
+
19
+ process.stdout.write(`${buildMs}|${existsMs}|${getMs}|${deleteMs}\n`);
20
+
21
+ function measureConstructor() {
22
+ for (let i = 0; i < WARMUP; i++) {
23
+ void new StorageBuilder();
24
+ }
25
+ const t0 = performance.now();
26
+ for (let i = 0; i < ITERATIONS; i++) {
27
+ void new StorageBuilder();
28
+ }
29
+ return performance.now() - t0;
30
+ }
31
+
32
+ async function measureExists() {
33
+ for (let i = 0; i < WARMUP; i++) {
34
+ await Storage.build(disk).exists(`${root}/bench:key-old`);
35
+ }
36
+ const t0 = performance.now();
37
+ for (let i = 0; i < ITERATIONS; i++) {
38
+ await Storage.build(disk).exists(`${root}/bench:key:${i % 1000}`);
39
+ }
40
+ return performance.now() - t0;
41
+ }
42
+
43
+ async function measureGet() {
44
+ await Storage.build(disk).put(`${root}/bench:get`, "bench-value");
45
+ for (let i = 0; i < WARMUP; i++) {
46
+ await Storage.build(disk).get(`${root}/bench:get`);
47
+ }
48
+ const t0 = performance.now();
49
+ for (let i = 0; i < ITERATIONS; i++) {
50
+ await Storage.build(disk).get(`${root}/bench:get`);
51
+ }
52
+ return performance.now() - t0;
53
+ }
54
+
55
+ async function measureDelete() {
56
+ for (let i = 0; i < WARMUP; i++) {
57
+ const f = `${root}/bench:del:${i}`;
58
+ await Bun.write(f, "x");
59
+ await Storage.build(disk).delete(f);
60
+ }
61
+ const t0 = performance.now();
62
+ for (let i = 0; i < ITERATIONS; i++) {
63
+ const f = `${root}/bench:del:${i % 200}`;
64
+ await Bun.write(f, "x");
65
+ await Storage.build(disk).delete(f);
66
+ }
67
+ return performance.now() - t0;
68
+ }
@@ -0,0 +1,68 @@
1
+ console.log = () => {};
2
+ console.error = () => {};
3
+
4
+ const {default: Storage} = await import("../../src/facades/Storage.ts");
5
+ const {default: StorageBuilder} = await import("../../src/builders/StorageBuilder.ts");
6
+
7
+ const ITERATIONS = 20_000;
8
+ const WARMUP = 500;
9
+ const root = `${process.env.TMPDIR ?? "/tmp"}/bejibun-storage-bench-optimized`;
10
+ const disk = {driver: "local", root};
11
+
12
+ await Bun.write(`${root}/.keep`, "");
13
+
14
+ const buildMs = measureConstructor();
15
+ const existsMs = await measureExists();
16
+ const getMs = await measureGet();
17
+ const deleteMs = await measureDelete();
18
+
19
+ process.stdout.write(`${buildMs}|${existsMs}|${getMs}|${deleteMs}\n`);
20
+
21
+ function measureConstructor() {
22
+ for (let i = 0; i < WARMUP; i++) {
23
+ void new StorageBuilder();
24
+ }
25
+ const t0 = performance.now();
26
+ for (let i = 0; i < ITERATIONS; i++) {
27
+ void new StorageBuilder();
28
+ }
29
+ return performance.now() - t0;
30
+ }
31
+
32
+ async function measureExists() {
33
+ for (let i = 0; i < WARMUP; i++) {
34
+ await Storage.build(disk).exists(`${root}/bench:key-old`);
35
+ }
36
+ const t0 = performance.now();
37
+ for (let i = 0; i < ITERATIONS; i++) {
38
+ await Storage.build(disk).exists(`${root}/bench:key:${i % 1000}`);
39
+ }
40
+ return performance.now() - t0;
41
+ }
42
+
43
+ async function measureGet() {
44
+ await Storage.build(disk).put(`${root}/bench:get`, "bench-value");
45
+ for (let i = 0; i < WARMUP; i++) {
46
+ await Storage.build(disk).get(`${root}/bench:get`);
47
+ }
48
+ const t0 = performance.now();
49
+ for (let i = 0; i < ITERATIONS; i++) {
50
+ await Storage.build(disk).get(`${root}/bench:get`);
51
+ }
52
+ return performance.now() - t0;
53
+ }
54
+
55
+ async function measureDelete() {
56
+ for (let i = 0; i < WARMUP; i++) {
57
+ const f = `${root}/bench:del:${i}`;
58
+ await Bun.write(f, "x");
59
+ await Storage.build(disk).delete(f);
60
+ }
61
+ const t0 = performance.now();
62
+ for (let i = 0; i < ITERATIONS; i++) {
63
+ const f = `${root}/bench:del:${i % 200}`;
64
+ await Bun.write(f, "x");
65
+ await Storage.build(disk).delete(f);
66
+ }
67
+ return performance.now() - t0;
68
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Throughput benchmark.
3
+ *
4
+ * Measures the hot paths touched on every storage operation, method by method. On baseline
5
+ * each `new StorageBuilder()` re-reads the config file from disk (`fs.existsSync` +
6
+ * `require()`) and re-resolves the driver; the optimized build caches the resolved config
7
+ * at module load and uses native nullish checks instead of `defineValue`/`isEmpty`.
8
+ * `exists`/`get`/`delete` exercise the full facade path against a temporary local disk.
9
+ *
10
+ * Run: bun run scripts/throughput.mjs
11
+ */
12
+ import {spawnSync} from "node:child_process";
13
+ import {fileURLToPath} from "node:url";
14
+ import path from "node:path";
15
+ import {printTable} from "./table-format.mjs";
16
+ import {updateReadmeSection} from "./readme-writer.mjs";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const TRIALS = 9;
20
+ const runtime = process.execPath;
21
+
22
+ function runTrials(scriptPath) {
23
+ const results = [];
24
+ for (let i = 0; i < TRIALS; i++) {
25
+ const res = spawnSync(runtime, [scriptPath], {encoding: "utf8"});
26
+ if (res.status !== 0) {
27
+ console.error("Benchmark failed:", res.stderr);
28
+ process.exit(1);
29
+ }
30
+ results.push(res.stdout.trim().split("|").map(Number));
31
+ }
32
+ return results;
33
+ }
34
+
35
+ function medianRow(trials) {
36
+ const cols = trials[0].length;
37
+ const medians = [];
38
+ for (let c = 0; c < cols; c++) {
39
+ const sorted = trials.map((t) => t[c]).sort((a, b) => a - b);
40
+ medians.push(sorted[Math.floor(sorted.length / 2)]);
41
+ }
42
+ return medians;
43
+ }
44
+
45
+ const ITERATIONS = 20_000;
46
+ const methods = ["construction", "exists", "get", "delete"];
47
+ const bCols = medianRow(runTrials(path.join(__dirname, "throughput-baseline.mjs")));
48
+ const oCols = medianRow(runTrials(path.join(__dirname, "throughput-optimized.mjs")));
49
+
50
+ function fmt(ms) {
51
+ return ms < 1 ? `${(ms * 1000).toFixed(0)}\u00B5s` : `${ms.toFixed(1)}ms`;
52
+ }
53
+
54
+ function sp(b, o) {
55
+ const r = b / o;
56
+ return r >= 1.05 ? `${r.toFixed(2)}x` : r <= 0.95 ? `${r.toFixed(2)}x` : "~1.0x";
57
+ }
58
+
59
+ function ops(ms) {
60
+ return Math.round(ITERATIONS / (ms / 1000)).toLocaleString() + "/s";
61
+ }
62
+
63
+ const rows = methods.map((m, i) => ({
64
+ cells: [m, fmt(bCols[i]), fmt(oCols[i]), sp(bCols[i], oCols[i]), ops(oCols[i])]
65
+ }));
66
+
67
+ printTable({
68
+ title: "THROUGHPUT BENCHMARK",
69
+ subtitle: `${ITERATIONS.toLocaleString()} calls each, ${TRIALS} runs (median)`,
70
+ headers: ["Method", "Baseline (0.1.1)", "Optimized", "Speedup", "Optimized ops/s"],
71
+ rows
72
+ });
73
+
74
+ const lines = [
75
+ "| Method | baseline (0.1.1) | optimized | speedup | baseline ops/s | optimized ops/s |",
76
+ "|---|---|---|---|---|---|",
77
+ ...methods.map(
78
+ (m, i) =>
79
+ `| \`${m}\` | ${bCols[i].toFixed(1)}ms | ${oCols[i].toFixed(1)}ms | **${(bCols[i] / oCols[i]).toFixed(2)}x** | ${ops(bCols[i])} | ${ops(oCols[i])} |`
80
+ )
81
+ ];
82
+
83
+ updateReadmeSection("THROUGHPUT", lines.join("\n"));