Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c344072
feat: add logging to compaction and repro script for debugging blocks
jchris Jun 25, 2025
7f0de21
fix: prevent race conditions in autoCompact by waiting for commit que…
jchris Jun 25, 2025
f9337dd
test: add regression test for block compaction during concurrent writ…
jchris Jun 25, 2025
b20a1e7
chore: remove unused repro-blocks test script
jchris Jun 26, 2025
266b090
chore: run proper tests in our test enviromments
mabels Jun 26, 2025
096d439
feat: add docker management scripts and blocks regression test
jchris Jun 26, 2025
e6d5c36
style: improve formatting and whitespace in docs and tests
jchris Jun 26, 2025
3066dfe
docs: document compaction race condition analysis and solution strategy
jchris Jun 26, 2025
d2f479c
test: add repro tests and docs for meta block dangling refs during co…
jchris Jun 27, 2025
72cfec4
wip
mabels Jul 10, 2025
7f820b6
test: add repro tests for meta block dangling refs during compaction
jchris Jul 19, 2025
4213592
refactor: make compact function optional in CRDT blockstore initializ…
jchris Jul 19, 2025
745540d
refactor: improve type safety in CRDT blockstore options handling
jchris Jul 19, 2025
bc3db1c
test: disable compaction in repro blocks tests to isolate block behavior
jchris Jul 19, 2025
262b2d3
setting null here runs default blockstore (full) compaction and resul…
jchris Jul 19, 2025
d06da4f
fix: correct event types and fix dangling refs in meta block tests. s…
jchris Jul 25, 2025
a145215
feat: replace compact option with CompactionMode enum for clearer com…
jchris Jul 25, 2025
10009b8
test: enable repro-blocks test suite and fix dbName scoping
jchris Jul 25, 2025
5bb9803
test: add test coverage for default compaction mode alongside full co…
jchris Jul 25, 2025
652800b
skip the process test
jchris Jul 26, 2025
8ca44f2
style: remove trailing whitespace in test description
jchris Jul 26, 2025
1c5b7c2
docs: clarify comment about filtering document rows
jchris Jul 26, 2025
c2fda9d
chore: update main + compactStrategie
mabels Jul 29, 2025
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
145 changes: 145 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Fireproof is a lightweight embedded document database with encrypted live sync for JavaScript environments. It's designed to work in browsers, Node.js, Deno, and other JavaScript runtimes with a unified API. The repository is structured as a monorepo with multiple packages and includes React hooks integration.

## Common Development Commands

- `pnpm run check` - Run format, lint, test, and build in sequence

### Building and Development

- `pnpm run build` - Build all packages (runs prebuild, build scripts, and pub scripts)
- `pnpm run build:tsc` - Build using TypeScript compiler
- `pnpm run build:tsup` - Build using tsup bundler
- `pnpm run dev` - Start development servers for cloud components
- `pnpm run dev:dashboard` - Start dashboard development server on port 3002
- `pnpm run dev:3rd-party` - Start 3rd-party development server on port 3001

### Testing

- `pnpm run test` - Run all tests using vitest
- `pnpm run test:file` - Run file-based tests
- `pnpm run test:indexeddb` - Run IndexedDB-specific tests
- `pnpm run test:deno` - Run tests in Deno environment
- `pnpm run test -t 'test name pattern' path/to/test/file` - Run specific tests
- `FP_DEBUG=Loader pnpm run test --project file -t 'codec implicit iv' crdt` - Run specific test with debugging

### Code Quality

- `pnpm run lint` - Run ESLint
- `pnpm run format` - Run Prettier formatting

### Docker Management

- `pnpm run docker:down` - Stop Docker containers
- `pnpm run docker:up` - Start Docker containers
- `pnpm run docker:restart` - Restart Docker containers
- `pnpm run docker:logs` - View Docker container logs
- `pnpm run docker:health` - Check Docker container and MinIO health

### Publishing and Distribution

- `pnpm run smoke` - Run smoke tests against built packages
- `pnpm run fppublish` - Publish packages to npm
- `pnpm run presmoke` - Build and publish to local registry for smoke testing

## Architecture Overview

### Core Components

**Database Layer (`src/database.ts`, `src/ledger.ts`)**

