@trestleinc/replicate 1.1.2 → 1.2.0-preview.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.
Files changed (39) hide show
  1. package/README.md +40 -41
  2. package/dist/client/index.d.ts +34 -26
  3. package/dist/client/index.js +904 -732
  4. package/dist/component/_generated/api.d.ts +2 -2
  5. package/dist/component/_generated/component.d.ts +84 -27
  6. package/dist/component/convex.config.d.ts +2 -2
  7. package/dist/component/mutations.d.ts +131 -0
  8. package/dist/component/mutations.js +493 -0
  9. package/dist/component/schema.d.ts +71 -31
  10. package/dist/component/schema.js +37 -14
  11. package/dist/server/index.d.ts +58 -47
  12. package/dist/server/index.js +227 -132
  13. package/package.json +3 -1
  14. package/src/client/collection.ts +334 -523
  15. package/src/client/errors.ts +1 -1
  16. package/src/client/index.ts +4 -7
  17. package/src/client/merge.ts +2 -2
  18. package/src/client/persistence/indexeddb.ts +10 -14
  19. package/src/client/prose.ts +147 -203
  20. package/src/client/services/awareness.ts +373 -0
  21. package/src/client/services/context.ts +114 -0
  22. package/src/client/services/seq.ts +78 -0
  23. package/src/client/services/session.ts +20 -0
  24. package/src/client/services/sync.ts +122 -0
  25. package/src/client/subdocs.ts +263 -0
  26. package/src/component/_generated/api.ts +2 -2
  27. package/src/component/_generated/component.ts +73 -28
  28. package/src/component/mutations.ts +734 -0
  29. package/src/component/schema.ts +31 -14
  30. package/src/server/collection.ts +98 -0
  31. package/src/server/index.ts +2 -2
  32. package/src/server/{storage.ts → replicate.ts} +214 -75
  33. package/dist/component/public.d.ts +0 -83
  34. package/dist/component/public.js +0 -325
  35. package/src/client/prose-schema.ts +0 -55
  36. package/src/client/services/cursor.ts +0 -109
  37. package/src/component/public.ts +0 -453
  38. package/src/server/builder.ts +0 -98
  39. /package/src/client/{replicate.ts → ops.ts} +0 -0
