@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
@@ -1,29 +1,32 @@
1
- import { defineSchema, defineTable } from 'convex/server';
2
- import { v } from 'convex/values';
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
3
 
4
4
  export default defineSchema({
5
5
  documents: defineTable({
6
6
  collection: v.string(),
7
7
  documentId: v.string(),
8
8
  crdtBytes: v.bytes(),
9
- version: v.number(),
10
- timestamp: v.number(),
9
+ seq: v.number(),
11
10
  })
12
- .index('by_collection', ['collection'])
13
- .index('by_collection_document_version', ['collection', 'documentId', 'version'])
14
- .index('by_timestamp', ['collection', 'timestamp']),
11
+ .index("by_collection", ["collection"])
12
+ .index("by_collection_document", ["collection", "documentId"])
13
+ .index("by_seq", ["collection", "seq"]),
15
14
 
16
15
  snapshots: defineTable({
17
16
  collection: v.string(),
18
17
  documentId: v.string(),
19
18
  snapshotBytes: v.bytes(),
20
- latestCompactionTimestamp: v.number(),
19
+ stateVector: v.bytes(),
20
+ snapshotSeq: v.number(),
21
21
  createdAt: v.number(),
22
- metadata: v.optional(
23
- v.object({
24
- deltaCount: v.number(),
25
- totalSize: v.number(),
26
- })
27
- ),
28
- }).index('by_document', ['collection', 'documentId']),
22
+ }).index("by_document", ["collection", "documentId"]),
23
+
24
+ peers: defineTable({
25
+ collection: v.string(),
26
+ peerId: v.string(),
27
+ lastSyncedSeq: v.number(),
28
+ lastSeenAt: v.number(),
29
+ })
30
+ .index("by_collection", ["collection"])
31
+ .index("by_collection_peer", ["collection", "peerId"]),
29
32
  });
@@ -1,17 +1,22 @@
1
- import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from 'convex/server';
2
- import { Replicate } from '$/server/storage.js';
1
+ import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from "convex/server";
2
+ import { Replicate } from "$/server/storage";
3
+ import type { CompactionConfig } from "$/shared/types";
3
4
 
4
5
  /**
5
6
  * Configuration for replicate handlers (without component - used with factory pattern).
6
7
  */
