Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
132 changes: 132 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Firestate Agent Guide

This file is the repo-level operating manual for coding agents. Read it before
changing code.

## What This Package Is

Firestate is a TypeScript library for using Cloud Firestore from React with
real-time listeners, optimistic local state, debounced writes, undo/redo, sync
state tracking, and optional Zod validation.

The recommended application API is registry-based:

```ts
import { z } from 'zod'
import { defineFirestate, doc, col } from '@hvakr/firestate'

const TaskListSchema = z.object({ name: z.string(), createdAt: z.number() })
const TaskSchema = z.object({
title: z.string(),
completed: z.boolean(),
})

export const { useTaskList, useTasks } = defineFirestate({
taskList: doc({ path: 'taskLists/{listId}', schema: TaskListSchema }),
tasks: col({ path: 'taskLists/{listId}/tasks', schema: TaskSchema }),
})
```

The lower-level API is `defineDocument` / `defineCollection` plus
`useDocument` / `useCollection`. Use it for custom path derivation, non-React
usage, or plain TypeScript shapes without Zod validation.

## Commands

Use pnpm.

```bash
pnpm install
pnpm typecheck
pnpm test
pnpm build
```

CI runs `pnpm typecheck`, `pnpm build`, and `pnpm test` on Node 22.

## Source Map

- `src/index.ts` - public exports. Update this when adding public API.
- `src/firestate.ts` - registry API: `defineFirestate`, `doc`, `col`, path
template validation, generated hook typing.
- `src/schema.ts` - lower-level definition helpers.
- `src/types.ts` - public state, handle, definition, undo, and config types.
- `src/hooks.ts` - React hooks and `useSyncExternalStore` integration.
- `src/provider.tsx` - React providers and unsaved-changes hook.
- `src/store.ts` - shared Firestore config, undo manager, global sync state,
error reporting.
- `src/document.ts` - single-document subscription, optimistic state, set,
update, delete, sync, conflict rebase.
- `src/collection.ts` - collection subscription, add, update, remove, batched
sync, lazy loading.
- `src/diff.ts` - Firestore-aware diff, flattening, cloning, equality helpers.
- `src/undo.ts` - framework-agnostic undo manager.
- `examples/react-tasks/` - runnable React + Firebase example.

## Behavioral Contracts

Preserve these unless the task explicitly changes them.

- The registry API requires Zod schemas. `doc()` and `col()` infer data types
from `schema` and infer hook params from `{name}` placeholders in `path`.
- `defineDocument` and `defineCollection` keep the plain TypeScript escape
hatch. Their `schema` field is optional.
- Schemas are validation guards only. Firestate calls `schema.parse(...)` on
full writes (`document.set`, `collection.add`) but stores the caller's
original object. Do not store the parsed result unless intentionally changing
this contract.
- Partial `update(diff)` calls are not Zod-validated because diffs may include
Firestore sentinels such as `serverTimestamp()`, `arrayUnion()`, or
`deleteField()`.
- Document `update()` requires existing current data. Use `set()` to create or
replace a document.
- Collection `add`, `update`, and `remove` require the first snapshot. They
bail before the initial snapshot to avoid clobbering unknown server fields.
- `enabled: false` on hooks must not resolve paths or create subscriptions. It
returns stable no-op handles.
- `queryConstraints` should be treated by reference. Callers are expected to
memoize inline arrays.
- `useSyncExternalStore` snapshots and handles must have stable identity between
changes. Do not rebuild snapshots on every `getSnapshot()` call.
- Unmounting a subscription clears its autosave timer and unregisters its sync
state. Pending debounced edits are not automatically flushed on `stop()`.
- Undo actions are client-local. Grouped undo actions undo newest to oldest and
redo oldest to newest.
- Firestore updates use flattened diffs for `updateDoc`; full document
replacement and creation use `setDoc`.

## Test Guide

Add or update focused tests near the behavior being changed:

- Registry typing/path behavior: `src/schema.test.ts` and
`src/firestate.test.ts`.
- Document subscription behavior: `src/firestate.test.ts` and
`src/firestate.integration.test.ts`.
- Collection behavior: `src/firestate.integration.test.ts` and
`src/store.test.ts`.
- Diff behavior: `src/diff.test.ts`.
- Undo behavior: `src/undo.test.ts`.
- Store/global sync behavior: `src/store.test.ts`.

Before finishing code changes, run at least:

```bash
pnpm typecheck
pnpm test
```

For public API or build output changes, also run:

```bash
pnpm build
```

## Documentation

- User README: `README.md`
- Architecture notes: `docs/architecture.md`
- Usage recipes: `docs/api-recipes.md`
- Contributor workflow: `CONTRIBUTING.md`

When changing public behavior, update the README or docs in the same change.
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Claude Instructions

Start with `AGENTS.md`. It is the source of truth for repo structure,
commands, architecture, and behavioral contracts.

Then use:

- `README.md` for public usage documentation.
- `docs/architecture.md` for internal design.
- `docs/api-recipes.md` for implementation examples and gotchas.
- `CONTRIBUTING.md` for local development and PR workflow.
74 changes: 74 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Contributing

Thanks for working on Firestate. This repo is a small TypeScript library, so
changes should stay focused and come with tests when behavior changes.

## Setup

```bash
pnpm install
```

This package targets Node 18+ for consumers. CI runs on Node 22.

## Development Commands

```bash
pnpm typecheck
pnpm test
pnpm build
```

Useful variants:

```bash
pnpm test:watch
pnpm test:coverage
```

## Project Layout

- `src/` contains the library source.
- `src/index.ts` is the public export surface.
- `examples/react-tasks/` is a runnable React + Firebase example.
- `docs/architecture.md` explains how the internals fit together.
- `docs/api-recipes.md` contains usage examples and edge-case guidance.