package/README.md CHANGED
@@ -127,16 +127,14 @@ export default defineSchema({
127
127
 
128
128
  ### Step 3: Create Replication Functions
129
129
 
130
- Use `replicate()` to bind your component and create collection functions:
130
+ Use `collection.create()` to create server-side collection functions:
131
131
 
132
132
  ```typescript
133
133
  // convex/tasks.ts
134
- import { replicate } from '@trestleinc/replicate/server';
134
+ import { collection } from '@trestleinc/replicate/server';
135
135
  import { components } from './_generated/api';
136
136
  import type { Task } from '../src/useTasks';
137
137
 
138
- const r = replicate(components.replicate);
139
-
140
138
  export const {
141
139
  stream,
142
140
  material,
@@ -144,18 +142,15 @@ export const {
144
142
  insert,
145
143
  update,
146
144
  remove,
147
- mark, // Peer sync progress tracking
148
- compact, // Manual compaction trigger
149
- } = r<Task>({
150
- collection: 'tasks',
151
- compaction: {
152
- sizeThreshold: "5mb", // Type-safe size: "100kb", "5mb", "1gb"
153
- peerTimeout: "24h", // Type-safe duration: "30m", "24h", "7d"
154
- },
155
- });
145
+ mark,
146
+ compact,
147
+ sessions,
148
+ cursors,
149
+ leave,
150
+ } = collection.create<Task>(components.replicate, 'tasks');
156
151
  ```
157
152
 
158
- **What `replicate()` generates:**
153
+ **What `collection.create()` generates:**
159
154
 
160
155
  - `stream` - Real-time CRDT stream query (cursor-based subscriptions with `seq` numbers)
161
156
  - `material` - SSR-friendly query (for server-side rendering)
@@ -165,6 +160,9 @@ export const {
165
160
  - `remove` - Dual-storage delete mutation (auto-compacts when threshold exceeded)
166
161
  - `mark` - Report sync progress to server (peer tracking for safe compaction)
167
162
  - `compact` - Manual compaction trigger (peer-aware, respects active peer sync state)
163
+ - `sessions` - Get connected sessions (presence query)
164
+ - `cursors` - Get cursor positions for collaborative editing
165
+ - `leave` - Explicit disconnect mutation
168
166
 
169
167
  ### Step 4: Define Your Collection
170
168
 
@@ -296,7 +294,7 @@ For frameworks that support SSR (TanStack Start, Next.js, Remix, SvelteKit), pre
296
294
 
297
295
  **Step 1: Prefetch material on the server**
298
296
 
299
- Use `ConvexHttpClient` to fetch data during SSR. The `material` query is generated by `replicate()`:
297
+ Use `ConvexHttpClient` to fetch data during SSR. The `material` query is generated by `collection.create()`:
300
298
 
301
299
  ```typescript
302
300
  // TanStack Start: src/routes/__root.tsx
@@ -471,12 +469,10 @@ You can customize the behavior of generated functions using optional hooks:
471
469
 
472
470
  ```typescript
473
471
  // convex/tasks.ts
474
- import { replicate } from '@trestleinc/replicate/server';
472
+ import { collection } from '@trestleinc/replicate/server';
475
473
  import { components } from './_generated/api';
476
474
  import type { Task } from '../src/useTasks';
477
475
 
478
- const r = replicate(components.replicate);
479
-
480
476
  export const {
481
477
  stream,
482
478
  material,
@@ -486,9 +482,10 @@ export const {
486
482
  remove,
487
483
  mark,
488
484
  compact,
489
- } = r<Task>({
490
- collection: 'tasks',
491
-
485
+ sessions,
486
+ cursors,
487
+ leave,
488
+ } = collection.create<Task>(components.replicate, 'tasks', {
492
489
  // Optional hooks for authorization and lifecycle events
493
490
  hooks: {
494
491
  // Permission checks (eval* hooks validate BEFORE execution, throw to deny)
@@ -500,7 +497,7 @@ export const {
500
497
  const userId = await ctx.auth.getUserIdentity();
501
498
  if (!userId) throw new Error('Unauthorized');
502
499
  },
503
- evalRemove: async (ctx, documentId) => {
500
+ evalRemove: async (ctx, document) => {
504
501
  const userId = await ctx.auth.getUserIdentity();
505
502
  if (!userId) throw new Error('Unauthorized');
506
503
  },
@@ -509,7 +506,7 @@ export const {
509
506
  const userId = await ctx.auth.getUserIdentity();
510
507
  if (!userId) throw new Error('Unauthorized');
511
508
  },
512
- evalCompact: async (ctx, documentId) => {
509
+ evalCompact: async (ctx, document) => {
513
510
  // Restrict compaction to admin users
514
511
  const userId = await ctx.auth.getUserIdentity();
515
512
  if (!userId) throw new Error('Unauthorized');
@@ -519,7 +516,7 @@ export const {
519
516
  onStream: async (ctx, result) => { /* after stream query */ },
520
517
  onInsert: async (ctx, doc) => { /* after insert */ },
521
518
  onUpdate: async (ctx, doc) => { /* after update */ },
522
- onRemove: async (ctx, documentId) => { /* after remove */ },
519
+ onRemove: async (ctx, document) => { /* after remove */ },
523
520
 
524
521
  // Transform hook (modify documents before returning)
525
522
  transform: async (docs) => docs.filter(d => d.isPublic),
@@ -711,7 +708,7 @@ interface CollectionConfig<T> {
711
708
  schema: ZodObject; // Required: Zod schema for type inference
712
709
  getKey: (item: T) => string | number; // Extract unique key from item
713
710
  convexClient: ConvexClient; // Convex client instance
714
- api: { // API from replicate()
711
+ api: { // API from collection.create()
715
712
  stream: FunctionReference; // Real-time subscription
716
713
  insert: FunctionReference; // Insert mutation
717
714
  update: FunctionReference; // Update mutation
@@ -816,34 +813,33 @@ errors.NonRetriable // Errors that should not be retried (auth, validation)
816
813
 
817
814
  ### Server-Side (`@trestleinc/replicate/server`)
818
815
 
819
- #### `replicate(component)`
816
+ #### `collection.create<T>(component, name, options?)`
820
817
 
821
- Factory function that creates a bound replicate function for your collections.
818
+ Creates server-side collection functions that mirror the client-side collection.
822
819
 
823
820
  **Parameters:**
824
821
  - `component` - Your Convex component reference (`components.replicate`)
825
-
826
- **Returns:** A function `<T>(config: ReplicateConfig<T>)` that generates collection operations.
822
+ - `name` - Collection name (e.g., `'tasks'`)
823
+ - `options` - Optional configuration for compaction and hooks
827
824
 
828
825
  **Example:**
829
826
  ```typescript
830
- import { replicate } from '@trestleinc/replicate/server';
827
+ import { collection } from '@trestleinc/replicate/server';
831
828
  import { components } from './_generated/api';
832
829
 
833
- const r = replicate(components.replicate);
834
- export const tasks = r<Task>({ collection: 'tasks' });
830
+ export const {
831
+ stream, material, insert, update, remove, recovery, mark, compact, sessions, cursors, leave,
832
+ } = collection.create<Task>(components.replicate, 'tasks');
835
833
  ```
836
834
 
837
- #### `ReplicateConfig<T>`
835
+ #### `CollectionOptions<T>`
838
836
 
839
- Configuration for the bound replicate function.
837
+ Optional configuration for `collection.create()`.
840
838
 
841
839
  **Config:**
842
840
  ```typescript
843
- interface ReplicateConfig<T> {
844
- collection: string; // Collection name (e.g., 'tasks')
845
-
846
- // Optional: Compaction settings with type-safe values
841
+ interface CollectionOptions<T> {
842
+ // Optional: Compaction settings
847
843
  compaction?: {
848
844
  sizeThreshold?: Size; // Size threshold: "100kb", "5mb", "1gb" (default: "5mb")
849
845
  peerTimeout?: Duration; // Peer timeout: "30m", "24h", "7d" (default: "24h")
@@ -854,15 +850,15 @@ interface ReplicateConfig<T> {
854
850
  // Permission checks (throw to reject)
855
851
  evalRead?: (ctx, collection) => Promise<void>;
856
852
  evalWrite?: (ctx, doc) => Promise<void>;
857
- evalRemove?: (ctx, documentId) => Promise<void>;
853
+ evalRemove?: (ctx, document) => Promise<void>;
858
854
  evalMark?: (ctx, peerId) => Promise<void>;
859
- evalCompact?: (ctx, documentId) => Promise<void>;
855
+ evalCompact?: (ctx, document) => Promise<void>;
860
856
 
861
857
  // Lifecycle callbacks (run after operation)
862
858
  onStream?: (ctx, result) => Promise<void>;
863
859
  onInsert?: (ctx, doc) => Promise<void>;
864
860
  onUpdate?: (ctx, doc) => Promise<void>;
865
- onRemove?: (ctx, documentId) => Promise<void>;
861
+ onRemove?: (ctx, document) => Promise<void>;
866
862
 
867
863
  // Transform hook (modify documents before returning)
868
864
  transform?: (docs) => Promise<T[]>;
@@ -883,6 +879,9 @@ interface ReplicateConfig<T> {
883
879
  - `remove` - Dual-storage delete mutation (auto-compacts when threshold exceeded)
884
880
  - `mark` - Peer sync tracking mutation (reports `syncedSeq` to server)
885
881
  - `compact` - Manual compaction mutation (peer-aware, safe for active clients)
882
+ - `sessions` - Get connected sessions (presence query)
883
+ - `cursors` - Get cursor positions for collaborative editing
884
+ - `leave` - Explicit disconnect mutation
886
885
 
887
886
  #### `schema.table(userFields, applyIndexes?)`
888
887
 
@@ -2,6 +2,7 @@ import * as Y from "yjs";
2
2
  import { FunctionReference } from "convex/server";
3
3
  import { BaseCollectionConfig, Collection, NonSingleResult } from "@tanstack/db";
4
4
  import { Context, Effect, Layer } from "effect";
5
+ import { Awareness } from "y-protocols/awareness";
5
6
  import { ConvexClient } from "convex/browser";
6
7
  import { StandardSchemaV1 } from "@standard-schema/spec";
7
8
  import * as effect_Types0 from "effect/Types";
@@ -97,7 +98,7 @@ declare const ProseError_base: new <A extends Record<string, any> = {}>(args: ef
97
98
  readonly _tag: "ProseError";
98
99
  } & Readonly<A>;
99
100
  declare class ProseError extends ProseError_base<{
100
- readonly documentId: string;
101
+ readonly document: string;
101
102
  readonly field: string;
102
103
  readonly collection: string;
103
104
  }> {}
@@ -113,8 +114,8 @@ declare class NonRetriableError extends Error {
113
114
  constructor(message: string);
114
115
  }
115
116
  //#endregion
116
- //#region src/client/services/cursor.d.ts
117
- type Cursor = number;
117
+ //#region src/client/services/seq.d.ts
118
+ type Seq = number;
118
119
  //#endregion
119
120
  //#region src/shared/types.d.ts
120
121
  /** ProseMirror-compatible JSON for XmlFragment serialization */
@@ -152,9 +153,12 @@ type ProseFields<T> = { [K in keyof T]: T[K] extends ProseValue ? K : never }[ke
152
153
  /** Server-rendered material data for SSR hydration */
153
154
  interface Materialized<T> {
154
155
  documents: readonly T[];
155
- cursor?: Cursor;
156
+ cursor?: Seq;
156
157
  count?: number;
157
- crdtBytes?: ArrayBuffer;
158
+ crdt?: Record<string, {
159
+ bytes: ArrayBuffer;
160
+ seq: number;
161
+ }>;
158
162
  }
159
163
  /** API object from replicate() */
160
164
  interface ConvexCollectionApi {
@@ -166,6 +170,9 @@ interface ConvexCollectionApi {
166
170
  mark: FunctionReference<"mutation">;
167
171
  compact: FunctionReference<"mutation">;
168
172
  material?: FunctionReference<"query">;
173
+ sessions?: FunctionReference<"query">;
174
+ cursors?: FunctionReference<"query">;
175
+ leave?: FunctionReference<"mutation">;
169
176
  }
170
177
  interface ConvexCollectionConfig<T extends object = object, TSchema extends StandardSchemaV1 = never, TKey extends string | number = string | number> extends BaseCollectionConfig<T, TKey, TSchema> {
171
178
  schema: TSchema;
@@ -173,39 +180,42 @@ interface ConvexCollectionConfig<T extends object = object, TSchema extends Stan
173
180
  api: ConvexCollectionApi;
174
181
  persistence: Persistence;
175
182
  material?: Materialized<T>;
176
- undoCaptureTimeout?: number;
177
183
  }
178
- /** Editor binding for BlockNote/TipTap collaboration */
184
+ /**
185
+ * Binding returned by collection.utils.prose() for collaborative editing.
186
+ *
187
+ * Compatible with TipTap's Collaboration/CollaborationCursor and BlockNote's
188
+ * collaboration config. The editor handles undo/redo internally via y-prosemirror.
189
+ */
179
190
  interface EditorBinding {
180
- /** The Y.XmlFragment bound to the editor */
191
+ /** Yjs XmlFragment for content sync */
181
192
  readonly fragment: Y.XmlFragment;
182
- /** Provider stub for BlockNote compatibility */
193
+ /**
194
+ * Provider with Yjs Awareness for cursor/presence sync.
195
+ * Pass to CollaborationCursor.configure({ provider: binding.provider })
196
+ * or BlockNote's collaboration.provider
197
+ */
183
198
  readonly provider: {
184
- readonly awareness: null;
199
+ readonly awareness: Awareness;
200
+ readonly document: Y.Doc;
185
201
  };
186
- /** Current sync state - true if unsent changes exist */
202
+ /** Whether there are unsaved local changes */
187
203
  readonly pending: boolean;
188
- /** Subscribe to pending state changes. Returns unsubscribe function. */
204
+ /** Subscribe to pending state changes */
189
205
  onPendingChange(callback: (pending: boolean) => void): () => void;
190
- /** Undo the last content edit */
191
- undo(): void;
192
- /** Redo the last undone edit */
193
- redo(): void;
194
- /** Check if undo is available */
195
- canUndo(): boolean;
196
- /** Check if redo is available */
197
- canRedo(): boolean;
206
+ /** Cleanup - call when unmounting editor */
207
+ destroy(): void;
198
208
  }
199
209
  /** Utilities exposed on collection.utils */
200
210
  interface ConvexCollectionUtils<T extends object> {
201
211
  /**
202
212
  * Get an editor binding for a prose field.
203
213
  * Waits for Y.Doc to be ready (IndexedDB loaded) before returning.
204
- * @param documentId - The document ID
214
+ * @param document - The document ID
205
215
  * @param field - The prose field name (must be in `prose` config)
206
216
  * @returns Promise resolving to EditorBinding
207
217
  */
208
- prose(documentId: string, field: ProseFields<T>): Promise<EditorBinding>;
218
+ prose(document: string, field: ProseFields<T>): Promise<EditorBinding>;
209
219
  }
210
220
  type LazyCollectionConfig<TSchema extends z.ZodObject<z.ZodRawShape>> = Omit<ConvexCollectionConfig<z.infer<TSchema>, TSchema, string>, "persistence" | "material">;
211
221
  interface LazyCollection<T extends object> {
@@ -228,7 +238,7 @@ declare const collection: {
228
238
  */
229
239
  declare function extract(content: unknown): string;
230
240
  //#endregion
231
- //#region src/client/prose-schema.d.ts
241
+ //#region src/client/prose.d.ts
232
242
  declare function emptyProse(): ProseValue;
233
243
  declare function prose$1(): z.ZodType<ProseValue>;
234
244
  declare namespace prose$1 {
@@ -305,10 +315,8 @@ declare const errors: {
305
315
  readonly CollectionNotReady: typeof CollectionNotReadyError;
306
316
  readonly NonRetriable: typeof NonRetriableError;
307
317
  };
308
- declare function empty(): ProseValue;
309
318
  declare const prose: typeof prose$1 & {
310
319
  extract: typeof extract;
311
- empty: typeof empty;
312
320
  };
313
321
  //#endregion
314
- export { type ConvexCollection, type EditorBinding, type Materialized, type Persistence, type StorageAdapter, collection, errors, persistence, prose };
322
+ export { type ConvexCollection, type EditorBinding, type Materialized, type Persistence, type Seq, type StorageAdapter, collection, errors, persistence, prose };