7
8
  export interface ReplicateConfig<T extends object> {
8
9
  collection: string;
9
- /** Size threshold for auto-compaction (default: 5MB). Set to 0 to disable. */
10
- compaction?: { threshold?: number };
10
+ compaction?: Partial<CompactionConfig>;
11
11
  hooks?: {
12
12
  evalRead?: (ctx: GenericQueryCtx<GenericDataModel>, collection: string) => void | Promise<void>;
13
13
  evalWrite?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
14
14
  evalRemove?: (ctx: GenericMutationCtx<GenericDataModel>, docId: string) => void | Promise<void>;
15
+ evalMark?: (ctx: GenericMutationCtx<GenericDataModel>, peerId: string) => void | Promise<void>;
16
+ evalCompact?: (
17
+ ctx: GenericMutationCtx<GenericDataModel>,
18
+ documentId: string,
19
+ ) => void | Promise<void>;
15
20
  onStream?: (ctx: GenericQueryCtx<GenericDataModel>, result: any) => void | Promise<void>;
16
21
  onInsert?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
17
22
  onUpdate?: (ctx: GenericMutationCtx<GenericDataModel>, doc: T) => void | Promise<void>;
@@ -48,11 +53,11 @@ export function replicate(component: any) {
48
53
  * Internal implementation for replicate.
49
54
  */
50
55
  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
- });
56
+ const storage = new Replicate<T>(component, config.collection, config.compaction);
54
57
 
55
58
  return {
59
+ __collection: config.collection,
60
+
56
61
  stream: storage.createStreamQuery({
57
62
  evalRead: config.hooks?.evalRead,
58
63
  onStream: config.hooks?.onStream,
@@ -81,5 +86,13 @@ function replicateInternal<T extends object>(component: any, config: ReplicateCo
81
86
  evalRemove: config.hooks?.evalRemove,
82
87
  onRemove: config.hooks?.onRemove,
83
88
  }),
89
+
90
+ mark: storage.createMarkMutation({
91
+ evalWrite: config.hooks?.evalMark,
92
+ }),
93
+
94
+ compact: storage.createCompactMutation({
95
+ evalWrite: config.hooks?.evalCompact,
96
+ }),
84
97
  };
85
98
  }
@@ -1,11 +1,9 @@
1
- export { replicate } from '$/server/builder.js';
2
- export type { ReplicateConfig } from '$/server/builder.js';
1
+ export { replicate } from "$/server/builder";
2
+ export type { ReplicateConfig } from "$/server/builder";
3
3
 
4
- import { table, prose } from '$/server/schema.js';
4
+ import { table, prose } from "$/server/schema";
5
5
 
6
6
  export const schema = {
7
7
  table,
8
8
  prose,
9
9
  } as const;
10
-
11
- export type { ReplicationFields } from '$/server/schema.js';
@@ -1,14 +1,14 @@
1
- import { defineTable } from 'convex/server';
2
- import { v } from 'convex/values';
1
+ import { defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
3
 
4
4
  /** Fields automatically added to replicated tables */
5
- export type ReplicationFields = {
5
+ export interface ReplicationFields {
6
6
  timestamp: number;
7
- };
7
+ }
8
8
 
9
9
  export const prose = () =>
10
10
  v.object({
11
- type: v.literal('doc'),
11
+ type: v.literal("doc"),
12
12
  content: v.optional(v.array(v.any())),
13
13
  });
14
14
 
@@ -1,13 +1,29 @@
1
- import { v } from 'convex/values';
2
- import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from 'convex/server';
3
- import { queryGeneric, mutationGeneric } from 'convex/server';
1
+ import { v } from "convex/values";
2
+ import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from "convex/server";
3
+ import { queryGeneric, mutationGeneric } from "convex/server";
4
+ import { type CompactionConfig, parseSize, parseDuration } from "$/shared/types";
5
+
6
+ const BYTES_PER_MB = 1024 * 1024;
7
+ const MS_PER_HOUR = 60 * 60 * 1000;
8
+ const DEFAULT_SIZE_THRESHOLD_5MB = 5 * BYTES_PER_MB;
9
+ const DEFAULT_PEER_TIMEOUT_24H = 24 * MS_PER_HOUR;
4
10
 
5
11
  export class Replicate<T extends object> {
12
+ private sizeThreshold: number;
13
+ private peerTimeout: number;
14
+
6
15
  constructor(
7
16
  public component: any,
8
17
  public collectionName: string,
9
- private options?: { threshold?: number }
10
- ) {}
18
+ compaction?: Partial<CompactionConfig>,
19
+ ) {
20
+ this.sizeThreshold = compaction?.sizeThreshold
21
+ ? parseSize(compaction.sizeThreshold)
22
+ : DEFAULT_SIZE_THRESHOLD_5MB;
23
+ this.peerTimeout = compaction?.peerTimeout
24
+ ? parseDuration(compaction.peerTimeout)
25
+ : DEFAULT_PEER_TIMEOUT_24H;
26
+ }
11
27
 
12
28
  createStreamQuery(opts?: {
13
29
  evalRead?: (ctx: GenericQueryCtx<GenericDataModel>, collection: string) => void | Promise<void>;
@@ -18,22 +34,22 @@ export class Replicate<T extends object> {
18
34
 
19
35
  return queryGeneric({
20
36
  args: {
21
- checkpoint: v.object({ lastModified: v.number() }),
37
+ cursor: v.number(),
22
38
  limit: v.optional(v.number()),
23
- vector: v.optional(v.bytes()),
39
+ sizeThreshold: v.optional(v.number()),
24
40
  },
25
41
  returns: v.object({
26
42
  changes: v.array(
27
43
  v.object({
28
- documentId: v.optional(v.string()),
44
+ documentId: v.string(),
29
45
  crdtBytes: v.bytes(),
30
- version: v.number(),
31
- timestamp: v.number(),
46
+ seq: v.number(),
32
47
  operationType: v.string(),
33
- })
48
+ }),
34
49
  ),
35
- checkpoint: v.object({ lastModified: v.number() }),
50
+ cursor: v.number(),
36
51
  hasMore: v.boolean(),
52
+ compact: v.optional(v.string()),
37
53
  }),
38
54
  handler: async (ctx, args) => {
39
55
  if (opts?.evalRead) {
@@ -41,9 +57,9 @@ export class Replicate<T extends object> {
41
57
  }
42
58
  const result = await ctx.runQuery(component.public.stream, {
43
59
  collection,
44
- checkpoint: args.checkpoint,
60
+ cursor: args.cursor,
45
61
  limit: args.limit,
46
- vector: args.vector,
62
+ sizeThreshold: args.sizeThreshold,
47
63
  });
48
64
 
49
65
  if (opts?.onStream) {
@@ -67,7 +83,7 @@ export class Replicate<T extends object> {
67
83
  args: {},
68
84
  returns: v.object({
69
85
  documents: v.any(),
70
- checkpoint: v.optional(v.object({ lastModified: v.number() })),
86
+ cursor: v.optional(v.number()),
71
87
  count: v.number(),
72
88
  crdtBytes: v.optional(v.bytes()),
73
89
  }),
@@ -80,17 +96,13 @@ export class Replicate<T extends object> {
80
96
  docs = await opts.transform(docs);
81
97
  }
82
98
 
83
- const latestTimestamp =
84
- docs.length > 0 ? Math.max(...docs.map((doc: any) => doc.timestamp || 0)) : 0;
85
-
86
99
  const response: {
87
100
  documents: T[];
88
- checkpoint?: { lastModified: number };
101
+ cursor?: number;
89
102
  count: number;
90
103
  crdtBytes?: ArrayBuffer;
91
104
  } = {
92
105
  documents: docs,
93
- checkpoint: latestTimestamp > 0 ? { lastModified: latestTimestamp } : undefined,
94
106
  count: docs.length,
95
107
  };
96
108
 
@@ -101,7 +113,7 @@ export class Replicate<T extends object> {
101
113
 
102
114
  if (crdtState) {
103
115
  response.crdtBytes = crdtState.crdtBytes;
104
- response.checkpoint = crdtState.checkpoint;
116
+ response.cursor = crdtState.cursor;
105
117
  }
106
118
  }
107
119
  return response;
@@ -115,7 +127,6 @@ export class Replicate<T extends object> {
115
127
  }) {
116
128
  const component = this.component;
117
129
  const collection = this.collectionName;
118
- const threshold = this.options?.threshold;
119
130
 
120
131
  return mutationGeneric({
121
132
  args: {
@@ -125,7 +136,7 @@ export class Replicate<T extends object> {
125
136
  },
126
137
  returns: v.object({
127
138
  success: v.boolean(),
128
- metadata: v.any(),
139
+ seq: v.number(),
129
140
  }),
130
141
  handler: async (ctx, args) => {
131
142
  const doc = args.materializedDoc as T;
@@ -134,13 +145,10 @@ export class Replicate<T extends object> {
134
145
  await opts.evalWrite(ctx, doc);
135
146
  }
136
147
 
137
- const version = Date.now();
138
- await ctx.runMutation(component.public.insertDocument, {
148
+ const result = await ctx.runMutation(component.public.insertDocument, {
139
149
  collection,
140
150
  documentId: args.documentId,
141
151
  crdtBytes: args.crdtBytes,
142
- version,
143
- threshold,
144
152
  });
145
153
 
146
154
  await ctx.db.insert(collection, {
@@ -155,11 +163,7 @@ export class Replicate<T extends object> {
155
163
 
156
164
  return {
157
165
  success: true,
158
- metadata: {
159
- documentId: args.documentId,
160
- timestamp: Date.now(),
161
- collection,
162
- },
166
+ seq: result.seq,
163
167
  };
164
168
  },
165
169
  });
@@ -171,7 +175,6 @@ export class Replicate<T extends object> {
171
175
  }) {
172
176
  const component = this.component;
173
177
  const collection = this.collectionName;
174
- const threshold = this.options?.threshold;
175
178
 
176
179
  return mutationGeneric({
177
180
  args: {
@@ -181,7 +184,7 @@ export class Replicate<T extends object> {
181
184
  },
182
185
  returns: v.object({
183
186
  success: v.boolean(),
184
- metadata: v.any(),
187
+ seq: v.number(),
185
188
  }),
186
189
  handler: async (ctx, args) => {
187
190
  const doc = args.materializedDoc as T;
@@ -190,22 +193,19 @@ export class Replicate<T extends object> {
190
193
  await opts.evalWrite(ctx, doc);
191
194
  }
192
195
 
193
- const version = Date.now();
194
- await ctx.runMutation(component.public.updateDocument, {
196
+ const result = await ctx.runMutation(component.public.updateDocument, {
195
197
  collection,
196
198
  documentId: args.documentId,
197
199
  crdtBytes: args.crdtBytes,
198
- version,
199
- threshold,
200
200
  });
201
201
 
202
202
  const existing = await ctx.db
203
203
  .query(collection)
204
- .withIndex('by_doc_id', (q) => q.eq('id', args.documentId))
204
+ .withIndex("by_doc_id", q => q.eq("id", args.documentId))
205
205
  .first();
206
206
 
207
207
  if (existing) {
208
- await ctx.db.patch(collection, existing._id, {
208
+ await ctx.db.patch(existing._id, {
209
209
  ...(args.materializedDoc as object),
210
210
  timestamp: Date.now(),
211
211
  });
@@ -217,11 +217,7 @@ export class Replicate<T extends object> {
217
217
 
218
218
  return {
219
219
  success: true,
220
- metadata: {
221
- documentId: args.documentId,
222
- timestamp: Date.now(),
223
- collection,
224
- },
220
+ seq: result.seq,
225
221
  };
226
222
  },
227
223
  });
@@ -233,7 +229,6 @@ export class Replicate<T extends object> {
233
229
  }) {
234
230
  const component = this.component;
235
231
  const collection = this.collectionName;
236
- const threshold = this.options?.threshold;
237
232
 
238
233
  return mutationGeneric({
239
234
  args: {
@@ -242,30 +237,27 @@ export class Replicate<T extends object> {
242
237
  },
243
238
  returns: v.object({
244
239
  success: v.boolean(),
245
- metadata: v.any(),
240
+ seq: v.number(),
246
241
  }),
247
242
  handler: async (ctx, args) => {
248
- const documentId = args.documentId as string;
243
+ const documentId = args.documentId;
249
244
  if (opts?.evalRemove) {
250
245
  await opts.evalRemove(ctx, documentId);
251
246
  }
252
247
 
253
- const version = Date.now();
254
- await ctx.runMutation(component.public.deleteDocument, {
248
+ const result = await ctx.runMutation(component.public.deleteDocument, {
255
249
  collection,
256
250
  documentId: documentId,
257
251
  crdtBytes: args.crdtBytes,
258
- version,
259
- threshold,
260
252
  });
261
253
 
262
254
  const existing = await ctx.db
263
255
  .query(collection)
264
- .withIndex('by_doc_id', (q) => q.eq('id', documentId))
256
+ .withIndex("by_doc_id", q => q.eq("id", documentId))
265
257
  .first();
266
258
 
267
259
  if (existing) {
268
- await ctx.db.delete(collection, existing._id);
260
+ await ctx.db.delete(existing._id);
269
261
  }
270
262
 
271
263
  if (opts?.onRemove) {
@@ -274,16 +266,77 @@ export class Replicate<T extends object> {
274
266
 
275
267
  return {
276
268
  success: true,
277
- metadata: {
278
- documentId: documentId,
279
- timestamp: Date.now(),
280
- collection,
281
- },
269
+ seq: result.seq,
282
270
  };
283
271
  },
284
272
  });
285
273
  }
286
274
 
275
+ createMarkMutation(opts?: {
276
+ evalWrite?: (ctx: GenericMutationCtx<GenericDataModel>, peerId: string) => void | Promise<void>;
277
+ }) {
278
+ const component = this.component;
279
+ const collection = this.collectionName;
280
+
281
+ return mutationGeneric({
282
+ args: {
283
+ peerId: v.string(),
284
+ syncedSeq: v.number(),
285
+ },
286
+ returns: v.null(),
287
+ handler: async (ctx, args) => {
288
+ if (opts?.evalWrite) {
289
+ await opts.evalWrite(ctx, args.peerId);
290
+ }
291
+
292
+ await ctx.runMutation(component.public.mark, {
293
+ collection,
294
+ peerId: args.peerId,
295
+ syncedSeq: args.syncedSeq,
296
+ });
297
+
298
+ return null;
299
+ },
300
+ });
301
+ }
302
+
303
+ createCompactMutation(opts?: {
304
+ evalWrite?: (
305
+ ctx: GenericMutationCtx<GenericDataModel>,
306
+ documentId: string,
307
+ ) => void | Promise<void>;
308
+ }) {
309
+ const component = this.component;
310
+ const collection = this.collectionName;
311
+
312
+ return mutationGeneric({
313
+ args: {
314
+ documentId: v.string(),
315
+ snapshotBytes: v.bytes(),
316
+ stateVector: v.bytes(),
317
+ peerTimeout: v.optional(v.number()),
318
+ },
319
+ returns: v.object({
320
+ success: v.boolean(),
321
+ removed: v.number(),
322
+ retained: v.number(),
323
+ }),
324
+ handler: async (ctx, args) => {
325
+ if (opts?.evalWrite) {
326
+ await opts.evalWrite(ctx, args.documentId);
327
+ }
328
+
329
+ return await ctx.runMutation(component.public.compact, {
330
+ collection,
331
+ documentId: args.documentId,
332
+ snapshotBytes: args.snapshotBytes,
333
+ stateVector: args.stateVector,
334
+ peerTimeout: args.peerTimeout,
335
+ });
336
+ },
337
+ });
338
+ }
339
+
287
340
  createRecoveryQuery(opts?: {
288
341
  evalRead?: (ctx: GenericQueryCtx<GenericDataModel>, collection: string) => void | Promise<void>;
289
342
  }) {
@@ -297,6 +350,7 @@ export class Replicate<T extends object> {
297
350
  returns: v.object({
298
351
  diff: v.optional(v.bytes()),
299
352
  serverStateVector: v.bytes(),
353
+ cursor: v.number(),
300
354
  }),
301
355
  handler: async (ctx, args) => {
302
356
  if (opts?.evalRead) {
@@ -1,5 +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";
1
+ export type {
2
+ ProseValue,
3
+ XmlFragmentJSON,
4
+ XmlNodeJSON,
5
+ } from "./types.js";
@@ -17,6 +17,17 @@ export interface XmlFragmentJSON {
17
17
  content?: XmlNodeJSON[];
18
18
  }
19
19
 
20
+ declare const PROSE_BRAND: unique symbol;
21
+
22
+ /**
23
+ * Branded prose type for Zod schemas.
24
+ * Extends XmlFragmentJSON with a unique brand for type-level detection.
25
+ * Use the `prose()` helper from `@trestleinc/replicate/client` to create this type.
26
+ */
27
+ export interface ProseValue extends XmlFragmentJSON {
28
+ readonly [PROSE_BRAND]: typeof PROSE_BRAND;
29
+ }
30
+
20
31
  /** ProseMirror node structure */
21
32
  export interface XmlNodeJSON {
22
33
  type: string;
@@ -33,20 +44,46 @@ export enum OperationType {
33
44
  }
34
45
 
35
46
  /**
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
- * ```
47
+ * Extract prose field names from T (fields typed as ProseValue).
48
+ * Used internally for type-safe prose field operations.
49
49
  */
50
50
  export type ProseFields<T> = {
51
- [K in keyof T]: T[K] extends XmlFragmentJSON ? K : never;
51
+ [K in keyof T]: T[K] extends ProseValue ? K : never;
52
52
  }[keyof T];
53
+
54
+ type SizeUnit = "kb" | "mb" | "gb";
55
+ export type Size = `${number}${SizeUnit}`;
56
+
57
+ type DurationUnit = "m" | "h" | "d";
58
+ export type Duration = `${number}${DurationUnit}`;
59
+
60
+ export interface CompactionConfig {
61
+ sizeThreshold: Size;
62
+ peerTimeout: Duration;
63
+ }
64
+
65
+ const SIZE_MULTIPLIERS: Record<SizeUnit, number> = {
66
+ kb: 1024,
67
+ mb: 1024 ** 2,
68
+ gb: 1024 ** 3,
69
+ };
70
+
71
+ const DURATION_MULTIPLIERS: Record<DurationUnit, number> = {
72
+ m: 60_000,
73
+ h: 3_600_000,
74
+ d: 86_400_000,
75
+ };
76
+
77
+ export function parseSize(s: Size): number {
78
+ const match = /^(\d+)(kb|mb|gb)$/i.exec(s);
79
+ if (!match) throw new Error(`Invalid size: ${s}`);
80
+ const [, num, unit] = match;
81
+ return parseInt(num) * SIZE_MULTIPLIERS[unit.toLowerCase() as SizeUnit];
82
+ }
83
+
84
+ export function parseDuration(s: Duration): number {
85
+ const match = /^(\d+)(m|h|d)$/i.exec(s);
86
+ if (!match) throw new Error(`Invalid duration: ${s}`);
87
+ const [, num, unit] = match;
88
+ return parseInt(num) * DURATION_MULTIPLIERS[unit.toLowerCase() as DurationUnit];
89
+ }
@@ -1,96 +0,0 @@
1
- import * as Y from 'yjs';
2
- import type { Persistence } from '$/client/persistence/types.js';
3
- import type { ConvexClient } from 'convex/browser';
4
- import type { FunctionReference } from 'convex/server';
5
- import type { CollectionConfig, Collection } from '@tanstack/db';
6
- import type { ProseFields } from '$/shared/types.js';
7
- /** Server-rendered material data for SSR hydration */
8
- export type Materialized<T> = {
9
- documents: ReadonlyArray<T>;
10
- checkpoint?: {
11
- lastModified: number;
12
- };
13
- count?: number;
14
- crdtBytes?: ArrayBuffer;
15
- };
16
- /** Configuration for creating a Convex-backed collection */
17
- export interface ConvexCollectionOptionsConfig<T extends object> {
18
- getKey: (item: T) => string | number;
19
- material?: Materialized<T>;
20
- convexClient: ConvexClient;
21
- api: {
22
- stream: FunctionReference<'query'>;
23
- insert: FunctionReference<'mutation'>;
24
- update: FunctionReference<'mutation'>;
25
- remove: FunctionReference<'mutation'>;
26
- recovery: FunctionReference<'query'>;
27
- material?: FunctionReference<'query'>;
28
- [key: string]: any;
29
- };
30
- collection: string;
31
- /** Fields that contain prose (rich text) content stored as Y.XmlFragment */
32
- prose: Array<ProseFields<T>>;
33
- /** Undo capture timeout in ms. Changes within this window merge into one undo. Default: 500 */
34
- undoCaptureTimeout?: number;
35
- /** Persistence provider for Y.Doc and key-value storage */
36
- persistence: Persistence;
37
- }
38
- /** Editor binding for BlockNote/TipTap collaboration */
39
- export interface EditorBinding {
40
- /** The Y.XmlFragment bound to the editor */
41
- readonly fragment: Y.XmlFragment;
42
- /** Provider stub for BlockNote compatibility */
43
- readonly provider: {
44
- readonly awareness: null;
45
- };
46
- /** Current sync state - true if unsent changes exist */
47
- readonly pending: boolean;
48
- /** Subscribe to pending state changes. Returns unsubscribe function. */
49
- onPendingChange(callback: (pending: boolean) => void): () => void;
50
- /** Undo the last content edit */
51
- undo(): void;
52
- /** Redo the last undone edit */
53
- redo(): void;
54
- /** Check if undo is available */
55
- canUndo(): boolean;
56
- /** Check if redo is available */
57
- canRedo(): boolean;
58
- }
59
- /** Utilities exposed on collection.utils */
60
- interface ConvexCollectionUtils<T extends object> {
61
- /**
62
- * Get an editor binding for a prose field.
63
- * Waits for Y.Doc to be ready (IndexedDB loaded) before returning.
64
- * @param documentId - The document ID
65
- * @param field - The prose field name (must be in `prose` config)
66
- * @returns Promise resolving to EditorBinding
67
- */
68
- prose(documentId: string, field: ProseFields<T>): Promise<EditorBinding>;
69
- }
70
- /** Extended collection with prose field utilities */
71
- export interface ConvexCollection<T extends object> extends Collection<T> {
72
- /** Utilities for prose field operations */
73
- utils: ConvexCollectionUtils<T>;
74
- }
75
- /**
76
- * Create TanStack DB collection options with Convex + Yjs replication.
77
- *
78
- * @example
79
- * ```typescript
80
- * const options = convexCollectionOptions<Task>({
81
- * getKey: (t) => t.id,
82
- * convexClient,
83
- * api: { stream: api.tasks.stream, insert: api.tasks.insert, ... },
84
- * collection: 'tasks',
85
- * });
86
- * const collection = createCollection(options);
87
- * ```
88
- */
89
- export declare function convexCollectionOptions<T extends object>({ getKey, material, convexClient, api, collection, prose: proseFields, undoCaptureTimeout, persistence, }: ConvexCollectionOptionsConfig<T>): CollectionConfig<T> & {
90
- _convexClient: ConvexClient;
91
- _collection: string;
92
- _proseFields: Array<ProseFields<T>>;
93
- _persistence: Persistence;
94
- utils: ConvexCollectionUtils<T>;
95
- };
96
- export {};