@trestleinc/replicate 1.1.0 → 1.1.2-preview.0

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 (91) hide show
  1. package/README.md +446 -260
  2. package/dist/client/index.d.ts +311 -19
  3. package/dist/client/index.js +4027 -0
  4. package/dist/component/_generated/api.d.ts +13 -17
  5. package/dist/component/_generated/api.js +24 -4
  6. package/dist/component/_generated/component.d.ts +79 -77
  7. package/dist/component/_generated/component.js +1 -0
  8. package/dist/component/_generated/dataModel.d.ts +12 -15
  9. package/dist/component/_generated/dataModel.js +1 -0
  10. package/dist/component/_generated/server.d.ts +19 -22
  11. package/dist/component/_generated/server.js +65 -1
  12. package/dist/component/_virtual/rolldown_runtime.js +18 -0
  13. package/dist/component/convex.config.d.ts +6 -2
  14. package/dist/component/convex.config.js +7 -3
  15. package/dist/component/logger.d.ts +10 -6
  16. package/dist/component/logger.js +25 -28
  17. package/dist/component/public.d.ts +70 -61
  18. package/dist/component/public.js +311 -295
  19. package/dist/component/schema.d.ts +53 -45
  20. package/dist/component/schema.js +26 -32
  21. package/dist/component/shared/types.d.ts +9 -0
  22. package/dist/component/shared/types.js +15 -0
  23. package/dist/server/index.d.ts +134 -13
  24. package/dist/server/index.js +368 -0
  25. package/dist/shared/index.d.ts +27 -3
  26. package/dist/shared/index.js +1 -2
  27. package/package.json +34 -29
  28. package/src/client/collection.ts +339 -306
  29. package/src/client/errors.ts +9 -9
  30. package/src/client/index.ts +13 -32
  31. package/src/client/logger.ts +2 -2
  32. package/src/client/merge.ts +37 -34
  33. package/src/client/persistence/custom.ts +84 -0
  34. package/src/client/persistence/index.ts +9 -46
  35. package/src/client/persistence/indexeddb.ts +111 -84
  36. package/src/client/persistence/memory.ts +3 -3
  37. package/src/client/persistence/sqlite/browser.ts +168 -0
  38. package/src/client/persistence/sqlite/native.ts +29 -0
  39. package/src/client/persistence/sqlite/schema.ts +124 -0
  40. package/src/client/persistence/types.ts +32 -28
  41. package/src/client/prose-schema.ts +55 -0
  42. package/src/client/prose.ts +28 -25
  43. package/src/client/replicate.ts +5 -5
  44. package/src/client/services/cursor.ts +109 -0
  45. package/src/component/_generated/component.ts +31 -29
  46. package/src/component/convex.config.ts +2 -2
  47. package/src/component/logger.ts +7 -7
  48. package/src/component/public.ts +225 -237
  49. package/src/component/schema.ts +18 -15
  50. package/src/server/builder.ts +20 -7
  51. package/src/server/index.ts +3 -5
  52. package/src/server/schema.ts +5 -5
  53. package/src/server/storage.ts +113 -59
  54. package/src/shared/index.ts +5 -5
  55. package/src/shared/types.ts +51 -14
  56. package/dist/client/collection.d.ts +0 -96
  57. package/dist/client/errors.d.ts +0 -59
  58. package/dist/client/logger.d.ts +0 -2
  59. package/dist/client/merge.d.ts +0 -77
  60. package/dist/client/persistence/adapters/index.d.ts +0 -8
  61. package/dist/client/persistence/adapters/opsqlite.d.ts +0 -46
  62. package/dist/client/persistence/adapters/sqljs.d.ts +0 -83
  63. package/dist/client/persistence/index.d.ts +0 -49
  64. package/dist/client/persistence/indexeddb.d.ts +0 -17
  65. package/dist/client/persistence/memory.d.ts +0 -16
  66. package/dist/client/persistence/sqlite-browser.d.ts +0 -51
  67. package/dist/client/persistence/sqlite-level.d.ts +0 -63
  68. package/dist/client/persistence/sqlite-rn.d.ts +0 -36
  69. package/dist/client/persistence/sqlite.d.ts +0 -47
  70. package/dist/client/persistence/types.d.ts +0 -42
  71. package/dist/client/prose.d.ts +0 -56
  72. package/dist/client/replicate.d.ts +0 -40
  73. package/dist/client/services/checkpoint.d.ts +0 -18
  74. package/dist/client/services/reconciliation.d.ts +0 -24
  75. package/dist/index.js +0 -1620
  76. package/dist/server/builder.d.ts +0 -94
  77. package/dist/server/schema.d.ts +0 -27
  78. package/dist/server/storage.d.ts +0 -80
  79. package/dist/server.js +0 -281
  80. package/dist/shared/types.d.ts +0 -50
  81. package/dist/shared/types.js +0 -6
  82. package/dist/shared.js +0 -6
  83. package/src/client/persistence/adapters/index.ts +0 -8
  84. package/src/client/persistence/adapters/opsqlite.ts +0 -54
  85. package/src/client/persistence/adapters/sqljs.ts +0 -128
  86. package/src/client/persistence/sqlite-browser.ts +0 -107
  87. package/src/client/persistence/sqlite-level.ts +0 -407
  88. package/src/client/persistence/sqlite-rn.ts +0 -44
  89. package/src/client/persistence/sqlite.ts +0 -161
  90. package/src/client/services/checkpoint.ts +0 -86
  91. package/src/client/services/reconciliation.ts +0 -108
package/README.md CHANGED
@@ -2,74 +2,64 @@
2
2
 
3
3
  **Offline-first sync library using Yjs CRDTs and Convex for real-time data synchronization.**
4
4
 
