@trestleinc/replicate 0.1.0 → 1.1.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 (94) hide show
  1. package/README.md +356 -420
  2. package/dist/client/collection.d.ts +78 -76
  3. package/dist/client/errors.d.ts +59 -0
  4. package/dist/client/index.d.ts +22 -18
  5. package/dist/client/logger.d.ts +0 -1
  6. package/dist/client/merge.d.ts +77 -0
  7. package/dist/client/persistence/adapters/index.d.ts +8 -0
  8. package/dist/client/persistence/adapters/opsqlite.d.ts +46 -0
  9. package/dist/client/persistence/adapters/sqljs.d.ts +83 -0
  10. package/dist/client/persistence/index.d.ts +49 -0
  11. package/dist/client/persistence/indexeddb.d.ts +17 -0
  12. package/dist/client/persistence/memory.d.ts +16 -0
  13. package/dist/client/persistence/sqlite-browser.d.ts +51 -0
  14. package/dist/client/persistence/sqlite-level.d.ts +63 -0
  15. package/dist/client/persistence/sqlite-rn.d.ts +36 -0
  16. package/dist/client/persistence/sqlite.d.ts +47 -0
  17. package/dist/client/persistence/types.d.ts +42 -0
  18. package/dist/client/prose.d.ts +56 -0
  19. package/dist/client/replicate.d.ts +40 -0
  20. package/dist/client/services/checkpoint.d.ts +18 -0
  21. package/dist/client/services/reconciliation.d.ts +24 -0
  22. package/dist/component/_generated/api.d.ts +35 -0
  23. package/dist/component/_generated/api.js +3 -3
  24. package/dist/component/_generated/component.d.ts +89 -0
  25. package/dist/component/_generated/component.js +0 -0
  26. package/dist/component/_generated/dataModel.d.ts +45 -0
  27. package/dist/component/_generated/dataModel.js +0 -0
  28. package/{src → dist}/component/_generated/server.d.ts +9 -38
  29. package/dist/component/convex.config.d.ts +2 -2
  30. package/dist/component/convex.config.js +2 -1
  31. package/dist/component/logger.d.ts +8 -0
  32. package/dist/component/logger.js +30 -0
  33. package/dist/component/public.d.ts +36 -61
  34. package/dist/component/public.js +232 -58
  35. package/dist/component/schema.d.ts +32 -8
  36. package/dist/component/schema.js +19 -6
  37. package/dist/index.js +1553 -308
  38. package/dist/server/builder.d.ts +94 -0
  39. package/dist/server/index.d.ts +14 -17
  40. package/dist/server/schema.d.ts +17 -63
  41. package/dist/server/storage.d.ts +80 -0
  42. package/dist/server.js +268 -83
  43. package/dist/shared/index.d.ts +5 -0
  44. package/dist/shared/index.js +2 -0
  45. package/dist/shared/types.d.ts +50 -0
  46. package/dist/shared/types.js +6 -0
  47. package/dist/shared.js +6 -0
  48. package/package.json +59 -49
  49. package/src/client/collection.ts +877 -450
  50. package/src/client/errors.ts +45 -0
  51. package/src/client/index.ts +52 -26
  52. package/src/client/logger.ts +2 -28
  53. package/src/client/merge.ts +374 -0
  54. package/src/client/persistence/adapters/index.ts +8 -0
  55. package/src/client/persistence/adapters/opsqlite.ts +54 -0
  56. package/src/client/persistence/adapters/sqljs.ts +128 -0
  57. package/src/client/persistence/index.ts +54 -0
  58. package/src/client/persistence/indexeddb.ts +110 -0
  59. package/src/client/persistence/memory.ts +61 -0
  60. package/src/client/persistence/sqlite-browser.ts +107 -0
  61. package/src/client/persistence/sqlite-level.ts +407 -0
  62. package/src/client/persistence/sqlite-rn.ts +44 -0
  63. package/src/client/persistence/sqlite.ts +161 -0
  64. package/src/client/persistence/types.ts +49 -0
  65. package/src/client/prose.ts +369 -0
  66. package/src/client/replicate.ts +80 -0
  67. package/src/client/services/checkpoint.ts +86 -0
  68. package/src/client/services/reconciliation.ts +108 -0
  69. package/src/component/_generated/api.ts +52 -0
  70. package/src/component/_generated/component.ts +103 -0
  71. package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +1 -1
  72. package/src/component/_generated/server.ts +161 -0
  73. package/src/component/convex.config.ts +3 -1
  74. package/src/component/logger.ts +36 -0
  75. package/src/component/public.ts +364 -111
  76. package/src/component/schema.ts +18 -5
  77. package/src/env.d.ts +31 -0
  78. package/src/server/builder.ts +85 -0
  79. package/src/server/index.ts +9 -24
  80. package/src/server/schema.ts +20 -76
  81. package/src/server/storage.ts +313 -0
  82. package/src/shared/index.ts +5 -0
  83. package/src/shared/types.ts +52 -0
  84. package/LICENSE.package +0 -201
  85. package/dist/client/storage.d.ts +0 -143
  86. package/dist/server/replication.d.ts +0 -122
  87. package/dist/server/ssr.d.ts +0 -79
  88. package/dist/ssr.js +0 -19
  89. package/src/client/storage.ts +0 -206
  90. package/src/component/_generated/api.d.ts +0 -95
  91. package/src/component/_generated/api.js +0 -23
  92. package/src/component/_generated/server.js +0 -90
  93. package/src/server/replication.ts +0 -244
  94. package/src/server/ssr.ts +0 -106