- `DatabaseImpl` - Main database implementation with CRUD operations
- `Ledger` - Lower-level data storage and versioning layer
- CRDT (Conflict-free Replicated Data Types) implementation for distributed consistency

**Blockstore (`src/blockstore/`)**

- Content-addressed storage system using IPLD blocks
- Multiple gateway implementations (file, IndexedDB, memory, cloud)
- Encryption and serialization handling
- Transaction management and commit queues

**Runtime (`src/runtime/`)**

- Platform-specific implementations (Node.js, Deno, browser)
- File system abstractions
- Key management and cryptography
- Storage gateway factory patterns

**React Integration (`src/react/`)**

- `useFireproof` - Main hook for database access
- `useLiveQuery` - Real-time query results
- `useDocument` - Document-level operations
- `useAllDocs` - Bulk document operations
- `ImgFile` component for file attachments

**Protocols (`src/protocols/`)**

- Cloud synchronization protocols
- Dashboard API protocols
- Message passing and connection management

### Storage Gateways

The system supports multiple storage backends:

- **File** - Local file system storage (Node.js/Deno)
- **IndexedDB** - Browser-based storage
- **Memory** - In-memory storage for testing
- **Cloud** - Remote storage with sync capabilities

### Testing Infrastructure

Uses Vitest with multiple configurations:

- `vitest.workspace.ts` - Main workspace configuration
- Separate configs for file, memory, IndexedDB, and cloud testing
- Screenshot testing for React components
- Multiple test environments (file, memory, indexeddb, cloud variants)

## Key File Locations

- `src/index.ts` - Main entry point
- `src/database.ts` - Database implementation
- `src/ledger.ts` - Core ledger functionality
- `src/crdt.ts` - CRDT implementation
- `src/blockstore/` - Storage layer
- `src/runtime/` - Platform-specific code
- `src/react/` - React hooks and components
- `tests/` - Test suites organized by component

## Development Notes

- Uses pnpm for package management
- TypeScript with strict configuration
- ESM modules throughout
- Supports Node.js >=20.18.1
- Uses Vitest for testing with multiple environments
- Includes comprehensive smoke testing pipeline
- Debug logging available via `FP_DEBUG` environment variable
- Uses content-addressed storage with cryptographic integrity
- Implements causal consistency for distributed operations

## React Development

When working with React components:

- Use `useFireproof` hook to access database functionality
- `useLiveQuery` provides real-time query results that update automatically
- `useDocument` handles individual document operations with optimistic updates
- File attachments are handled through the `_files` property and `ImgFile` component
- Test React components using the testing utilities in `tests/react/`

## Cloud and Sync

- Cloud functionality is in the `cloud/` directory
- Supports multiple cloud backends (CloudFlare D1, LibSQL, etc.)
- WebSocket and HTTP-based synchronization
- Encrypted data transmission and storage
- Multi-tenant architecture support
30 changes: 23 additions & 7 deletions core/blockstore/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,33 @@ export class BaseBlockstoreImpl implements BlockFetcher {
return new CarTransactionImpl(this, opts);
}

inflightCompaction = false;

needsCompaction() {
if (!this.inflightCompaction && this.ebOpts.autoCompact && this.loader.carLog.length > this.ebOpts.autoCompact) {
this.inflightCompaction = true;
// Wait until the commit queue is idle before triggering compaction to
// ensure no commits are still in-flight. This prevents race conditions
// where compaction runs before all blocks have been persisted.
this.loader.commitQueue
.waitIdle()
.then(() => this.compact())
.catch((err) => {
this.logger.Warn().Err(err).Msg("autoCompact scheduling failed");
})
.finally(() => {
this.inflightCompaction = false;
});
}
}