5
- Replicate provides a dual-storage architecture for building offline-capable applications with automatic conflict resolution. It combines Yjs CRDTs (96% smaller than Automerge, no WASM) with TanStack DB's reactive state management and Convex's reactive backend for real-time synchronization and efficient querying.
6
-
7
- ## Features
8
-
9
- - **Offline-first** - Works without internet, syncs when reconnected
10
- - **Yjs CRDTs** - Automatic conflict-free replication with Yjs (96% smaller than Automerge, no WASM)
11
- - **Real-time sync** - Convex WebSocket-based synchronization
12
- - **TanStack DB integration** - Reactive state management for React and Svelte
13
- - **Dual-storage pattern** - CRDT layer for conflict resolution + main tables for queries
14
- - **Event sourcing** - Append-only event log preserves complete history
15
- - **Type-safe** - Full TypeScript support
16
- - **Multi-tab sync** - Changes sync instantly across browser tabs via TanStack coordination
17
- - **SSR support** - Server-side rendering with data preloading
18
- - **Network resilience** - Automatic retry with exponential backoff
19
- - **Component-based** - Convex component for plug-and-play CRDT storage
20
- - **Swappable persistence** - IndexedDB (browser), SQLite (React Native), or in-memory (testing)
21
- - **React Native compatible** - SQLite persistence with y-op-sqlite and op-sqlite
22
- - **Version history** - Create, list, restore, and delete document versions
23
- - **Auto-compaction** - Size-based per-document compaction (no cron jobs needed)
5
+ Replicate provides a dual-storage architecture for building offline-capable applications with automatic conflict resolution. It combines Yjs CRDTs with TanStack DB's reactive state management and Convex's reactive backend for real-time synchronization and efficient querying.
6
+
24
7
 
25
8
  ## Architecture
26
9
 
27
- ### Data Flow: Real-Time Sync
10
+ ### Data Flow
28
11
 
29
12
  ```mermaid
30
13
  sequenceDiagram
31
- participant User
32
- participant UI as React/Svelte Component
33
- participant TDB as TanStack DB
14
+ participant UI as React Component
15
+ participant Collection as TanStack DB Collection
34
16
  participant Yjs as Yjs CRDT
35
- participant Offline as Offline Executor
36
- participant Convex as Convex Component
17
+ participant Storage as Local Storage<br/>(SQLite)
18
+ participant Convex as Convex Backend
37
19
  participant Table as Main Table
38
20
 
39
- User->>UI: Create/Update Task
40
- UI->>TDB: collection.insert/update
41
- TDB->>Yjs: Update Yjs CRDT
42
- Yjs-->>TDB: Notify change
43
- TDB-->>UI: Re-render (optimistic)
21
+ Note over UI,Storage: Client-side (offline-capable)
22
+ UI->>Collection: insert/update/delete
23
+ Collection->>Yjs: Apply change to Y.Doc
24
+ Yjs->>Storage: Persist locally
25
+ Collection-->>UI: Re-render (optimistic)
44
26
 
45
- Note over Offline: Automatic retry with backoff
46
- Offline->>Yjs: Get CRDT delta
47
- Offline->>Convex: insert/update mutation
48
- Convex->>Component: Append delta to event log
49
- Convex->>Table: Insert/Update materialized doc
27
+ Note over Collection,Convex: Sync layer
28
+ Collection->>Convex: Send CRDT delta
29
+ Convex->>Convex: Append to event log
30
+ Convex->>Table: Update materialized doc
50
31
 
51
- Note over Convex,Table: Change detected
52
- Table-->>UI: Subscription update
53
- UI-->>User: Re-render with synced data
32
+ Note over Convex,UI: Real-time updates
33
+ Table-->>Collection: stream subscription
34
+ Collection-->>UI: Re-render with server state
54
35
  ```
55
36
 
56
- ### Dual-Storage Architecture
37
+ ### Dual-Storage Pattern
57
38
 
58
39
  ```mermaid
59
- graph LR
60
- Client[Client<br/>Yjs CRDT]
61
- Component[Component Storage<br/>Event Log<br/>CRDT Deltas]
62
- MainTable[Main Application Table<br/>Materialized Docs<br/>Efficient Queries]
63
-
64
- Client -->|insert/update/remove| Component
65
- Component -->|also writes to| MainTable
66
- MainTable -->|subscription| Client
40
+ graph TB
41
+ subgraph Client
42
+ TDB[TanStack DB]
43
+ Yjs[Yjs CRDT]
44
+ Local[(SQLite)]
45
+ TDB <--> Yjs
46
+ Yjs <--> Local
47
+ end
48
+
49
+ subgraph Convex
50
+ Component[(Event Log<br/>CRDT Deltas)]
51
+ Main[(Main Table<br/>Materialized Docs)]
52
+ Component --> Main
53
+ end
54
+
55
+ Yjs -->|insert/update/remove| Component
56
+ Main -->|stream subscription| TDB
67
57
  ```
68
58
 
69
- **Why both?**
70
- - **Component Storage (Event Log)**: Append-only CRDT deltas, complete history, conflict resolution
71
- - **Main Tables (Read Model)**: Current state, efficient server-side queries, indexes, and reactive subscriptions
72
- - Similar to CQRS/Event Sourcing: component = event log, main table = materialized view
59
+ **Why dual storage?**
60
+ - **Event Log (Component)**: Append-only CRDT deltas for conflict resolution and history
61
+ - **Main Table**: Materialized current state for efficient queries and indexes
62
+ - Similar to CQRS: event log = write model, main table = read model
73
63
 
74
64
  ## Installation
75
65
 
@@ -115,25 +105,24 @@ export default defineSchema({
115
105
  tasks: schema.table(
116
106
  {
117
107
  // Your application fields only!
118
- // version and timestamp are automatically injected by schema.table()
108
+ // timestamp is automatically injected by schema.table()
119
109
  id: v.string(),
120
110
  text: v.string(),
121
111
  isCompleted: v.boolean(),
122
112
  },
123
113
  (t) => t
124
- .index('by_user_id', ['id']) // Required for document lookups
114
+ .index('by_doc_id', ['id']) // Required for document lookups
125
115
  .index('by_timestamp', ['timestamp']) // Required for incremental sync
126
116
  ),
127
117
  });
128
118
  ```
129
119
 
130
120
  **What `schema.table()` does:**
131
- - Automatically injects `version: v.number()` (for CRDT versioning)
132
121
  - Automatically injects `timestamp: v.number()` (for incremental sync)
133
122
  - You only define your business logic fields
134
123
 
135
124
  **Required indexes:**
136
- - `by_user_id` on `['id']` - Enables fast document lookups during updates
125
+ - `by_doc_id` on `['id']` - Enables fast document lookups during updates
137
126
  - `by_timestamp` on `['timestamp']` - Enables efficient incremental synchronization
138
127
 
139
128
  ### Step 3: Create Replication Functions
@@ -151,77 +140,97 @@ const r = replicate(components.replicate);
151
140
  export const {
152
141
  stream,
153
142
  material,
143
+ recovery,
154
144
  insert,
155
145
  update,
156
146
  remove,
157
- versions
147
+ mark, // Peer sync progress tracking
148
+ compact, // Manual compaction trigger
158
149
  } = r<Task>({
159
150
  collection: 'tasks',
160
- compaction: { threshold: 5_000_000 }, // Optional: size threshold for auto-compaction (default: 5MB)
151
+ compaction: {
152
+ sizeThreshold: "5mb", // Type-safe size: "100kb", "5mb", "1gb"
153
+ peerTimeout: "24h", // Type-safe duration: "30m", "24h", "7d"
154
+ },
161
155
  });
162
156
  ```