@@ -0,0 +1,85 @@
1
+ import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from 'convex/server';
2
+ import { Replicate } from '$/server/storage.js';
3
+
4
+ /**
5
+ * Configuration for replicate handlers (without component - used with factory pattern).
6
+ */
7
+ export interface ReplicateConfig<T extends object> {
8
+ collection: string;
9
+ /** Size threshold for auto-compaction (default: 5MB). Set to 0 to disable. */
10
+ compaction?: { threshold?: number };
11
+ hooks?: {
12
+ evalRead?: (ctx: GenericQueryCtx<GenericDataModel>, collection: string) => void | Promise<void>;
13
+ evalWrite?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
14
+ evalRemove?: (ctx: GenericMutationCtx<GenericDataModel>, docId: string) => void | Promise<void>;
15
+ onStream?: (ctx: GenericQueryCtx<GenericDataModel>, result: any) => void | Promise<void>;
16
+ onInsert?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
17
+ onUpdate?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
18
+ onRemove?: (ctx: GenericMutationCtx<GenericDataModel>, docId: string) => void | Promise<void>;
19
+ transform?: (docs: T[]) => T[] | Promise<T[]>;
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Create a replicate function bound to your component. Call this once in your
25
+ * convex/replicate.ts file, then use the returned function for all collections.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // convex/replicate.ts (create once)
30
+ * import { replicate } from '@trestleinc/replicate/server';
31
+ * import { components } from './_generated/api';
32
+ *
33
+ * export const tasks = replicate(components.replicate)<Task>({ collection: 'tasks' });
34
+ *
35
+ * // Or bind once and reuse:
36
+ * const r = replicate(components.replicate);
37
+ * export const tasks = r<Task>({ collection: 'tasks' });
38
+ * export const notebooks = r<Notebook>({ collection: 'notebooks' });
39
+ * ```
40
+ */
41
+ export function replicate(component: any) {
42
+ return function boundReplicate<T extends object>(config: ReplicateConfig<T>) {
43
+ return replicateInternal<T>(component, config);
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Internal implementation for replicate.
49
+ */
50
+ function replicateInternal<T extends object>(component: any, config: ReplicateConfig<T>) {
51
+ const storage = new Replicate<T>(component, config.collection, {
52
+ threshold: config.compaction?.threshold,
53
+ });
54
+
55
+ return {
56
+ stream: storage.createStreamQuery({
57
+ evalRead: config.hooks?.evalRead,
58
+ onStream: config.hooks?.onStream,
59
+ }),
60
+
61
+ material: storage.createSSRQuery({
62
+ evalRead: config.hooks?.evalRead,
63
+ transform: config.hooks?.transform,
64
+ }),
65
+
66
+ recovery: storage.createRecoveryQuery({
67
+ evalRead: config.hooks?.evalRead,
68
+ }),
69
+
70
+ insert: storage.createInsertMutation({
71
+ evalWrite: config.hooks?.evalWrite,
72
+ onInsert: config.hooks?.onInsert,
73
+ }),
74
+
75
+ update: storage.createUpdateMutation({
76
+ evalWrite: config.hooks?.evalWrite,
77
+ onUpdate: config.hooks?.onUpdate,
78
+ }),
79
+
80
+ remove: storage.createRemoveMutation({
81
+ evalRemove: config.hooks?.evalRemove,
82
+ onRemove: config.hooks?.onRemove,
83
+ }),
84
+ };
85
+ }
@@ -1,26 +1,11 @@
1
- /**
2
- * Server-side utilities for Convex backend.
3
- * Import this in your Convex functions (convex/*.ts files).
4
- *
5
- * @example
6
- * ```typescript
7
- * // convex/tasks.ts
8
- * import {
9
- * insertDocumentHelper,
10
- * updateDocumentHelper,
11
- * deleteDocumentHelper,
12
- * streamHelper,
13
- * } from '@trestleinc/replicate/server';
14
- * ```
15
- */
1
+ export { replicate } from '$/server/builder.js';
2
+ export type { ReplicateConfig } from '$/server/builder.js';
16
3
 
17
- // Replication helpers for mutations/queries
18
- export {
19
- insertDocumentHelper,
20
- updateDocumentHelper,
21
- deleteDocumentHelper,
22
- streamHelper,
23
- } from './replication.js';
4
+ import { table, prose } from '$/server/schema.js';
24
5
 
25
- // Schema utilities
26
- export { replicatedTable, type ReplicationFields } from './schema.js';
6
+ export const schema = {
7
+ table,
8
+ prose,
9
+ } as const;
10
+
11
+ export type { ReplicationFields } from '$/server/schema.js';
@@ -1,97 +1,41 @@
1
- /**
2
- * Schema utilities for defining replicated tables.
3
- * Automatically adds replication metadata fields so users don't have to.
4
- *
5
- * @example
6
- * ```typescript
7
- * // convex/schema.ts
8
- * import { defineSchema } from 'convex/server';
9
- * import { v } from 'convex/values';
10
- * import { replicatedTable } from '@trestleinc/replicate/server';
11
- *
12
- * export default defineSchema({
13
- * tasks: replicatedTable(
14
- * {
15
- * id: v.string(),
16
- * text: v.string(),
17
- * isCompleted: v.boolean(),
18
- * },
19
- * (table) => table
20
- * .index('by_id', ['id'])
21
- * .index('by_timestamp', ['timestamp'])
22
- * ),
23
- * });
24
- * ```
25
- */
26
-
27
1
  import { defineTable } from 'convex/server';
28
2
  import { v } from 'convex/values';
29
3
 
30
- /**
31
- * Internal replication metadata fields added to every replicated table.
32
- * These are managed automatically by the replication layer.
33
- */
4
+ /** Fields automatically added to replicated tables */
34
5
  export type ReplicationFields = {
35
- /** Version number for conflict resolution */
36
- version: number;
37
- /** Last modification timestamp (Unix ms) */
38
6
  timestamp: number;
39
7
  };
40
8
 
9
+ export const prose = () =>
10
+ v.object({
11
+ type: v.literal('doc'),
12
+ content: v.optional(v.array(v.any())),
13
+ });
14
+
41
15
  /**
42
- * Wraps a table definition to automatically add replication metadata fields.
43
- *
44
- * Users define their business logic fields, and we inject:
45
- * - `version` - For conflict resolution and CRDT versioning
46
- * - `timestamp` - For incremental sync and change tracking
47
- *
48
- * Enables:
49
- * - Dual-storage architecture (CRDT component + main table)
50
- * - Conflict-free replication across clients
51
- * - Hard delete support with CRDT history preservation
52
- * - Event sourcing via component storage
53
- *
54
- * @param userFields - User's business logic fields (id, text, etc.)
55
- * @param applyIndexes - Optional callback to add indexes to the table
56
- * @returns TableDefinition with replication fields injected
16
+ * Define a table with automatic timestamp field for replication.
17
+ * All replicated tables must have an `id` field and define a `by_doc_id` index.
57
18
  *
58
19
  * @example
59
20
  * ```typescript
60
- * // Simple table with hard delete support
61
- * tasks: replicatedTable({
62
- * id: v.string(),
63
- * text: v.string(),
64
- * })
65
- *
66
- * // With indexes
67
- * tasks: replicatedTable(
68
- * {
69
- * id: v.string(),
70
- * text: v.string(),
71
- * },
72
- * (table) => table
73
- * .index('by_id', ['id'])
74
- * .index('by_timestamp', ['timestamp'])
75
- * )
21
+ * // convex/schema.ts
22
+ * export default defineSchema({
23
+ * tasks: table(
24
+ * { id: v.string(), text: v.string(), isCompleted: v.boolean() },
25
+ * (t) => t.index('by_doc_id', ['id']).index('by_completed', ['isCompleted'])
26
+ * ),
27
+ * });
76
28
  * ```
77
29
  */
78
- export function replicatedTable(
79
- userFields: Record<string, any>,
80
- applyIndexes?: (table: any) => any
81
- ): any {
82
- // Create table with user fields + replication metadata
83
- const tableWithMetadata = defineTable({
30
+ export function table(userFields: Record<string, any>, applyIndexes?: (table: any) => any): any {
31
+ const tbl = defineTable({
84
32
  ...userFields,
85
-
86
- // Injected replication fields (hidden from user's mental model)
87
- version: v.number(),
88
33
  timestamp: v.number(),
89
34
  });
90
35
 
91
- // Apply user-defined indexes if provided
92
36
  if (applyIndexes) {
93
- return applyIndexes(tableWithMetadata);
37
+ return applyIndexes(tbl);
94
38
  }
95
39
 
96
- return tableWithMetadata;
40
+ return tbl;
97
41
  }
@@ -0,0 +1,313 @@
1
+ import { v } from 'convex/values';
2
+ import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from 'convex/server';
3
+ import { queryGeneric, mutationGeneric } from 'convex/server';
4
+
5
+ export class Replicate<T extends object> {
6
+ constructor(
7
+ public component: any,
8
+ public collectionName: string,
9
+ private options?: { threshold?: number }
10
+ ) {}
11
+
12
+ createStreamQuery(opts?: {
13
+ evalRead?: (ctx: GenericQueryCtx<GenericDataModel>, collection: string) => void | Promise<void>;
14
+ onStream?: (ctx: GenericQueryCtx<GenericDataModel>, result: any) => void | Promise<void>;
15
+ }) {
16
+ const component = this.component;
17
+ const collection = this.collectionName;
18
+
19
+ return queryGeneric({
20
+ args: {
21
+ checkpoint: v.object({ lastModified: v.number() }),
22
+ limit: v.optional(v.number()),
23
+ vector: v.optional(v.bytes()),
24
+ },
25
+ returns: v.object({
26
+ changes: v.array(
27
+ v.object({
28
+ documentId: v.optional(v.string()),
29
+ crdtBytes: v.bytes(),
30
+ version: v.number(),
31
+ timestamp: v.number(),
32
+ operationType: v.string(),
33
+ })
34
+ ),
35
+ checkpoint: v.object({ lastModified: v.number() }),
36
+ hasMore: v.boolean(),
37
+ }),
38
+ handler: async (ctx, args) => {
39
+ if (opts?.evalRead) {
40
+ await opts.evalRead(ctx, collection);
41
+ }
42
+ const result = await ctx.runQuery(component.public.stream, {
43
+ collection,
44
+ checkpoint: args.checkpoint,
45
+ limit: args.limit,
46
+ vector: args.vector,
47
+ });
48
+
49
+ if (opts?.onStream) {
50
+ await opts.onStream(ctx, result);
51
+ }
52
+
53
+ return result;
54
+ },
55
+ });
56
+ }
57
+
58
+ createSSRQuery(opts?: {
59
+ evalRead?: (ctx: GenericQueryCtx<GenericDataModel>, collection: string) => void | Promise<void>;
60
+ transform?: (docs: T[]) => T[] | Promise<T[]>;
61
+ includeCRDTState?: boolean;
62
+ }) {
63
+ const collection = this.collectionName;
64
+ const component = this.component;
65
+
66
+ return queryGeneric({
67
+ args: {},
68
+ returns: v.object({
69
+ documents: v.any(),
70
+ checkpoint: v.optional(v.object({ lastModified: v.number() })),
71
+ count: v.number(),
72
+ crdtBytes: v.optional(v.bytes()),
73
+ }),
74
+ handler: async (ctx) => {
75
+ if (opts?.evalRead) {
76
+ await opts.evalRead(ctx, collection);
77
+ }
78
+ let docs = (await ctx.db.query(collection).collect()) as T[];
79
+ if (opts?.transform) {
80
+ docs = await opts.transform(docs);
81
+ }
82
+
83
+ const latestTimestamp =
84
+ docs.length > 0 ? Math.max(...docs.map((doc: any) => doc.timestamp || 0)) : 0;
85
+
86
+ const response: {
87
+ documents: T[];
88
+ checkpoint?: { lastModified: number };
89
+ count: number;
90
+ crdtBytes?: ArrayBuffer;
91
+ } = {
92
+ documents: docs,
93
+ checkpoint: latestTimestamp > 0 ? { lastModified: latestTimestamp } : undefined,
94
+ count: docs.length,
95
+ };
96
+
97
+ if (opts?.includeCRDTState) {
98
+ const crdtState = await ctx.runQuery(component.public.getInitialState, {
99
+ collection,
100
+ });
101
+
102
+ if (crdtState) {
103
+ response.crdtBytes = crdtState.crdtBytes;
104
+ response.checkpoint = crdtState.checkpoint;
105
+ }
106
+ }
107
+ return response;
108
+ },
109
+ });
110
+ }
111
+
112
+ createInsertMutation(opts?: {
113
+ evalWrite?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
114
+ onInsert?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
115
+ }) {
116
+ const component = this.component;
117
+ const collection = this.collectionName;
118
+ const threshold = this.options?.threshold;
119
+
120
+ return mutationGeneric({
121
+ args: {
122
+ documentId: v.string(),
123
+ crdtBytes: v.bytes(),
124
+ materializedDoc: v.any(),
125
+ },
126
+ returns: v.object({
127
+ success: v.boolean(),
128
+ metadata: v.any(),
129
+ }),
130
+ handler: async (ctx, args) => {
131
+ const doc = args.materializedDoc as T;
132
+
133
+ if (opts?.evalWrite) {
134
+ await opts.evalWrite(ctx, doc);
135
+ }
136
+
137
+ const version = Date.now();
138
+ await ctx.runMutation(component.public.insertDocument, {
139
+ collection,
140
+ documentId: args.documentId,
141
+ crdtBytes: args.crdtBytes,
142
+ version,
143
+ threshold,
144
+ });
145
+
146
+ await ctx.db.insert(collection, {
147
+ id: args.documentId,
148
+ ...(args.materializedDoc as object),
149
+ timestamp: Date.now(),
150
+ });
151
+
152
+ if (opts?.onInsert) {
153
+ await opts.onInsert(ctx, doc);
154
+ }
155
+
156
+ return {
157
+ success: true,
158
+ metadata: {
159
+ documentId: args.documentId,
160
+ timestamp: Date.now(),
161
+ collection,
162
+ },
163
+ };
164
+ },
165
+ });
166
+ }
167
+
168
+ createUpdateMutation(opts?: {
169
+ evalWrite?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
170
+ onUpdate?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
171
+ }) {
172
+ const component = this.component;
173
+ const collection = this.collectionName;
174
+ const threshold = this.options?.threshold;
175
+
176
+ return mutationGeneric({
177
+ args: {
178
+ documentId: v.string(),
179
+ crdtBytes: v.bytes(),
180
+ materializedDoc: v.any(),
181
+ },
182
+ returns: v.object({
183
+ success: v.boolean(),
184
+ metadata: v.any(),
185
+ }),
186
+ handler: async (ctx, args) => {
187
+ const doc = args.materializedDoc as T;
188
+
189
+ if (opts?.evalWrite) {
190
+ await opts.evalWrite(ctx, doc);
191
+ }
192
+
193
+ const version = Date.now();
194
+ await ctx.runMutation(component.public.updateDocument, {
195
+ collection,
196
+ documentId: args.documentId,
197
+ crdtBytes: args.crdtBytes,
198
+ version,
199
+ threshold,
200
+ });
201
+
202
+ const existing = await ctx.db
203
+ .query(collection)
204
+ .withIndex('by_doc_id', (q) => q.eq('id', args.documentId))
205
+ .first();
206
+
207
+ if (existing) {
208
+ await ctx.db.patch(collection, existing._id, {
209
+ ...(args.materializedDoc as object),
210
+ timestamp: Date.now(),
211
+ });
212
+ }
213
+
214
+ if (opts?.onUpdate) {
215
+ await opts.onUpdate(ctx, doc);
216
+ }
217
+
218
+ return {
219
+ success: true,
220
+ metadata: {
221
+ documentId: args.documentId,
222
+ timestamp: Date.now(),
223
+ collection,
224
+ },
225
+ };
226
+ },
227
+ });
228
+ }
229
+
230
+ createRemoveMutation(opts?: {
231
+ evalRemove?: (ctx: GenericMutationCtx<GenericDataModel>, docId: string) => void | Promise<void>;
232
+ onRemove?: (ctx: GenericMutationCtx<GenericDataModel>, docId: string) => void | Promise<void>;
233
+ }) {
234
+ const component = this.component;
235
+ const collection = this.collectionName;
236
+ const threshold = this.options?.threshold;
237
+
238
+ return mutationGeneric({
239
+ args: {
240
+ documentId: v.string(),
241
+ crdtBytes: v.bytes(),
242
+ },
243
+ returns: v.object({
244
+ success: v.boolean(),
245
+ metadata: v.any(),
246
+ }),
247
+ handler: async (ctx, args) => {
248
+ const documentId = args.documentId as string;
249
+ if (opts?.evalRemove) {
250
+ await opts.evalRemove(ctx, documentId);
251
+ }
252
+
253
+ const version = Date.now();
254
+ await ctx.runMutation(component.public.deleteDocument, {
255
+ collection,
256
+ documentId: documentId,
257
+ crdtBytes: args.crdtBytes,
258
+ version,
259
+ threshold,
260
+ });
261
+
262
+ const existing = await ctx.db
263
+ .query(collection)
264
+ .withIndex('by_doc_id', (q) => q.eq('id', documentId))
265
+ .first();
266
+
267
+ if (existing) {
268
+ await ctx.db.delete(collection, existing._id);
269
+ }
270
+
271
+ if (opts?.onRemove) {
272
+ await opts.onRemove(ctx, documentId);
273
+ }
274
+
275
+ return {
276
+ success: true,
277
+ metadata: {
278
+ documentId: documentId,
279
+ timestamp: Date.now(),
280
+ collection,
281
+ },
282
+ };
283
+ },
284
+ });
285
+ }
286
+
287
+ createRecoveryQuery(opts?: {
288
+ evalRead?: (ctx: GenericQueryCtx<GenericDataModel>, collection: string) => void | Promise<void>;
289
+ }) {
290
+ const component = this.component;
291
+ const collection = this.collectionName;
292
+
293
+ return queryGeneric({
294
+ args: {
295
+ clientStateVector: v.bytes(),
296
+ },
297
+ returns: v.object({
298
+ diff: v.optional(v.bytes()),
299
+ serverStateVector: v.bytes(),
300
+ }),
301
+ handler: async (ctx, args) => {
302
+ if (opts?.evalRead) {
303
+ await opts.evalRead(ctx, collection);
304
+ }
305
+
306
+ return await ctx.runQuery(component.public.recovery, {
307
+ collection,
308
+ clientStateVector: args.clientStateVector,
309
+ });
310
+ },
311
+ });
312
+ }
313
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared types re-exported for top-level access
3
+ */
4
+ export type { FragmentValue, XmlFragmentJSON, XmlNodeJSON } from "./types.js";
5
+ export { OperationType } from "./types.js";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared types for @trestleinc/replicate
3
+ *
4
+ * These types are used across client, server, and component code.
5
+ * They are safe to import in any environment (browser, Node.js, Convex).
6
+ */
7
+
8
+ /** Marker used during insert/update to signal a fragment field */
9
+ export interface FragmentValue {
10
+ __xmlFragment: true;
11
+ content?: XmlFragmentJSON;
12
+ }
13
+
14
+ /** ProseMirror-compatible JSON for XmlFragment serialization */
15
+ export interface XmlFragmentJSON {
16
+ type: "doc";
17
+ content?: XmlNodeJSON[];
18
+ }
19
+
20
+ /** ProseMirror node structure */
21
+ export interface XmlNodeJSON {
22
+ type: string;
23
+ attrs?: Record<string, unknown>;
24
+ content?: XmlNodeJSON[];
25
+ text?: string;
26
+ marks?: { type: string; attrs?: Record<string, unknown> }[];
27
+ }
28
+
29
+ /** Operation type for streaming changes */
30
+ export enum OperationType {
31
+ Delta = "delta",
32
+ Snapshot = "snapshot",
33
+ }
34
+
35
+ /**
36
+ * Extract field names from T where the value type is XmlFragmentJSON.
37
+ * Used for type-safe prose field configuration.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * interface Notebook {
42
+ * id: string;
43
+ * title: string;
44
+ * content: XmlFragmentJSON;
45
+ * }
46
+ *
47
+ * type Fields = ProseFields<Notebook>; // 'content'
48
+ * ```
49
+ */
50
+ export type ProseFields<T> = {
51
+ [K in keyof T]: T[K] extends XmlFragmentJSON ? K : never;
52
+ }[keyof T];