## Change Guidelines

- Prefer the registry API (`defineFirestate`, `doc`, `col`) for examples and
app-facing docs.
- Keep the lower-level API (`defineDocument`, `defineCollection`,
`useDocument`, `useCollection`) working as an escape hatch.
- Preserve the Zod contract: schemas validate `set` and `add`, but parsed
values are not written and partial `update` diffs are not validated.
- Keep React snapshots stable for `useSyncExternalStore`.
- Do not add dependencies unless they remove real maintenance burden.
- Update docs when changing public behavior.

## Testing Expectations

Run these before opening a PR:

```bash
pnpm typecheck
pnpm test
pnpm build
```

Add focused tests for behavior changes:

- Diff behavior: `src/diff.test.ts`
- Undo behavior: `src/undo.test.ts`
- Registry and schema behavior: `src/schema.test.ts`, `src/firestate.test.ts`
- Store and sync behavior: `src/store.test.ts`
- Firestore subscription behavior: `src/firestate.integration.test.ts`

## Releases

Publishing is handled by the `Publish Package to npm` workflow when a GitHub
release is published. The package build command is:

```bash
pnpm build
```
96 changes: 95 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Both layers share the same store, undo manager, and sync semantics — the regis
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Examples](#examples)
- [Documentation](#documentation)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Diff Utilities](#diff-utilities)
Expand Down Expand Up @@ -245,6 +246,14 @@ Check out the [examples](./examples) directory for complete, runnable examples:

- **[React Tasks](./examples/react-tasks)** - A simple task manager demonstrating documents, collections, undo/redo, sync indicators, and real-time updates.

## Documentation

- [Architecture](./docs/architecture.md) - how the registry API, hooks, store, subscriptions, diffing, sync, and undo layers fit together.
- [API Recipes](./docs/api-recipes.md) - focused examples for common usage patterns and edge cases.
- [Contributing](./CONTRIBUTING.md) - local setup, commands, tests, and release notes.
- [Agent Guide](./AGENTS.md) - repo map and behavioral contracts for AI coding agents.
- [Claude Instructions](./CLAUDE.md) - short pointer for Claude Code.

## Core Concepts

### Documents vs Collections
Expand Down Expand Up @@ -359,6 +368,86 @@ awaiting writes is not feasible.

## API Reference

### Registry API

#### `defineFirestate(registry)`

Creates typed React hooks from a registry object. Each key becomes a hook named
`use{CapitalizedKey}`.

```typescript
import { z } from 'zod'
import { defineFirestate, doc, col } from '@hvakr/firestate'

const ProjectSchema = z.object({
name: z.string(),
createdAt: z.number(),
})

const SpaceSchema = z.object({
name: z.string(),
area: z.number(),
floor: z.number(),
})

export const { useProject, useSpaces } = defineFirestate({
project: doc({
path: 'projects/{projectId}',
schema: ProjectSchema,
}),
spaces: col({
path: 'projects/{projectId}/spaces',
schema: SpaceSchema,
lazy: true,
}),
})
```

Generated hooks require the params implied by the path template:

```tsx
const project = useProject({ projectId })
const spaces = useSpaces({ projectId })
```

Use the second argument for hook options such as `enabled`, `readOnly`,
`undoable`, or collection `queryConstraints`:

```tsx
const spaces = useSpaces(
{ projectId },
{
enabled: Boolean(projectId),
queryConstraints,
}
)
```

#### `doc(options)` and `col(options)`

Declare registry entries. A Zod `schema` is required and drives both the
generated TypeScript data type and runtime validation for full writes.

```typescript
doc({
path: 'projects/{projectId}',
schema: ProjectSchema,
autosave: 1000,
readOnly: false,
retryOnError: false,
})

col({
path: 'projects/{projectId}/spaces',
schema: SpaceSchema,
lazy: true,
queryConstraints: [],
})
```

Path placeholders must look like `{name}`. Empty param values throw at runtime
when a path is resolved.

### Definition Helpers

#### `defineDocument(definition)`
Expand Down Expand Up @@ -417,6 +506,7 @@ const {
params: { projectId: '123' },
readOnly: false, // Optional: override read-only
undoable: true, // Optional: enable undo (default: true)
enabled: true, // Optional: set false until required params exist
})
```

Expand All @@ -442,6 +532,7 @@ const {
params: { projectId: '123' },
queryConstraints: [where('floor', '==', 1)],
undoable: true,
enabled: true, // Optional: set false until required params exist
})

// Update existing documents
Expand Down Expand Up @@ -606,7 +697,7 @@ const combined = mergeDiffs(diff1, diff2)

## Notes

- **`enabled` flag** — pass `enabled: false` to `useDocument`/`useCollection` to skip the subscription when params aren't ready (e.g., during a route transition).
- **`enabled` flag** — pass `enabled: false` to generated hooks or to `useDocument`/`useCollection` when route params or auth-derived ids are not ready yet. Disabled hooks do not resolve paths or attach listeners, which avoids building invalid Firestore paths like `projects//spaces`.
- **Navigation flicker** — changing `params` rebuilds the listener and briefly shows `isLoading: true`. To keep the previous data visible across the transition, wrap your param in `useDeferredValue`.
- **No cross-doc transactions** — writes are atomic per document and per collection (via `writeBatch`), but not across them. For now, use Firestore's `runTransaction` directly via `handle.ref`.
- **Per-client undo** — `useUndoManager` is local; one user's undo doesn't propagate to others.
Expand Down Expand Up @@ -853,6 +944,9 @@ function ProjectEditor({ projectId }) {

Contributions are welcome! Please feel free to submit a Pull Request.

See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, workflow, and testing
guidelines.

### Development

```bash
Expand Down
Loading
Loading