163
157
 
164
158
  **What `replicate()` generates:**
165
159
 
166
- - `stream` - Real-time CRDT stream query (for client subscriptions)
160
+ - `stream` - Real-time CRDT stream query (cursor-based subscriptions with `seq` numbers)
167
161
  - `material` - SSR-friendly query (for server-side rendering)
162
+ - `recovery` - State vector sync query (for startup reconciliation)
168
163
  - `insert` - Dual-storage insert mutation (auto-compacts when threshold exceeded)
169
164
  - `update` - Dual-storage update mutation (auto-compacts when threshold exceeded)
170
165
  - `remove` - Dual-storage delete mutation (auto-compacts when threshold exceeded)
171
- - `versions` - Version history APIs (create, list, get, restore, remove)
166
+ - `mark` - Report sync progress to server (peer tracking for safe compaction)
167
+ - `compact` - Manual compaction trigger (peer-aware, respects active peer sync state)
172
168
 
173
- ### Step 4: Create a Custom Hook
169
+ ### Step 4: Define Your Collection
174
170
 
175
- Create a hook that wraps TanStack DB with Convex collection options:
171
+ Create a collection definition using `collection.create()`. This is SSR-safe because persistence and config are deferred until `init()` is called in the browser:
176
172
 
177
173
  ```typescript
178
- // src/useTasks.ts
179
- import { createCollection } from '@tanstack/react-db';
180
- import { convexCollectionOptions } from '@trestleinc/replicate/client';
181
- import { api } from '../convex/_generated/api';
182
- import { convexClient } from './router';
183
- import { useMemo } from 'react';
174
+ // src/collections/tasks.ts
175
+ import { collection, persistence } from '@trestleinc/replicate/client';
176
+ import { ConvexClient } from 'convex/browser';
177
+ import { api } from '../../convex/_generated/api';
178
+ import initSqlJs from 'sql.js';
179
+ import { z } from 'zod';
184
180
 
185
- export interface Task {
186
- id: string;
187
- text: string;
188
- isCompleted: boolean;
189
- }
181
+ // Define your Zod schema (required)
182
+ const taskSchema = z.object({
183
+ id: z.string(),
184
+ text: z.string(),
185
+ isCompleted: z.boolean(),
186
+ });
190
187
 
191
- // Module-level singleton to prevent multiple collection instances
192
- // This ensures only one sync process runs, even across component remounts
193
- let tasksCollection: ReturnType<typeof createCollection<Task>>;
194
-
195
- export function useTasks(
196
- initialData?: { documents: Task[], checkpoint?: any, count?: number, crdtBytes?: Uint8Array }
197
- ) {
198
- return useMemo(() => {
199
- if (!tasksCollection) {
200
- tasksCollection = createCollection(
201
- convexCollectionOptions<Task>({
202
- convexClient,
203
- api: api.tasks,
204
- collection: 'tasks',
205
- getKey: (task) => task.id,
206
- material: initialData,
207
- })
208
- );
209
- }
210
- return tasksCollection;
211
- }, [initialData]);
212
- }
188
+ export type Task = z.infer<typeof taskSchema>;
189
+
190
+ // Create lazy-initialized collection (SSR-safe)
191
+ export const tasks = collection.create({
192
+ // Async factory - only called in browser during init()
193
+ persistence: async () => {
194
+ const SQL = await initSqlJs({ locateFile: (f) => `/${f}` });
195
+ return persistence.sqlite.browser(SQL, 'tasks');
196
+ },
197
+ // Sync factory - only called in browser during init()
198
+ config: () => ({
199
+ schema: taskSchema,
200
+ convexClient: new ConvexClient(import.meta.env.VITE_CONVEX_URL),
201
+ api: api.tasks,
202
+ getKey: (task) => task.id,
203
+ }),
204
+ });
213
205
  ```
214
206
 
215
- ### Step 5: Use in Components
207
+ **Key points:**
208
+ - `collection.create()` returns a lazy collection that's safe to import during SSR
209
+ - `persistence` and `config` are factory functions, not values - they're only called during `init()`
210
+ - `schema` is required (Zod schema for type inference and prose field detection)
211
+ - Collection name is auto-extracted from `api.tasks` function path
212
+
213
+ ### Step 5: Initialize and Use in Components
214
+
215
+ Initialize the collection once in your app's entry point (browser only), then use it in components:
216
216
 
217
217
  ```typescript
218
- // src/routes/index.tsx
218
+ // src/routes/__root.tsx (or app entry point)
219
+ import { tasks } from '../collections/tasks';
220
+
221
+ // Initialize once during app startup (browser only)
222
+ // For SSR frameworks, do this in a client-side effect or loader
223
+ await tasks.init();
224
+ ```
225
+
226
+ ```typescript
227
+ // src/components/TaskList.tsx
219
228
  import { useLiveQuery } from '@tanstack/react-db';
220
- import { useTasks } from '../useTasks';
229
+ import { tasks, type Task } from '../collections/tasks';
221
230
 
222
231
  export function TaskList() {
223
- const collection = useTasks();
224
- const { data: tasks, isLoading, isError } = useLiveQuery(collection);
232
+ const collection = tasks.get();
233
+ const { data: taskList, isLoading, isError } = useLiveQuery(collection);
225
234
 
226
235
  const handleCreate = () => {
227
236
  collection.insert({
@@ -254,7 +263,7 @@ export function TaskList() {
254
263
  <div>
255
264
  <button onClick={handleCreate}>Add Task</button>
256
265
 
257
- {tasks.map((task) => (
266
+ {taskList.map((task) => (
258
267
  <div key={task.id}>
259
268
  <input
260
269
  type="checkbox"
@@ -270,9 +279,14 @@ export function TaskList() {
270
279
  }
271
280
  ```
272
281
 
