@smoothbricks/nx-cache-server 0.0.0-bootstrap.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # @smoothbricks/nx-cache-server
1
+ # Nx cache server
2
2
 
3
- This is a bootstrap placeholder. Real package versions are published by SmoothBricks release automation.
3
+ A Bun implementation of Nx's self-hosted `GET`/`PUT /v1/cache/{hash}` protocol. Archives are opaque binary bytes in Nx's
4
+ native format; this package does not unpack them, modify Nx, introduce blob CAS, or share workspace databases. The
5
+ governing host contract is `specs/cowshed/03_caches.md` §Nx accepted-task artifact service. Gateway authorization and
6
+ exact-head/task-hash landing eligibility belong to `05_gateway.md` and `16_build_volumes.md`.
7
+
8
+ ## Start and supervise
9
+
10
+ `nx-cache-server` runs the published `dist/ts/main.js` with Bun. The package ships no standalone executables (tg#7504):
11
+ its `bundle` target, part of the release `build`, writes one platform-independent file,
12
+ `dist/server/nx-cache-server.js`, which runs as `bun dist/server/nx-cache-server.js` with the `bun` found on `PATH`.
13
+ Cowshed's setup installs a content-addressed copy of that bundle, and its gateway runs the copy with the `bun` on the
14
+ `PATH` that setup recorded, so supervision uses the installed artifact, not a workspace source path. One process owns
15
+ one project store. Each project must have its own parent directory: `<project>/entries` holds archives and
16
+ `<project>/sieve.hand` holds the SIEVE cursor. No other process may open that store or sweep it concurrently.
17
+
18
+ Invocation is `nx-cache-server [--exit-on-stdin-eof]` (or `bun dist/server/nx-cache-server.js [--exit-on-stdin-eof]`);
19
+ the optional flag enables the parent-held stdin pipe lease described below.
20
+
21
+ | Environment | Meaning |
22
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `NX_CACHE_SERVER_DIR` | Required absolute project entries directory. |
24
+ | `NX_CACHE_SERVER_HOST` | Bind address; default `127.0.0.1`. |
25
+ | `NX_CACHE_SERVER_PORT` | Port; default `8765`; `0` selects a free port. |
26
+ | `NX_CACHE_SERVER_MAX_BYTES` | Maximum one upload; default 1 GiB. |
27
+ | `NX_CACHE_SERVER_CAP_BYTES` | Published-archive cap and independent staging-reservation budget, each a separate logical cap (not one combined physical disk cap); default 10 GiB each; must be at least `MAX_BYTES`. |
28
+ | `NX_CACHE_SERVER_WRITE_TOKENS` | Comma-separated `label:secret` writer principals. |
29
+ | `NX_CACHE_SERVER_READ_TOKENS` | Comma-separated `label:secret` read-only principals. |
30
+
31
+ At least one principal is required. Labels are unique lowercase/digit/dash names of at most 32 characters; secrets must
32
+ be nonempty and unique. Configuration and backend tokens are host-owned, never committed, and never passed to sandbox
33
+ clients. Cowshed clients receive only gateway authority, not these backend credentials. Ordinary iteration reads
34
+ accepted project results. Only accepted landing transactions publish successful current hashes, including local hits
35
+ without rerunning their tasks. There is no WIP/scratch store.
36
+
37
+ stdout contains one JSON startup record:
38
+
39
+ - Ready: `{ "kind": "ready", "origin": "http://127.0.0.1:<port>", "port": <port>, "reclaimedStagingFiles": <count> }`.
40
+ - Configuration failure: `{ "kind": "failed", "stage": "config", "problems": [...] }`, exit 2.
41
+ - Operational failure:
42
+ `{ "kind": "failed", "ok": false, "stage": "store" | "gc" | "listen", "directory": "...", "reason": "..." }`, exit 1.
43
+ - Store-locked refusal:
44
+ `{ "kind": "failed", "ok": false, "stage": "store-locked", "directory": "...", "path": "<directory>/.owner.lock", "reason": "..." }`,
45
+ exit 1. This is a distinct typed refusal; the generic `store`, `gc`, and `listen` failures keep their shape without
46
+ `path`, and configuration failure remains `{ "kind": "failed", "stage": "config", "problems": [...] }` with no `ok`.
47
+ Gateways act on `stage` and `path`, never on `reason` text.
48
+
49
+ stderr carries logfmt events, byte counts, timings, principal labels and refusal reasons. It never prints a bearer
50
+ secret. The supervisor captures those events; the package writes no permanent text log. SIGTERM/SIGINT drains requests
51
+ before exiting. SIGKILL can leave staged bytes, which the next startup reclaims and reports, never a partial hit.
52
+
53
+ With `--exit-on-stdin-eof`, the parent holds the write end of the process's stdin pipe for the child's lifetime. When
54
+ the parent dies, the kernel closes the pipe; EOF drains requests and the process exits. Without the flag, the process
55
+ keeps the SIGTERM/SIGINT behavior described above. The flag is argv-only and is not inherited through an environment
56
+ mode.
57
+
58
+ Each entries directory has a persistent `.owner.lock`, which is never unlinked. Before any staging reclamation, the
59
+ backend takes a nonblocking exclusive kernel `flock` (`LOCK_EX | LOCK_NB`) through a minimal Bun FFI binding to libc
60
+ (`libc.dylib` on Darwin, `libc.so.6` on Linux; see https://bun.com/docs/runtime/ffi) and holds it through drain and
61
+ stop. Hard death, including SIGKILL, releases it in the kernel. A busy lock fails startup as store-locked before
62
+ listening. Any other errno, or unavailable FFI, fails startup loudly with no fallback. Normal and supervised modes use
63
+ the same lease.
64
+
65
+ A busy lock is the only `store-locked` refusal. The root lease is the sole store-exclusion mechanism; EOF changes
66
+ lifecycle only.
67
+
68
+ ## Library API and retention
69
+
70
+ The root export provides `cacheServerConfigFromEnv`, `CacheServerConfig`, `startCacheServer`, `RunningCacheServer`,
71
+ `openCacheStore`, `stageEntry`, `publishEntry`, `readEntry`, `directoryStore`, `sweep`, and `sweepDirectory`.
72
+ `startCacheServer(config)` returns `{ ok: true, server }` or `{ ok: false, stage, directory, reason }`; a `store-locked`
73
+ failure also carries `path`. A running server provides `origin`, `port`, `reclaimedStagingFiles`, and asynchronous
74
+ `stop()`. `stageEntry` returns `ready` (a verified `CompleteUpload`), `incomplete` (received bytes), or `failed`
75
+ (reason). `publishEntry(store, upload)` atomically publishes that complete value and returns `stored`, `exists`, or
76
+ `failed`. Terminal outcomes carry `stagingRetained`: admission accounting may be released only after cleanup removed the
77
+ file. `readEntry` returns a pinned inode stream and byte count, `missing`, or `failed`; unlinking an archive cannot
78
+ truncate an already-opened GET. `sweepDirectory(store, capBytes, protectedKey?)` takes an owned `CacheStore`, not a bare
79
+ path, and returns `{ ok: true, outcome }` or `{ ok: false, reason }`. The standalone
80
+ `bun src/gc.ts <entries-dir> <positive-cap-bytes>` CLI acquires the store lease and passes its store; callers must not
81
+ run it beside a live server.
82
+
83
+ `openCacheStore(directory)` returns `{ ok: true, store }` or `{ ok: false, kind: 'store-locked', path }`. A successful
84
+ store has an async `close()`, and callers must close it after all use. Other low-level IO or errno errors propagate to
85
+ `startCacheServer`'s typed failure. The standalone `gc` CLI takes the same store lease; embedded server sweeps use the
86
+ already-held lease, and nothing sweeps the store concurrently.
87
+
88
+ Parallel streams produce complete staged values. One serialized publication section hard-links each value without
89
+ replacement and sweeps SIEVE before successful completion; no publication interleaves another commit's sweep. Startup
90
+ also sweeps. Each completed successful commit enforces the published archive-length cap before replying 200. SIEVE
91
+ orders by mtime, reads `atime > mtime` as the visited bit, and clears it by restoring atime to mtime. A real APFS GET
92
+ left atime unchanged, so GET explicitly marks the pinned inode when clear, retaining the modification-time cursor.
93
+
94
+ Authenticated writers reserve their declared upload lengths before streaming from an independent staging budget, also
95
+ `capBytes`. Full staging admission returns 403; there is no admission queue or retry service. Reservations cover
96
+ streaming and completed uploads waiting to commit and release after cleanup. A cleanup failure retains its reservation
97
+ and logs the path/reason until restart reclaims it; startup refuses if reclamation fails. Published archives and staging
98
+ each have their own logical cap, not one combined disk cap. Actual allocation and metadata are measured separately.
99
+ GET/PUT transfers and client archive extraction copy bytes; this is not zero-copy.
100
+
101
+ ## Protocol and publication safety
102
+
103
+ | Operation | Status |
104
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
105
+ | GET complete archive | 200, `application/octet-stream`, standard chunked complete binary body (pinned stream uses chunked HTTP framing; archive bytes unchanged) |
106
+ | GET missing/evicted archive | 404 |
107
+ | PUT complete new archive | 200 |
108
+ | PUT existing key | 409, original bytes untouched |
109
+ | PUT read-only principal or over upload limit | 403 |
110
+ | Missing/unknown bearer | 401, exactly `text/plain`, `WWW-Authenticate: Bearer` |
111
+ | Missing declared length | 411 |
112
+ | PUT invalid key/length or complete but short body | 400 |
113
+ | GET invalid key | 404 |
114
+ | Stream/write, link, read, or retention failure | 500, reason logged |
115
+ | Cleanup failure after complete publication | 200, whole archive retained; staging reservation held and fault logged |
116
+
117
+ The native Nx client and the current official Nx self-hosted caching OpenAPI
118
+ (https://nx.dev/docs/kb/self-hosted-caching) both use PUT 200, with 409 for existing keys and 403 for read-only or
119
+ refused admission. Server-side 403 prevents publication; it does not disable client-side tar/gzip creation or attempted
120
+ PUT. Do not claim zero overhead or no PUT for read-only iteration without a separately measured supported client
121
+ mechanism.
122
+
123
+ Staging, declared-length verification, and atomic `link(2)` publication preserve the existing never-replace design.
124
+ Concurrent same-key uploads have one winner; different keys are independent. Eviction makes future retrieval a miss.
125
+ Archives are not validated by extraction here: exact successful-task eligibility is the trusted gateway's
126
+ responsibility.
127
+
128
+ ## Release and consumer cutover
129
+
130
+ The package is public via `npm:public`; conventional commits drive the normal independent Nx release. Source version
131
+ metadata is not proof of an available release. Consumer migration follows the single contract in
132
+ [03_caches.md, “Nx accepted-task artifact service”](../../specs/cowshed/03_caches.md#nx-accepted-task-artifact-service).