async commitTransaction<M extends TransactionMeta>(
t: CarTransaction,
done: M,
opts: CarTransactionOpts,
): Promise<TransactionWrapper<M>> {
if (!this.loader) throw this.logger.Error().Msg("loader required to commit").AsError();
const cars = await this.loader.commit<M>(t, done, opts);
if (this.ebOpts.autoCompact && this.loader.carLog.length > this.ebOpts.autoCompact) {
setTimeout(() => void this.compact(), 10);
}
this.needsCompaction();
if (cars) {
this.transactions.delete(t);
return { meta: done, cars, t };
Expand Down Expand Up @@ -255,9 +272,7 @@ export class EncryptedBlockstore extends BaseBlockstoreImpl {
this.logger.Debug().Msg("post super.transaction");
const cars = await this.loader.commit<M>(t, done, opts);
this.logger.Debug().Msg("post this.loader.commit");
if (this.ebOpts.autoCompact && this.loader.carLog.length > this.ebOpts.autoCompact) {
setTimeout(() => void this.compact(), 10);
}
this.needsCompaction();
if (cars) {
this.transactions.delete(t);
return { meta: done, cars, t };
Expand All @@ -278,6 +293,7 @@ export class EncryptedBlockstore extends BaseBlockstoreImpl {
}

async compact() {
this.logger.Debug().Any({ carLogLen_before: this.loader?.carLog.length }).Msg("compact() – start");
await this.ready();
if (!this.loader) throw this.logger.Error().Msg("loader required to compact").AsError();
if (this.loader.carLog.length < 2) return;
Expand Down
82 changes: 82 additions & 0 deletions core/tests/fireproof/repro-blocks-inline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Database, DocWithId, fireproof } from "@fireproof/core";
import { describe, it, expect } from "vitest";

interface Record {
id: string;
type: string;
createdAt: string;
}

async function findAll(db: Database): Promise<Record[]> {
const result = await db.query(
(doc: DocWithId<Record>) => {
if (doc.type === "CustomPropertyDefinition" && doc.createdAt && doc._deleted !== true) {
return doc.createdAt;
}
},
{ descending: true },
);
return result.rows
.filter((row) => row.doc) // Filter out any rows without documents
.map((row) => row.doc as Record);
}

const numberOfDocs = 100;

async function writeSampleData(db: Database): Promise<void> {
console.log("start puts");

Check warning on line 27 in core/tests/fireproof/repro-blocks-inline.test.ts

View workflow job for this annotation

GitHub Actions / compile_test

Unexpected console statement
for (let i = 10; i < numberOfDocs; i++) {
const record: DocWithId<Record> = {
_id: `record-${i}`,
id: `record-${i}`,
type: "CustomPropertyDefinition",
createdAt: new Date().toISOString(),
};
await db.put(record);
}
console.log("start dels");

Check warning on line 37 in core/tests/fireproof/repro-blocks-inline.test.ts

View workflow job for this annotation

GitHub Actions / compile_test

Unexpected console statement
for (let i = 10; i < numberOfDocs; i += 10) {
await db.del(`record-${i}`);
}
}

async function runReproBlocksOnce(iter: number, compactStrategy?: string) {
const db = fireproof(`test-db-inline-${iter}-${Date.now()}`, {
compactStrategy,
});

await writeSampleData(db);

const all = await db.allDocs<Record>();
const records = await findAll(db);

console.log(`repro-blocks inline run ${iter}: Found records:`, all.rows.length, records.length);

Check warning on line 53 in core/tests/fireproof/repro-blocks-inline.test.ts

View workflow job for this annotation

GitHub Actions / compile_test

Unexpected console statement
expect(all.rows.length).toBe(81); // 90 puts - 9 deletes = 81
expect(records.length).toBe(81);

// Clean up the database after the test
await db.destroy();
}

// Test both compaction modes in a single test process
describe("repro-blocks inline regression test", () => {
it(
"runs with fireproof-default compaction mode",
async () => {
for (let i = 1; i <= 3; i++) {
await runReproBlocksOnce(i, undefined);
}
},
2 * 60 * 1000, // 2 minutes
);

it(
"runs with full compaction mode",
async () => {
for (let i = 1; i <= 3; i++) {
await runReproBlocksOnce(i, "full");
}
},
2 * 60 * 1000, // 2 minutes
);
});
80 changes: 80 additions & 0 deletions core/tests/fireproof/repro-blocks.process.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, it } from "vitest";
import { Database, DocWithId, fireproof } from "@fireproof/core";

// Skip this entire suite when running inside a browser-like Vitest environment
const isNode = typeof process !== "undefined" && !!process.versions?.node;
const describeFn = isNode ? describe.skip : describe.skip;
Comment on lines +4 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the environment detection logic.

The current logic always uses describe.skip regardless of environment, preventing tests from ever running.

-const describeFn = isNode ? describe.skip : describe.skip;
+const describeFn = isNode ? describe : describe.skip;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Skip this entire suite when running inside a browser-like Vitest environment
const isNode = typeof process !== "undefined" && !!process.versions?.node;
const describeFn = isNode ? describe.skip : describe.skip;
// Skip this entire suite when running inside a browser-like Vitest environment
const isNode = typeof process !== "undefined" && !!process.versions?.node;
const describeFn = isNode ? describe : describe.skip;
🤖 Prompt for AI Agents
In core/tests/fireproof/repro-blocks.process.test.ts around lines 4 to 6, the
environment detection logic incorrectly assigns describe.skip for both Node and
non-Node environments, causing tests to always be skipped. Update the assignment
of describeFn so that it uses describe when running in Node (isNode true) and
describe.skip otherwise, enabling tests to run only in the intended environment.


/* eslint-disable no-console */

interface Record {
id: string;
type: string;
createdAt: string;
}

async function findAll(db: Database): Promise<Record[]> {
const result = await db.query(
(doc: DocWithId<Record>) => {
if (doc.type === "CustomPropertyDefinition" && doc.createdAt && doc._deleted !== true) {
return doc.createdAt;
}
},
{ descending: true },
);
return result.rows
.filter((row) => row.doc) // Filter out rows without documents
.map((row) => row.doc as Record);
}

const numberOfDocs = 100;

async function writeSampleData(db: Database): Promise<void> {
console.log("start puts");
for (let i = 10; i < numberOfDocs; i++) {
const record: DocWithId<Record> = {
Comment on lines +30 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the loop bounds to align with numberOfDocs expectation

The function starts from i=10 instead of i=0, which creates documents with IDs from record-10 to record-99 (90 documents) rather than the expected 100 documents. This is confusing given the variable name numberOfDocs = 100.

Apply this diff to fix the loop bounds:

 const numberOfDocs = 100;
 
 async function writeSampleData(db: Database): Promise<void> {
   console.log("start puts");
-  for (let i = 10; i < numberOfDocs; i++) {
+  for (let i = 0; i < numberOfDocs; i++) {

Also update the deletion loop:

   console.log("start dels");
-  for (let i = 10; i < numberOfDocs; i += 10) {
+  for (let i = 0; i < numberOfDocs; i += 10) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In core/tests/fireproof/repro-blocks.process.test.ts around lines 30 to 35, the
loop in writeSampleData starts at i=10 instead of i=0, resulting in only 90
documents instead of the expected 100. Change the loop to start from i=0 and run
while i < numberOfDocs to create exactly 100 documents. Additionally, update the
corresponding deletion loop to match these bounds to ensure all created
documents are properly deleted.

_id: `record-${i}`,
id: `record-${i}`,
type: "CustomPropertyDefinition",
createdAt: new Date().toISOString(),
};
await db.put(record);
}
console.log("start dels");
for (let i = 10; i < numberOfDocs; i += 10) {
await db.del(`record-${i}`);
}
}

async function runReproBlocksOnce(iter: number, compactStrategy?: string) {
const db = fireproof(`test-db-${iter}`, {
compactStrategy,
});

await writeSampleData(db);

const all = await db.allDocs<Record>();
const records = await findAll(db);

console.log(`repro-blocks run ${iter}: Found records:`, all.rows.length, records.length);
console.log(`repro-blocks run ${iter}: ok`); // useful in CI logs

// Clean up the database after the test
await db.destroy();
}

// Test both compaction modes
describeFn.each([
{ name: "fireproof-default", compactionMode: undefined },
{ name: "full-compaction", compactionMode: "full" },
])("repro-blocks regression test with $name compaction", ({ compactionMode }) => {
it(
"runs 10 consecutive times without compaction errors",
async () => {
for (let i = 1; i <= 10; i++) {
await runReproBlocksOnce(i, compactionMode);
}
},
5 * 60 * 1000, // allow up to 5 minutes – heavy disk workload
);
});
Loading
Loading