282
+ **Lifecycle:**
283
+ 1. `collection.create()` - Define collection (module-level, SSR-safe)
284
+ 2. `await tasks.init()` - Initialize persistence and config (browser only, call once)
285
+ 3. `tasks.get()` - Get the TanStack DB collection instance (after init)
286
+
273
287
  ### Step 6: Server-Side Rendering (Recommended)
274
288
 
275
- For frameworks that support SSR (TanStack Start, Next.js, Remix, SvelteKit), preloading data on the server is the recommended approach for instant page loads and better SEO.
289
+ For frameworks that support SSR (TanStack Start, Next.js, Remix, SvelteKit), preloading data on the server enables instant page loads.
276
290
 
277
291
  **Why SSR is recommended:**
278
292
  - **Instant page loads** - No loading spinners on first render
@@ -280,40 +294,136 @@ For frameworks that support SSR (TanStack Start, Next.js, Remix, SvelteKit), pre
280
294
  - **Reduced client work** - Data already available on hydration
281
295
  - **Seamless transition** - Real-time sync takes over after hydration
282
296
 
283
- **Step 1: Use the `material` query from replicate()**
284
-
285
- The `material` query is automatically generated by `replicate()` and returns all documents for SSR hydration.
297
+ **Step 1: Prefetch material on the server**
286
298
 
287
- **Step 2: Load data in your route loader**
299
+ Use `ConvexHttpClient` to fetch data during SSR. The `material` query is generated by `replicate()`:
288
300
 
289
301
  ```typescript
290
- // src/routes/index.tsx
291
- import { createFileRoute } from '@tanstack/react-router';
302
+ // TanStack Start: src/routes/__root.tsx
303
+ import { createRootRoute } from '@tanstack/react-router';
292
304
  import { ConvexHttpClient } from 'convex/browser';
293
305
  import { api } from '../convex/_generated/api';
294
- import type { Task } from '../useTasks';
295
306
 
296
307
  const httpClient = new ConvexHttpClient(import.meta.env.VITE_CONVEX_URL);
297
308
 
298
- export const Route = createFileRoute('/')({
309
+ export const Route = createRootRoute({
299
310
  loader: async () => {
300
- const tasks = await httpClient.query(api.tasks.material);
301
- return { tasks };
311
+ const tasksMaterial = await httpClient.query(api.tasks.material);
312
+ return { tasksMaterial };
302
313
  },
303
314
  });
315
+ ```
304
316
 
