Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
942495d
osfile: direct-read fast path for large reads, bypassing the page cache
OBrezhniev Jun 21, 2026
4639ab7
osfile: direct-write fast path for large writes, bypassing the page c…
OBrezhniev Jun 21, 2026
b37e440
fastfile: read open flags from fs.constants instead of importing node:fs
OBrezhniev Jun 21, 2026
e5ed85f
Merge remote-tracking branch 'origin/fix/rollup_build' into fix/rollu…
OBrezhniev Jun 22, 2026
572d2b8
Merge branch 'master' into feature/direct_rw_optimization
OBrezhniev Jun 22, 2026
b5852e0
Rebuild
OBrezhniev Jun 22, 2026
fd168d5
osfile: make direct-io page guards O(cached pages), not O(range)
OBrezhniev Jun 22, 2026
00b2bd8
osfile: gate direct-io on ArrayBuffer.isView (fix BigBuffer corruptio…
OBrezhniev Jun 22, 2026
661dc0e
compat: detect Node at runtime instead of process.browser
OBrezhniev Jun 23, 2026
e432ad2
compat (Tier 1): browser field stubs fs/constants so bundlers need no…
OBrezhniev Jun 23, 2026
86a85b4
Fix dev-dependency audit findings
OBrezhniev Jul 4, 2026
4ff3010
fix: correct destination offset when a cached-page read hits EOF
OBrezhniev Jul 8, 2026
f0b3783
fix: propagate page-cache IO errors instead of hanging or deferring t…
OBrezhniev Jul 9, 2026
ffc97b3
feat: stream reads over HTTP Range requests and from Blobs
OBrezhniev Jul 12, 2026
3322488
fix: cap page size in http/blob backends against disk-tuned hints
OBrezhniev Jul 12, 2026
6278879
fix: make close() idempotent, matching fs.promises.FileHandle semantics
OBrezhniev Aug 25, 2026
547eb27
test: cover the full API surface (99.5% lines, 100% functions); fix t…
OBrezhniev Aug 25, 2026
dba4d82
fix: degrade to full buffering when the origin stops honoring Range
OBrezhniev Aug 26, 2026
e7eb0f0
Migrate build/test tooling to Vite + Vitest (rebased onto fix/idempot…
OBrezhniev Jul 4, 2026
748772b
chore: re-pin ffjavascript (sequential vitest files)
OBrezhniev Aug 26, 2026
e74f95d
chore: re-pin ffjavascript (CI-hang fix: lockdown harness explicit exit)
OBrezhniev Aug 26, 2026
38ffb38
chore: re-pin ffjavascript (wasmcurves re-pin cascade)
OBrezhniev Aug 27, 2026
35cdf64
chore: drop committed vitest browser artifacts and ignore them
OBrezhniev Aug 27, 2026
bfb8fc6
ci: test on lts/-1 as well
OBrezhniev Aug 27, 2026
1f8d3d1
chore: re-pin ffjavascript (wasmcurves lts/-1 cascade)
OBrezhniev Aug 27, 2026
ad59e79
test: cover the Blob streaming path in the real-browser suite
OBrezhniev Aug 27, 2026
e82f452
docs: describe capabilities and per-backend usage; update copyright year
OBrezhniev Aug 27, 2026
87d38de
chore: re-pin ffjavascript (worker unref-on-terminate fix)
OBrezhniev Aug 27, 2026
306d759
Merge pull request #139 from iden3/feature/url-blob-streaming
OBrezhniev Aug 27, 2026
fd4c1b5
feat: opt-in IndexedDB-backed persistent block cache for the http bac…
OBrezhniev Aug 27, 2026
4ee05be
style: doublequotes in the idbcache test (lint)
OBrezhniev Aug 27, 2026
044abe9
fix: in-flight block dedupe in the persistent cache
OBrezhniev Aug 27, 2026
8a68e06
fix: cap concurrent range fetches below the browser connection limit
OBrezhniev Aug 27, 2026
bf54b79
feat: persistent cache covers full-download servers too
OBrezhniev Aug 27, 2026
b588ea3
fix: retry the probe unconditionally when conditional headers fail CORS
OBrezhniev Aug 27, 2026
3c0b908
feat: retry with backoff + stall timeout for range requests (evaluation)
OBrezhniev Aug 27, 2026
52b1732
chore: re-pin ffjavascript (fsqrt c=0 hang fix)
OBrezhniev Aug 28, 2026
653bce6
chore: re-pin ffjavascript (idle-termination dispatch-race fix)
OBrezhniev Aug 28, 2026
66f5695
Merge pull request #145 from iden3/feat/http-retry-stall
OBrezhniev Aug 28, 2026
3014b8c
chore: re-pin ffjavascript (terminate-settles-tasks + build-dedupe fi…
OBrezhniev Aug 28, 2026
011644e
chore: re-pin ffjavascript (worker input-buffer transfer-back)
OBrezhniev Aug 29, 2026
1514b2a
feat: apply cacheSize/pageSize fallbacks to descriptor-object sources
OBrezhniev Aug 29, 2026
83ccfe5
feat!: rename the http descriptor field persistentCache -> cache
OBrezhniev Aug 29, 2026
081fc5a
chore: pin LF line endings via .gitattributes
OBrezhniev Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions .eslintrc.cjs

This file was deleted.

11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Normalize all text to LF in the repository and in every checkout,
# regardless of the local core.autocrlf setting.
* text=auto eol=lf

# Binary test fixtures and artifacts: never subject to EOL conversion.
*.zkey -text
*.ptau -text
*.wtns -text
*.r1cs -text
*.wasm -text
*.png -text
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ name: Continuous Integration
on:
push:
branches:
- main
- master
pull_request:

jobs:
test:
name: Test
timeout-minutes: 15
runs-on: ${{ matrix.os }}

strategy:
Expand All @@ -18,10 +19,10 @@ jobs:

steps:
- name: Checkout project
uses: actions/checkout@v3
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
check-latest: true
Expand All @@ -30,5 +31,8 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install Playwright dependencies
run: npx playwright install

- name: Run tests
run: npm test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ tmp
# Workspace files are user-specific
*.sublime-workspace


# vitest browser-mode artifacts (failure screenshots, attachments)
.vitest-attachments/
test/__screenshots__/
177 changes: 151 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,176 @@
# fastfile

fastfile is a package to read/write binary files with a transparent cache.
fastfile is a package to read/write binary files through one small async API
backed by a transparent page cache. The same `FastFile` interface works over
several backends, so code written against it (for example the snarkjs
zkey/ptau readers) runs unchanged whether the data lives on disk, in memory,
behind an HTTP URL, or inside a browser `Blob`.

## Capabilities

- **Page-cached reads and writes** — small reads (headers, section tables,
`readULE32`-style scans) are served from cached pages; writes are cached and
flushed asynchronously, so sequential access to big files stays fast.
- **Multiple backends behind one API**:

| Backend | Type tag | Where | What it is for |
|---|---|---|---|
| OS file | `file` | Node | Regular files, with direct-I/O fast paths for large aligned reads/writes |
| Memory | `mem` | Node + browser | A `Uint8Array` you already have, or a scratch file that never touches disk |
| Big memory | `bigMem` | Node + browser | Like `mem`, but paged so it can hold buffers past the V8 single-allocation limits |
| HTTP(S) | `http` | Node + browser | Read-only streaming over `Range` requests; falls back to buffering the whole body when the server does not support ranges |
| Blob | `blob` | Browser (and Node ≥ 18) | Read-only zero-copy reads from a `Blob`/`File` (e.g. `<input type="file">`), so multi-GB files stream chunk-by-chunk |

- **Bounded memory on huge remote files** — with the `http` and `blob`
backends, large reads stream straight into the caller's buffer and only the
small-read page cache is retained; the whole file is never resident.
- **Node/browser split at package level** — the `exports` map gives Node the
filesystem implementation and gives bundlers a browser build with the
file backend removed (`mem`, `bigMem`, `http` and `blob` remain).

## Install

This library maintains a read and write cache that allows to write asynchronously
improving the speed of the reads and writes of big files.
```
npm install fastfile
```

## API

All entry points return a promise of a `FastFile` handle:

## Usage
- `createOverride(o, cacheSize?, pageSize?)` — create a file, truncating any
existing one.
- `createNoOverride(o, cacheSize?, pageSize?)` — create a file, failing if it
already exists (Node only distinction; alias of `createOverride` in the
browser).
- `readExisting(o, cacheSize?, pageSize?)` — open read-only. Accepts the most
input shapes (see the examples).
- `readWriteExisting(o, cacheSize?, pageSize?)` — open an existing file
read/write.
- `readWriteExistingOrCreate(o, cacheSize?, pageSize?)` — like the above but
creates the file when missing.

`o` is a file name/URL string, a `Uint8Array`, a `Blob`, or an options object
with a `type` tag from the table above. `cacheSize` (default 64 KiB) and
`pageSize` (default 8 KiB) tune the page cache. They apply to every source
form: for an options object they fill only the fields the object leaves
undefined, so explicit fields on the object always win.

The returned handle offers, among others:

```
npm install fastfile
read(len, pos) readToBuffer(buff, offset, len, pos)
write(buff, pos) readString(pos)
readULE32/readULE64/readUBE32(pos)
writeULE32/writeULE64/writeUBE32(v, pos)
totalSize close() discard()
```

```javascript
Reads return `Uint8Array`s. `close()` flushes pending writes and is
idempotent (repeat calls return the same promise instead of throwing).

const fastFile = require("fastfile");
## Examples

async function writeFile() {
const f = await fastFile.createOverride("pattern.bin");
### OS files (Node)

const buff = Buffer.from("0001020304050607", "hex");
for (let i=0; i<1000; i++) {
await f.write(buff, i*8);
}
```javascript
import * as fastFile from "fastfile";

await f.close();
const f = await fastFile.createOverride("pattern.bin");
const buff = Buffer.from("0001020304050607", "hex");
for (let i = 0; i < 1000; i++) {
await f.write(buff, i * 8);
}
await f.close();

async function readFile() {
const f = await fastFile.readExisting("pattern.bin");
const r = await fastFile.readExisting("pattern.bin");
const bytes = await r.read(16, 8); // 16 bytes from offset 8
await r.close();
```

const buff = await f.read(16, 8);
From CommonJS use `const fastFile = require("fastfile");` — the `require`
condition resolves to the CJS build.

await f.close();
### Memory

return buff;
```javascript
// Wrap bytes you already have (zero-copy):
const f1 = await fastFile.readExisting(new Uint8Array([1, 2, 3, 4]));

}
// Scratch file that never touches disk:
const f2 = await fastFile.createOverride({ type: "mem" });
await f2.writeULE32(42, 0);

// Beyond the single-allocation limit (paged):
const f3 = await fastFile.createOverride({ type: "bigMem" });
```

writeFile().then( () => {
readFile().then( (buff) => {
console.log(buff.toString("hex"));
});
### HTTP(S) with Range streaming

```javascript
// Works in Node and in the browser. Probes the server with
// `Range: bytes=0-0`: when ranges are supported, every read becomes a range
// request and large reads stream directly into your buffer -- the file is
// never fully resident. When the server ignores Range, the body is buffered
// once (the historical behavior).
const f = await fastFile.readExisting("https://example.com/circuit_final.zkey");
console.log(f.totalSize);
const header = await f.read(32, 0);
await f.close();

// Explicit form with cache tuning:
const g = await fastFile.readExisting({
type: "http",
url: "https://example.com/powersOfTau28_hez_final_10.ptau",
cacheSize: 1 << 20,
pageSize: 1 << 16,
});
```

#### Persistent warm start (browsers, experimental)

```javascript
// Opt-in: fetched blocks persist in IndexedDB, so a later session against
// the same URL serves its reads locally instead of re-downloading.
const f = await fastFile.readExisting({
type: "http",
url: "https://example.com/circuit_final.zkey",
cache: true, // or tune it:
// cache: { blockSize: 1 << 21, maxBytes: 1 << 29, dbName: "fastfile-http-cache" },
});
```

## License
Entries are keyed by URL plus the server's strong validator (ETag /
Last-Modified) and are dropped when the remote file changes; without a
strong validator nothing is cached. Works for Range-less servers too: the
first session persists the fully-downloaded body, and later sessions probe
conditionally (If-None-Match / If-Modified-Since) -- an unchanged file
answers a bodyless 304 and every read is served locally. Total storage is bounded (`maxBytes`,
default 512 MiB) with least-recently-opened files evicted first. Where
IndexedDB is unavailable (Node, private windows, blocked storage) the option
is a silent no-op and reads stream as usual.

### Blob / File (browser)

fastfile is part of the iden3 project copyright 2018 0KIMS association and published with GPL-3 license. Please check the COPYING file for more details.
```javascript
// e.g. a multi-GB zkey selected by the user; blob.slice() is zero-copy, so
// bytes reach memory only as they are read.
document.querySelector("input[type=file]").addEventListener("change", async (e) => {
const f = await fastFile.readExisting(e.target.files[0]);
const magic = await f.read(4, 0);
await f.close();
});
```

### In the browser

Bundlers pick the browser build automatically via the package `exports`.
String inputs to `readExisting` are treated as URLs (there is no filesystem);
writable files must be `mem`/`bigMem`. The file backend throws
`"File I/O is not supported in the browser"`.

## License

fastfile is part of the iden3 project copyright 2018-2026 0KIMS association
and published with GPL-3 license. Please check the COPYING file for more
details.
Loading