305
- function TasksPage() {
306
- const { tasks: initialTasks } = Route.useLoaderData();
317
+ ```typescript
318
+ // SvelteKit: src/routes/+layout.server.ts
319
+ import { ConvexHttpClient } from 'convex/browser';
320
+ import { api } from '../convex/_generated/api';
321
+ import { PUBLIC_CONVEX_URL } from '$env/static/public';
307
322
 
308
- // Pass initialData to your hook - no loading state on first render!
309
- const collection = useTasks(initialTasks);
310
- const { data: tasks } = useLiveQuery(collection);
323
+ const httpClient = new ConvexHttpClient(PUBLIC_CONVEX_URL);
311
324
 
312
- return <TaskList tasks={tasks} />;
325
+ export async function load() {
326
+ const tasksMaterial = await httpClient.query(api.tasks.material);
327
+ return { tasksMaterial };
313
328
  }
314
329
  ```
315
330
 
316
- **Note:** If your framework doesn't support SSR, the collection works just fine without `initialData` - it will fetch data on mount and show a loading state.
331
+ **Step 2: Pass material to init() on the client**
332
+
333
+ ```typescript
334
+ // TanStack Start: src/routes/__root.tsx (client component)
335
+ import { tasks } from '../collections/tasks';
336
+
337
+ function RootComponent() {
338
+ const { tasksMaterial } = Route.useLoaderData();
339
+
340
+ useEffect(() => {
341
+ // Initialize with SSR data - no loading state!
342
+ tasks.init(tasksMaterial);
343
+ }, []);
344
+
345
+ return <Outlet />;
346
+ }
347
+ ```
348
+
349
+ ```svelte
350
+ <!-- SvelteKit: src/routes/+layout.svelte -->
351
+ <script lang="ts">
352
+ import { tasks } from '../collections/tasks';
353
+ import { onMount } from 'svelte';
354
+
355
+ export let data; // From +layout.server.ts
356
+
357
+ onMount(async () => {
358
+ await tasks.init(data.tasksMaterial);
359
+ });
360
+ </script>
361
+ ```
362
+
363
+ **Note:** If your framework doesn't support SSR, just call `await tasks.init()` without arguments - it will fetch data on mount and show a loading state.
364
+
365
+ ## Sync Protocol
366
+
367
+ Replicate uses cursor-based sync with peer tracking for safe compaction.
368
+
369
+ ### `stream` - Cursor-Based Real-Time Sync
370
+
371
+ The primary sync mechanism uses monotonically increasing sequence numbers (`seq`):
372
+
373
+ 1. Client subscribes with last known `cursor` (seq number)
374
+ 2. Server returns all changes with `seq > cursor`
375
+ 3. Client applies changes and updates local cursor
376
+ 4. Client calls `mark` to report sync progress to server
377
+ 5. Subscription stays open for live updates
378
+
379
+ This approach enables:
380
+ - **Safe compaction**: Server knows which deltas each peer has synced
381
+ - **Peer tracking**: Active peers are tracked via `mark` calls
382
+ - **No data loss**: Compaction only removes deltas all active peers have received
383
+
384
+ ### `mark` - Peer Sync Tracking
385
+
386
+ Clients report their sync progress to the server:
387
+
388
+ ```typescript
389
+ // Called automatically after applying changes
390
+ await convexClient.mutation(api.tasks.mark, {
391
+ peerId: "client-uuid",
392
+ syncedSeq: 42, // Last processed seq number
393
+ });
394
+ ```
395
+
396
+ The server tracks:
397
+ - Which peers are actively syncing
398
+ - Each peer's last synced `seq` number
399
+ - Peer timeout for cleanup (configurable via `peerTimeout`)
400
+
401
+ ### `compact` - Peer-Aware Compaction
402
+
403
+ Compaction is safe because it respects peer sync state:
404
+
405
+ 1. Server checks minimum `syncedSeq` across all active peers
406
+ 2. Only deletes deltas where `seq < minSyncedSeq`
407
+ 3. Ensures no active peer loses data they haven't synced
408
+
409
+ **Compaction triggers:**
410
+ - **Automatic**: When document deltas exceed `sizeThreshold`
411
+ - **Manual**: Via `compact` mutation
412
+
413
+ ### `recovery` - State Vector Sync
414
+
415
+ Used on startup to reconcile client and server state using Yjs state vectors:
416
+
417
+ 1. Client encodes its local Y.Doc state vector (compact representation of what it has)
418
+ 2. Server merges all snapshots + deltas into full state
419
+ 3. Server computes diff between its state and client's state vector
420
+ 4. Server returns only the missing bytes
421
+ 5. Client applies the diff to catch up
422
+
423
+ **When recovery is used:**
424
+ - App startup (before stream subscription begins)
425
+ - After extended offline periods
426
+ - When cursor-based sync can't satisfy the request (deltas compacted)
317
427
 
318
428
  ## Delete Pattern: Hard Delete with Event History
319
429
 
@@ -370,10 +480,12 @@ const r = replicate(components.replicate);
370
480
  export const {
371
481
  stream,
372
482
  material,
483
+ recovery,
373
484
  insert,
374
485
  update,
375
486
  remove,
376
- versions
487
+ mark,
488
+ compact,
377
489
  } = r<Task>({
378
490
  collection: 'tasks',
379
491
 
@@ -392,16 +504,22 @@ export const {
392
504
  const userId = await ctx.auth.getUserIdentity();
393
505
  if (!userId) throw new Error('Unauthorized');
394
506
  },
395
- evalVersion: async (ctx, collection, documentId) => { /* auth for versioning */ },
396
- evalRestore: async (ctx, collection, documentId, versionId) => { /* auth for restore */ },
507
+ evalMark: async (ctx, peerId) => {
508
+ // Validate peer identity
509
+ const userId = await ctx.auth.getUserIdentity();
510
+ if (!userId) throw new Error('Unauthorized');
511
+ },
512
+ evalCompact: async (ctx, documentId) => {
513
+ // Restrict compaction to admin users
514
+ const userId = await ctx.auth.getUserIdentity();
515
+ if (!userId) throw new Error('Unauthorized');
516
+ },
397
517
 
398
518
  // Lifecycle callbacks (on* hooks run AFTER execution)
399
519
  onStream: async (ctx, result) => { /* after stream query */ },
400
520
  onInsert: async (ctx, doc) => { /* after insert */ },
401
521
  onUpdate: async (ctx, doc) => { /* after update */ },
402
522
  onRemove: async (ctx, documentId) => { /* after remove */ },
403
- onVersion: async (ctx, result) => { /* after version created */ },
404
- onRestore: async (ctx, result) => { /* after restore */ },
405
523
 
406
524
  // Transform hook (modify documents before returning)
407
525
  transform: async (docs) => docs.filter(d => d.isPublic),
@@ -434,91 +552,84 @@ const plainText = prose.extract(notebook.content);
434
552
  const binding = await collection.utils.prose(notebookId, 'content');
435
553
  ```
436
554
 
437
- ### Version History
555
+ ### Persistence Providers
438
556
 
439
- Create and manage document version history:
557
+ Choose the right storage backend for your platform. Persistence is configured in the `persistence` factory of `collection.create()`:
440
558
 
441
559
  ```typescript
442
- // convex/tasks.ts
443
- export const { versions } = replicate<Task>({
444
- collection: 'tasks',
445
- });
560
+ import { collection, persistence } from '@trestleinc/replicate/client';
446
561
 
447
- // Create a version
448
- await ctx.runMutation(api.tasks.versions.create, {
449
- documentId: 'task-123',
450
- label: 'Before major edit',
451
- createdBy: 'user-456',
452
- });
453
-
454
- // List versions
455
- const versionList = await ctx.runQuery(api.tasks.versions.list, {
456
- documentId: 'task-123',
457
- limit: 10,
562
+ // Browser SQLite: Uses sql.js WASM with OPFS persistence
563
+ export const tasks = collection.create({
564
+ persistence: async () => {
565
+ const initSqlJs = (await import('sql.js')).default;
566
+ const SQL = await initSqlJs({ locateFile: (f) => `/${f}` });
567
+ return persistence.sqlite.browser(SQL, 'my-app-db');
568
+ },
569
+ config: () => ({ /* ... */ }),
458
570
  });
459
571
 
460
- // Get a specific version
461
- const version = await ctx.runQuery(api.tasks.versions.get, {
462
- versionId: 'version-789',
572
+ // React Native SQLite: Uses op-sqlite (native SQLite)
573
+ export const tasks = collection.create({
574
+ persistence: async () => {
575
+ const { open } = await import('@op-engineering/op-sqlite');
576
+ const db = open({ name: 'my-app-db' });
577
+ return persistence.sqlite.native(db, 'my-app-db');
578
+ },
579
+ config: () => ({ /* ... */ }),
463
580
  });
464
581
 
465
- // Restore a version
466
- await ctx.runMutation(api.tasks.versions.restore, {
467
- documentId: 'task-123',
468
- versionId: 'version-789',
469
- createBackup: true, // Optional: create backup before restore
582
+ // Testing: In-memory (no persistence)
583
+ export const tasks = collection.create({
584
+ persistence: async () => persistence.memory(),
585
+ config: () => ({ /* ... */ }),
470
586
  });
471
587
 
472
- // Delete a version
473
- await ctx.runMutation(api.tasks.versions.remove, {
474
- versionId: 'version-789',
588
+ // Custom backend: Implement StorageAdapter interface
589
+ export const tasks = collection.create({
590
+ persistence: async () => persistence.custom(new MyCustomAdapter()),
591
+ config: () => ({ /* ... */ }),
475
592
  });
476
593
  ```
477
594
 
478
- ### Persistence Providers
595
+ **SQLite Browser** - Uses sql.js (SQLite compiled to WASM) with OPFS persistence. You initialize sql.js yourself and pass the SQL object.
479
596
 
480
- Choose the right storage backend for your platform:
597
+ **SQLite Native** - Uses op-sqlite for React Native. You create the database and pass it.
481
598
 
482
- ```typescript
483
- import { persistence, adapters } from '@trestleinc/replicate/client';
599
+ **Memory** - No persistence, useful for testing.
484
600
 
485
- // Browser: IndexedDB (default, no config needed)
486
- convexCollectionOptions<Task>({
487
- // ... other options
488
- persistence: persistence.indexeddb(),
489
- });
601
+ **Custom** - Implement `StorageAdapter` for any storage backend.
490
602
 
491
- // Browser SQLite: Uses sql.js WASM with OPFS persistence
492
- // You initialize sql.js and pass the SQL object
493
- import initSqlJs from 'sql.js';
494
- const SQL = await initSqlJs({ locateFile: (file) => `/${file}` });
495
- convexCollectionOptions<Task>({
496
- // ... other options
497
- persistence: await persistence.sqlite.browser(SQL, 'my-app-db'),
498
- });
603
+ ### Custom Storage Backends
499
604
 
500
- // React Native SQLite: Uses op-sqlite (native SQLite)
501
- import { open } from '@op-engineering/op-sqlite';
502
- const db = open({ name: 'my-app-db' });
503
- convexCollectionOptions<Task>({
504
- // ... other options
505
- persistence: await persistence.sqlite.native(db, 'my-app-db'),
506
- });
605
+ Implement `StorageAdapter` for custom storage (Chrome extensions, localStorage, cloud storage):
507
606
 
508
- // Testing: In-memory (no persistence)
509
- convexCollectionOptions<Task>({
510
- // ... other options
511
- persistence: persistence.memory(),
512
- });
513
- ```
607
+ ```typescript
608
+ import { persistence, type StorageAdapter } from '@trestleinc/replicate/client';
514
609
 
515
- **IndexedDB** (default) - Uses y-indexeddb for Y.Doc persistence and browser-level for metadata. Browser only.
610
+ class ChromeStorageAdapter implements StorageAdapter {
611
+ async get(key: string): Promise<Uint8Array | undefined> {
612
+ const result = await chrome.storage.local.get(key);
613
+ return result[key] ? new Uint8Array(result[key]) : undefined;
614
+ }
516
615
 
517
- **SQLite Browser** - Uses sql.js (SQLite compiled to WASM) with OPFS persistence. You initialize sql.js yourself and pass the SQL object.
616
+ async set(key: string, value: Uint8Array): Promise<void> {
617
+ await chrome.storage.local.set({ [key]: Array.from(value) });
618
+ }
518
619
 
519
- **SQLite Native** - Uses op-sqlite for React Native. You create the database and pass it.
620
+ async delete(key: string): Promise<void> {
621
+ await chrome.storage.local.remove(key);
622
+ }
623
+
624
+ async keys(prefix: string): Promise<string[]> {
625
+ const all = await chrome.storage.local.get(null);
626
+ return Object.keys(all).filter(k => k.startsWith(prefix));
627
+ }
628
+ }
520
629
 
521
- **Memory** - No persistence, useful for testing without IndexedDB side effects.
630
+ // Use custom adapter
631
+ const chromePersistence = persistence.custom(new ChromeStorageAdapter());
632
+ ```
522
633
 
523
634
  ### Logging Configuration
524
635
 
@@ -544,42 +655,90 @@ await configure({
544
655
 
545
656
  ### Client-Side (`@trestleinc/replicate/client`)
546
657
 
547
- #### `convexCollectionOptions<T>(config)`
658
+ #### `collection.create({ persistence, config })`
548
659
 
549
- Creates collection options for TanStack DB with Yjs CRDT integration.
660
+ Creates a lazy-initialized collection with deferred persistence and config resolution. Both `persistence` and `config` are factory functions that are only called when `init()` is invoked (browser-only).
550
661
 
551
- **Config:**
662
+ **Parameters:**
663
+ - `persistence` - Async factory function that returns a `Persistence` instance
664
+ - `config` - Sync factory function that returns the collection config (ConvexClient, schema, api, etc.)
665
+
666
+ **Returns:** `LazyCollection` with `init(material?)` and `get()` methods
667
+
668
+ **Example:**
552
669
  ```typescript
553
- interface ConvexCollectionOptionsConfig<T> {
554
- convexClient: ConvexClient;
555
- api: {
556
- stream: FunctionReference; // Real-time subscription endpoint
670
+ import { collection, persistence } from '@trestleinc/replicate/client';
671
+ import { ConvexClient } from 'convex/browser';
672
+ import initSqlJs from 'sql.js';
673
+
674
+ export const tasks = collection.create({
675
+ persistence: async () => {
676
+ const SQL = await initSqlJs({ locateFile: (f) => `/${f}` });
677
+ return persistence.sqlite.browser(SQL, 'tasks');
678
+ },
679
+ config: () => ({
680
+ schema: taskSchema,
681
+ convexClient: new ConvexClient(import.meta.env.VITE_CONVEX_URL),
682
+ api: api.tasks,
683
+ getKey: (task) => task.id,
684
+ }),
685
+ });
686
+
687
+ // In your app initialization (browser only):
688
+ // Pass SSR-prefetched material for instant hydration
689
+ await tasks.init(material);
690
+ const collection = tasks.get();
691
+ ```
692
+
693
+ **SSR Prefetch (server-side):**
694
+ ```typescript
695
+ // SvelteKit: +layout.server.ts
696
+ import { ConvexHttpClient } from 'convex/browser';
697
+ const httpClient = new ConvexHttpClient(PUBLIC_CONVEX_URL);
698
+
699
+ export async function load() {
700
+ const material = await httpClient.query(api.tasks.material);
701
+ return { material };
702
+ }
703
+ ```
704
+
705
+ #### Collection Config Options
706
+
707
+ The `config` factory in `collection.create()` accepts these options:
708
+
709
+ ```typescript
710
+ interface CollectionConfig<T> {
711
+ schema: ZodObject; // Required: Zod schema for type inference
712
+ getKey: (item: T) => string | number; // Extract unique key from item
713
+ convexClient: ConvexClient; // Convex client instance
714
+ api: { // API from replicate()
715
+ stream: FunctionReference; // Real-time subscription
557
716
  insert: FunctionReference; // Insert mutation
558
717
  update: FunctionReference; // Update mutation
559
718
  remove: FunctionReference; // Delete mutation
719
+ recovery: FunctionReference; // State vector sync
720
+ mark: FunctionReference; // Peer sync tracking
721
+ compact: FunctionReference; // Manual compaction
722
+ material?: FunctionReference; // SSR hydration query
560
723
  };
561
- collection: string;
562
- getKey: (item: T) => string | number;
563
- persistence?: Persistence; // Optional: defaults to indexeddbPersistence()
564
- material?: Materialized<T>; // SSR hydration data
565
- prose?: Array<keyof T>; // Optional: prose fields for rich text
566
724
  undoCaptureTimeout?: number; // Undo stack merge window (default: 500ms)
567
725
  }
568
726
  ```
569
727
 
570
- **Returns:** Collection options for `createCollection()`
571
-
572
728
  **Example:**
573
729
  ```typescript
574
- const collection = createCollection(
575
- convexCollectionOptions<Task>({
576
- convexClient,
577
- api: api.tasks,
578
- collection: 'tasks',
730
+ export const tasks = collection.create({
731
+ persistence: async () => {
732
+ const SQL = await initSqlJs({ locateFile: (f) => `/${f}` });
733
+ return persistence.sqlite.browser(SQL, 'tasks');
734
+ },
735
+ config: () => ({
736
+ schema: taskSchema,
579
737
  getKey: (task) => task.id,
580
- material: initialData,
581
- })
582
- );
738
+ convexClient: new ConvexClient(import.meta.env.VITE_CONVEX_URL),
739
+ api: api.tasks,
740
+ }),
741
+ });
583
742
  ```
584
743
 
585
744
  #### `prose.extract(proseJson)`
@@ -601,35 +760,54 @@ const plainText = prose.extract(task.content);
601
760
  #### Persistence Providers
602
761
 
603
762
  ```typescript
604
- import { persistence, adapters } from '@trestleinc/replicate/client';
763
+ import { persistence, type StorageAdapter } from '@trestleinc/replicate/client';
605
764
 
606
- // Persistence providers
607
- persistence.indexeddb() // Browser: IndexedDB (default)
765
+ // Persistence providers (use in collection.create persistence factory)
608
766
  persistence.sqlite.browser(SQL, name) // Browser: sql.js WASM + OPFS
609
767
  persistence.sqlite.native(db, name) // React Native: op-sqlite
610
- persistence.memory() // Testing: in-memory (no persistence)
611
-
612
- // SQLite adapters (for advanced use)
613
- adapters.sqljs // SqlJsAdapter class for browser
614
- adapters.opsqlite // OPSqliteAdapter class for React Native
768
+ persistence.memory() // Testing: in-memory (no persistence)
769
+ persistence.custom(adapter) // Custom: your StorageAdapter implementation
615
770
  ```
616
771
 
617
- **`persistence.indexeddb()`** - Browser-only, uses y-indexeddb + browser-level.
618
-
619
772
  **`persistence.sqlite.browser(SQL, name)`** - Browser SQLite using sql.js WASM. You initialize sql.js and pass the SQL object.
620
773
 
621
774
  **`persistence.sqlite.native(db, name)`** - React Native SQLite using op-sqlite. You create the database and pass it.
622
775
 
623
776
  **`persistence.memory()`** - In-memory, no persistence. Useful for testing.
624
777
 
778
+ **`persistence.custom(adapter)`** - Custom storage backend. Pass your `StorageAdapter` implementation.
779
+
780
+ #### `StorageAdapter` Interface
781
+
782
+ Implement for custom storage backends:
783
+
784
+ ```typescript
785
+ interface StorageAdapter {
786
+ /** Get value by key, returns undefined if not found */
787
+ get(key: string): Promise<Uint8Array | undefined>;
788
+
789
+ /** Set value by key */
790
+ set(key: string, value: Uint8Array): Promise<void>;
791
+
792
+ /** Delete value by key */
793
+ delete(key: string): Promise<void>;
794
+
795
+ /** List all keys matching prefix */
796
+ keys(prefix: string): Promise<string[]>;
797
+
798
+ /** Optional: cleanup when persistence is destroyed */
799
+ close?(): void;
800
+ }
801
+ ```
802
+
625
803
  #### Error Classes
626
804
 
627
805
  ```typescript
628
806
  import { errors } from '@trestleinc/replicate/client';
629
807
 
630
808
  errors.Network // Network-related failures
631
- errors.IDB // IndexedDB read errors
632
- errors.IDBWrite // IndexedDB write errors
809
+ errors.IDB // Storage read errors
810
+ errors.IDBWrite // Storage write errors
633
811
  errors.Reconciliation // Phantom document cleanup errors
634
812
  errors.Prose // Rich text field errors
635
813
  errors.CollectionNotReady// Collection not initialized
@@ -665,9 +843,10 @@ Configuration for the bound replicate function.
665
843
  interface ReplicateConfig<T> {
666
844
  collection: string; // Collection name (e.g., 'tasks')
667
845
 
668
- // Optional: Auto-compaction settings
846
+ // Optional: Compaction settings with type-safe values
669
847
  compaction?: {
670
- threshold?: number; // Size threshold in bytes (default: 5MB / 5_000_000)
848
+ sizeThreshold?: Size; // Size threshold: "100kb", "5mb", "1gb" (default: "5mb")
849
+ peerTimeout?: Duration; // Peer timeout: "30m", "24h", "7d" (default: "24h")
671
850
  };
672
851
 
673
852
  // Optional: Hooks for permissions and lifecycle
@@ -676,16 +855,14 @@ interface ReplicateConfig<T> {
676
855
  evalRead?: (ctx, collection) => Promise<void>;
677
856
  evalWrite?: (ctx, doc) => Promise<void>;
678
857
  evalRemove?: (ctx, documentId) => Promise<void>;
679
- evalVersion?: (ctx, collection, documentId) => Promise<void>;
680
- evalRestore?: (ctx, collection, documentId, versionId) => Promise<void>;
858
+ evalMark?: (ctx, peerId) => Promise<void>;
859
+ evalCompact?: (ctx, documentId) => Promise<void>;
681
860
 
682
861
  // Lifecycle callbacks (run after operation)
683
862
  onStream?: (ctx, result) => Promise<void>;
684
863
  onInsert?: (ctx, doc) => Promise<void>;
685
864
  onUpdate?: (ctx, doc) => Promise<void>;
686
865
  onRemove?: (ctx, documentId) => Promise<void>;
687
- onVersion?: (ctx, result) => Promise<void>;
688
- onRestore?: (ctx, result) => Promise<void>;
689
866
 
690
867
  // Transform hook (modify documents before returning)
691
868
  transform?: (docs) => Promise<T[]>;
@@ -693,17 +870,23 @@ interface ReplicateConfig<T> {
693
870
  }
694
871
  ```
695
872
 
873
+ **Type-safe values:**
874
+ - `Size`: `"100kb"`, `"5mb"`, `"1gb"`, etc.
875
+ - `Duration`: `"30m"`, `"24h"`, `"7d"`, etc.
876
+
696
877
  **Returns:** Object with generated functions:
697
- - `stream` - Real-time CRDT stream query
878
+ - `stream` - Real-time CRDT stream query (cursor-based with `seq` numbers)
698
879
  - `material` - SSR-friendly query for hydration
880
+ - `recovery` - State vector sync query (for startup reconciliation)
699
881
  - `insert` - Dual-storage insert mutation (auto-compacts when threshold exceeded)
700
882
  - `update` - Dual-storage update mutation (auto-compacts when threshold exceeded)
701
883
  - `remove` - Dual-storage delete mutation (auto-compacts when threshold exceeded)
702
- - `versions` - Version history APIs (create, list, get, restore, remove)
884
+ - `mark` - Peer sync tracking mutation (reports `syncedSeq` to server)
885
+ - `compact` - Manual compaction mutation (peer-aware, safe for active clients)
703
886
 
704
887
  #### `schema.table(userFields, applyIndexes?)`
705
888
 
706
- Automatically inject replication metadata fields (`version`, `timestamp`).
889
+ Automatically inject `timestamp` field for incremental sync.
707
890
 
708
891
  **Parameters:**
709
892
  - `userFields` - User's business logic fields
@@ -721,7 +904,7 @@ tasks: schema.table(
721
904
  text: v.string(),
722
905
  },
723
906
  (t) => t
724
- .index('by_user_id', ['id'])
907
+ .index('by_doc_id', ['id'])
725
908
  .index('by_timestamp', ['timestamp'])
726
909
  )
727
910
  ```
@@ -737,43 +920,38 @@ Validator for ProseMirror-compatible JSON fields.
737
920
  content: schema.prose() // Validates ProseMirror JSON structure
738
921
  ```
739
922
 
740
- ## Performance
741
-
742
- ### Storage Performance
923
+ ### Shared Types (`@trestleinc/replicate/shared`)
743
924
 
744
- - **Swappable persistence** - IndexedDB (browser), SQLite (React Native), or in-memory (testing)
745
- - **Yjs** CRDT operations are extremely fast (96% smaller than Automerge)
746
- - **TanStack DB** provides optimistic updates and reactive state management
747
- - **Indexed queries** in Convex for fast incremental sync
925
+ ```typescript
926
+ import type { ProseValue } from '@trestleinc/replicate/shared';
748
927
 
749
- ### Sync Performance
928
+ // ProseValue - branded type for prose fields in Zod schemas
929
+ // Use the prose() helper from client to create fields of this type
930
+ ```
750
931
 
751
- - **Real-time updates** - WebSocket-based change notifications
752
- - **Delta encoding** - Only send what changed (< 1KB per change vs 100KB+ full state)
753
- - **Event sourcing** - Append-only writes, no update conflicts
754
- - **Optimistic UI** - Instant updates without waiting for server
932
+ ## React Native
755
933
 
756
- ### Multi-Tab Sync
934
+ React Native doesn't include the Web Crypto API by default. Install these polyfills:
757
935
 
758
- - **TanStack coordination** - Built-in multi-tab sync via BroadcastChannel
759
- - **Yjs shared state** - Single source of truth per browser
760
- - **Leader election** - Only one tab runs sync operations
936
+ ```bash
937
+ npm install react-native-get-random-values react-native-random-uuid
938
+ ```
761
939
 
762
- ## Offline Behavior
940
+ Import them at the **very top** of your app's entry point (before any other imports):
763
941
 
764
- ### How It Works
942
+ ```javascript
943
+ // index.js or app/_layout.tsx - MUST be first!
944
+ import "react-native-get-random-values";
945
+ import "react-native-random-uuid";
765
946
 
766
- - **Writes** - Queue locally in Yjs CRDT, sync when online
767
- - **Reads** - Always work from local TanStack DB cache (instant!)
768
- - **UI** - Fully functional with optimistic updates
769
- - **Conflicts** - Auto-resolved by Yjs CRDTs (conflict-free!)
947
+ // Then your other imports...
948
+ ```
770
949
 
771
- ### Network Resilience
950
+ This provides:
951
+ - `crypto.getRandomValues()` - Required by Yjs for CRDT operations
952
+ - `crypto.randomUUID()` - Used for generating document and peer IDs
772
953
 
773
- - Automatic retry with exponential backoff
774
- - Network error detection (fetch errors, connection issues)
775
- - Queue changes while offline
776
- - Graceful degradation
954
+ See [`examples/expo/`](./examples/expo/) for a complete React Native example using Expo.
777
955
 
778
956
  ## Examples
779
957
 
@@ -781,21 +959,29 @@ content: schema.prose() // Validates ProseMirror JSON structure
781
959
 
782
960
  A full-featured offline-first issue tracker built with Replicate, demonstrating real-world usage patterns.
783
961
 
784
- 🔗 **Live Demo:** [interval.robelest.com](https://interval.robelest.com)
962
+ **Live Demo:** [interval.robelest.com](https://interval.robelest.com)
785
963
 
786
- 📦 **Source Code:** [github.com/robelest/interval](https://github.com/robelest/interval)
964
+ **Source Code:** Available in three framework variants:
965
+ - [`examples/tanstack-start/`](./examples/tanstack-start/) - TanStack Start (React, web)
966
+ - [`examples/sveltekit/`](./examples/sveltekit/) - SvelteKit (Svelte, web)
967
+ - [`examples/expo/`](./examples/expo/) - Expo (React Native, mobile)
787
968
 
788
- **Features demonstrated:**
969
+ **Web features demonstrated:**
789
970
  - Offline-first with SQLite persistence (sql.js + OPFS)
790
971
  - Rich text editing with TipTap + Yjs collaboration
791
972
  - PWA with custom service worker
792
973
  - Real-time sync across devices
793
974
  - Search with client-side text extraction (`prose.extract()`)
794
975
 
976
+ **Mobile features demonstrated (Expo):**
977
+ - Native SQLite persistence (op-sqlite)
978
+ - Plain TextInput prose binding via `useProseField` hook
979
+ - Crypto polyfills for React Native
980
+
795
981
  ## Development
796
982
 
797
983
  ```bash
798
- bun run build # Build with Rslib (includes ESLint + TypeScript checking)
984
+ bun run build # Build with tsdown (includes ESLint + TypeScript checking)
799
985
  bun run dev # Watch mode
800
986
  bun run clean # Remove build artifacts
801
987
  ```