@trestleinc/replicate 1.1.1 → 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 +395 -146
  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 -1618
  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 -160
  90. package/src/client/services/checkpoint.ts +0 -86
  91. package/src/client/services/reconciliation.ts +0 -108
@@ -1,309 +1,325 @@
1
- import { Doc, applyUpdateV2, diffUpdateV2, encodeStateVector, encodeStateVectorFromUpdateV2, mergeUpdatesV2 } from "yjs";
2
- import { v } from "convex/values";
3
- import { mutation, query } from "./_generated/server.js";
4
1
  import { getLogger } from "./logger.js";
5
- import { OperationType } from "../shared/types.js";
6
- const DEFAULT_SIZE_THRESHOLD = 5000000;
7
- async function _maybeCompactDocument(ctx, collection, documentId, threshold = DEFAULT_SIZE_THRESHOLD) {
8
- const logger = getLogger([
9
- 'compaction'
10
- ]);
11
- const deltas = await ctx.db.query('documents').withIndex('by_collection_document_version', (q)=>q.eq('collection', collection).eq('documentId', documentId)).collect();
12
- const totalSize = deltas.reduce((sum, d)=>sum + d.crdtBytes.byteLength, 0);
13
- if (totalSize < threshold) return null;
14
- logger.info('Auto-compacting document', {
15
- collection,
16
- documentId,
17
- deltaCount: deltas.length,
18
- totalSize,
19
- threshold
20
- });
21
- const sorted = deltas.sort((a, b)=>a.timestamp - b.timestamp);
22
- const updates = sorted.map((d)=>new Uint8Array(d.crdtBytes));
23
- const compactedState = mergeUpdatesV2(updates);
24
- const testDoc = new Doc({
25
- guid: `${collection}:${documentId}`
26
- });
27
- try {
28
- applyUpdateV2(testDoc, compactedState);
29
- } catch (error) {
30
- logger.error('Compacted state validation failed', {
31
- collection,
32
- documentId,
33
- error: String(error)
34
- });
35
- testDoc.destroy();
36
- return null;
37
- }
38
- testDoc.destroy();
39
- const existingSnapshot = await ctx.db.query('snapshots').withIndex('by_document', (q)=>q.eq('collection', collection).eq('documentId', documentId)).first();
40
- if (existingSnapshot) await ctx.db.delete('snapshots', existingSnapshot._id);
41
- await ctx.db.insert('snapshots', {
42
- collection,
43
- documentId,
44
- snapshotBytes: compactedState.buffer,
45
- latestCompactionTimestamp: sorted[sorted.length - 1].timestamp,
46
- createdAt: Date.now(),
47
- metadata: {
48
- deltaCount: deltas.length,
49
- totalSize
50
- }
51
- });
52
- for (const delta of sorted)await ctx.db.delete('documents', delta._id);
53
- logger.info('Auto-compaction completed', {
54
- collection,
55
- documentId,
56
- deltasCompacted: deltas.length,
57
- snapshotSize: compactedState.length
58
- });
59
- return {
60
- deltasCompacted: deltas.length,
61
- snapshotSize: compactedState.length
62
- };
2
+ import { mutation, query } from "./_generated/server.js";
3
+ import { OperationType } from "./shared/types.js";
4
+ import * as Y from "yjs";
5
+ import { v } from "convex/values";
6
+
7
+ //#region src/component/public.ts
8
+ const DEFAULT_SIZE_THRESHOLD = 5e6;
9
+ const DEFAULT_PEER_TIMEOUT = 300 * 1e3;
10
+ async function getNextSeq(ctx, collection) {
11
+ return ((await ctx.db.query("documents").withIndex("by_seq", (q) => q.eq("collection", collection)).order("desc").first())?.seq ?? 0) + 1;
63
12
  }
64
13
  const insertDocument = mutation({
65
- args: {
66
- collection: v.string(),
67
- documentId: v.string(),
68
- crdtBytes: v.bytes(),
69
- version: v.number(),
70
- threshold: v.optional(v.number())
71
- },
72
- returns: v.object({
73
- success: v.boolean(),
74
- compacted: v.optional(v.boolean())
75
- }),
76
- handler: async (ctx, args)=>{
77
- await ctx.db.insert('documents', {
78
- collection: args.collection,
79
- documentId: args.documentId,
80
- crdtBytes: args.crdtBytes,
81
- version: args.version,
82
- timestamp: Date.now()
83
- });
84
- const compactionResult = await _maybeCompactDocument(ctx, args.collection, args.documentId, args.threshold ?? DEFAULT_SIZE_THRESHOLD);
85
- return {
86
- success: true,
87
- compacted: null !== compactionResult
88
- };
89
- }
14
+ args: {
15
+ collection: v.string(),
16
+ documentId: v.string(),
17
+ crdtBytes: v.bytes()
18
+ },
19
+ returns: v.object({
20
+ success: v.boolean(),
21
+ seq: v.number()
22
+ }),
23
+ handler: async (ctx, args) => {
24
+ const seq = await getNextSeq(ctx, args.collection);
25
+ await ctx.db.insert("documents", {
26
+ collection: args.collection,
27
+ documentId: args.documentId,
28
+ crdtBytes: args.crdtBytes,
29
+ seq
30
+ });
31
+ return {
32
+ success: true,
33
+ seq
34
+ };
35
+ }
90
36
  });
91
37
  const updateDocument = mutation({
92
- args: {
93
- collection: v.string(),
94
- documentId: v.string(),
95
- crdtBytes: v.bytes(),
96
- version: v.number(),
97
- threshold: v.optional(v.number())
98
- },
99
- returns: v.object({
100
- success: v.boolean(),
101
- compacted: v.optional(v.boolean())
102
- }),
103
- handler: async (ctx, args)=>{
104
- await ctx.db.insert('documents', {
105
- collection: args.collection,
106
- documentId: args.documentId,
107
- crdtBytes: args.crdtBytes,
108
- version: args.version,
109
- timestamp: Date.now()
110
- });
111
- const compactionResult = await _maybeCompactDocument(ctx, args.collection, args.documentId, args.threshold ?? DEFAULT_SIZE_THRESHOLD);
112
- return {
113
- success: true,
114
- compacted: null !== compactionResult
115
- };
116
- }
38
+ args: {
39
+ collection: v.string(),
40
+ documentId: v.string(),
41
+ crdtBytes: v.bytes()
42
+ },
43
+ returns: v.object({
44
+ success: v.boolean(),
45
+ seq: v.number()
46
+ }),
47
+ handler: async (ctx, args) => {
48
+ const seq = await getNextSeq(ctx, args.collection);
49
+ await ctx.db.insert("documents", {
50
+ collection: args.collection,
51
+ documentId: args.documentId,
52
+ crdtBytes: args.crdtBytes,
53
+ seq
54
+ });
55
+ return {
56
+ success: true,
57
+ seq
58
+ };
59
+ }
117
60
  });
118
61
  const deleteDocument = mutation({
119
- args: {
120
- collection: v.string(),
121
- documentId: v.string(),
122
- crdtBytes: v.bytes(),
123
- version: v.number(),
124
- threshold: v.optional(v.number())
125
- },
126
- returns: v.object({
127
- success: v.boolean(),
128
- compacted: v.optional(v.boolean())
129
- }),
130
- handler: async (ctx, args)=>{
131
- await ctx.db.insert('documents', {
132
- collection: args.collection,
133
- documentId: args.documentId,
134
- crdtBytes: args.crdtBytes,
135
- version: args.version,
136
- timestamp: Date.now()
137
- });
138
- const compactionResult = await _maybeCompactDocument(ctx, args.collection, args.documentId, args.threshold ?? DEFAULT_SIZE_THRESHOLD);
139
- return {
140
- success: true,
141
- compacted: null !== compactionResult
142
- };
143
- }
62
+ args: {
63
+ collection: v.string(),
64
+ documentId: v.string(),
65
+ crdtBytes: v.bytes()
66
+ },
67
+ returns: v.object({
68
+ success: v.boolean(),
69
+ seq: v.number()
70
+ }),
71
+ handler: async (ctx, args) => {
72
+ const seq = await getNextSeq(ctx, args.collection);
73
+ await ctx.db.insert("documents", {
74
+ collection: args.collection,
75
+ documentId: args.documentId,
76
+ crdtBytes: args.crdtBytes,
77
+ seq
78
+ });
79
+ return {
80
+ success: true,
81
+ seq
82
+ };
83
+ }
84
+ });
85
+ const mark = mutation({
86
+ args: {
87
+ collection: v.string(),
88
+ peerId: v.string(),
89
+ syncedSeq: v.number()
90
+ },
91
+ returns: v.null(),
92
+ handler: async (ctx, args) => {
93
+ const existing = await ctx.db.query("peers").withIndex("by_collection_peer", (q) => q.eq("collection", args.collection).eq("peerId", args.peerId)).first();
94
+ if (existing) await ctx.db.patch(existing._id, {
95
+ lastSyncedSeq: Math.max(existing.lastSyncedSeq, args.syncedSeq),
96
+ lastSeenAt: Date.now()
97
+ });
98
+ else await ctx.db.insert("peers", {
99
+ collection: args.collection,
100
+ peerId: args.peerId,
101
+ lastSyncedSeq: args.syncedSeq,
102
+ lastSeenAt: Date.now()
103
+ });
104
+ return null;
105
+ }
106
+ });
107
+ const compact = mutation({
108
+ args: {
109
+ collection: v.string(),
110
+ documentId: v.string(),
111
+ snapshotBytes: v.bytes(),
112
+ stateVector: v.bytes(),
113
+ peerTimeout: v.optional(v.number())
114
+ },
115
+ returns: v.object({
116
+ success: v.boolean(),
117
+ removed: v.number(),
118
+ retained: v.number()
119
+ }),
120
+ handler: async (ctx, args) => {
121
+ const logger = getLogger(["compaction"]);
122
+ const now = Date.now();
123
+ const peerCutoff = now - (args.peerTimeout ?? DEFAULT_PEER_TIMEOUT);
124
+ const deltas = await ctx.db.query("documents").withIndex("by_collection_document", (q) => q.eq("collection", args.collection).eq("documentId", args.documentId)).collect();
125
+ const activePeers = await ctx.db.query("peers").withIndex("by_collection", (q) => q.eq("collection", args.collection)).filter((q) => q.gt(q.field("lastSeenAt"), peerCutoff)).collect();
126
+ const minSyncedSeq = activePeers.length > 0 ? Math.min(...activePeers.map((p) => p.lastSyncedSeq)) : Infinity;
127
+ const existingSnapshot = await ctx.db.query("snapshots").withIndex("by_document", (q) => q.eq("collection", args.collection).eq("documentId", args.documentId)).first();
128
+ if (existingSnapshot) await ctx.db.delete(existingSnapshot._id);
129
+ const snapshotSeq = deltas.length > 0 ? Math.max(...deltas.map((d) => d.seq)) : 0;
130
+ await ctx.db.insert("snapshots", {
131
+ collection: args.collection,
132
+ documentId: args.documentId,
133
+ snapshotBytes: args.snapshotBytes,
134
+ stateVector: args.stateVector,
135
+ snapshotSeq,
136
+ createdAt: now
137
+ });
138
+ let removed = 0;
139
+ for (const delta of deltas) if (delta.seq < minSyncedSeq) {
140
+ await ctx.db.delete(delta._id);
141
+ removed++;
142
+ }
143
+ logger.info("Compaction completed", {
144
+ collection: args.collection,
145
+ documentId: args.documentId,
146
+ removed,
147
+ retained: deltas.length - removed,
148
+ activePeers: activePeers.length,
149
+ minSyncedSeq
150
+ });
151
+ return {
152
+ success: true,
153
+ removed,
154
+ retained: deltas.length - removed
155
+ };
156
+ }
144
157
  });
145
158
  const stream = query({
146
- args: {
147
- collection: v.string(),
148
- checkpoint: v.object({
149
- lastModified: v.number()
150
- }),
151
- vector: v.optional(v.bytes()),
152
- limit: v.optional(v.number())
153
- },
154
- returns: v.object({
155
- changes: v.array(v.object({
156
- documentId: v.optional(v.string()),
157
- crdtBytes: v.bytes(),
158
- version: v.number(),
159
- timestamp: v.number(),
160
- operationType: v.string()
161
- })),
162
- checkpoint: v.object({
163
- lastModified: v.number()
164
- }),
165
- hasMore: v.boolean()
166
- }),
167
- handler: async (ctx, args)=>{
168
- const limit = args.limit ?? 100;
169
- const documents = await ctx.db.query('documents').withIndex('by_timestamp', (q)=>q.eq('collection', args.collection).gt('timestamp', args.checkpoint.lastModified)).order('asc').take(limit);
170
- if (documents.length > 0) {
171
- const changes = documents.map((doc)=>({
172
- documentId: doc.documentId,
173
- crdtBytes: doc.crdtBytes,
174
- version: doc.version,
175
- timestamp: doc.timestamp,
176
- operationType: OperationType.Delta
177
- }));
178
- const newCheckpoint = {
179
- lastModified: documents[documents.length - 1]?.timestamp ?? args.checkpoint.lastModified
180
- };
181
- return {
182
- changes,
183
- checkpoint: newCheckpoint,
184
- hasMore: documents.length === limit
185
- };
186
- }
187
- const oldestDelta = await ctx.db.query('documents').withIndex('by_timestamp', (q)=>q.eq('collection', args.collection)).order('asc').first();
188
- if (oldestDelta && args.checkpoint.lastModified < oldestDelta.timestamp) {
189
- const snapshots = await ctx.db.query('snapshots').withIndex('by_document', (q)=>q.eq('collection', args.collection)).collect();
190
- if (0 === snapshots.length) throw new Error(`Disparity detected but no snapshots available for collection: ${args.collection}. Client checkpoint: ${args.checkpoint.lastModified}, Oldest delta: ${oldestDelta.timestamp}`);
191
- const changes = snapshots.map((snapshot)=>({
192
- documentId: snapshot.documentId,
193
- crdtBytes: snapshot.snapshotBytes,
194
- version: 0,
195
- timestamp: snapshot.createdAt,
196
- operationType: OperationType.Snapshot
197
- }));
198
- const latestTimestamp = Math.max(...snapshots.map((s)=>s.latestCompactionTimestamp));
199
- return {
200
- changes,
201
- checkpoint: {
202
- lastModified: latestTimestamp
203
- },
204
- hasMore: false
205
- };
206
- }
207
- return {
208
- changes: [],
209
- checkpoint: args.checkpoint,
210
- hasMore: false
211
- };
212
- }
159
+ args: {
160
+ collection: v.string(),
161
+ cursor: v.number(),
162
+ limit: v.optional(v.number()),
163
+ sizeThreshold: v.optional(v.number())
164
+ },
165
+ returns: v.object({
166
+ changes: v.array(v.object({
167
+ documentId: v.string(),
168
+ crdtBytes: v.bytes(),
169
+ seq: v.number(),
170
+ operationType: v.string()
171
+ })),
172
+ cursor: v.number(),
173
+ hasMore: v.boolean(),
174
+ compact: v.optional(v.string())
175
+ }),
176
+ handler: async (ctx, args) => {
177
+ const limit = args.limit ?? 100;
178
+ const sizeThreshold = args.sizeThreshold ?? DEFAULT_SIZE_THRESHOLD;
179
+ const documents = await ctx.db.query("documents").withIndex("by_seq", (q) => q.eq("collection", args.collection).gt("seq", args.cursor)).order("asc").take(limit);
180
+ if (documents.length > 0) {
181
+ const changes = documents.map((doc) => ({
182
+ documentId: doc.documentId,
183
+ crdtBytes: doc.crdtBytes,
184
+ seq: doc.seq,
185
+ operationType: OperationType.Delta
186
+ }));
187
+ const newCursor = documents[documents.length - 1]?.seq ?? args.cursor;
188
+ let compactHint;
189
+ const allDocs = await ctx.db.query("documents").withIndex("by_collection", (q) => q.eq("collection", args.collection)).collect();
190
+ const sizeByDocument = /* @__PURE__ */ new Map();
191
+ for (const doc of allDocs) {
192
+ const current = sizeByDocument.get(doc.documentId) ?? 0;
193
+ sizeByDocument.set(doc.documentId, current + doc.crdtBytes.byteLength);
194
+ }
195
+ for (const [docId, size] of sizeByDocument) if (size > sizeThreshold) {
196
+ compactHint = docId;
197
+ break;
198
+ }
199
+ return {
200
+ changes,
201
+ cursor: newCursor,
202
+ hasMore: documents.length === limit,
203
+ compact: compactHint
204
+ };
205
+ }
206
+ const oldestDelta = await ctx.db.query("documents").withIndex("by_seq", (q) => q.eq("collection", args.collection)).order("asc").first();
207
+ if (oldestDelta && args.cursor < oldestDelta.seq) {
208
+ const snapshots = await ctx.db.query("snapshots").withIndex("by_document", (q) => q.eq("collection", args.collection)).collect();
209
+ if (snapshots.length === 0) throw new Error(`Disparity detected but no snapshots available for collection: ${args.collection}. Client cursor: ${args.cursor}, Oldest delta seq: ${oldestDelta.seq}`);
210
+ return {
211
+ changes: snapshots.map((snapshot) => ({
212
+ documentId: snapshot.documentId,
213
+ crdtBytes: snapshot.snapshotBytes,
214
+ seq: snapshot.snapshotSeq,
215
+ operationType: OperationType.Snapshot
216
+ })),
217
+ cursor: Math.max(...snapshots.map((s) => s.snapshotSeq)),
218
+ hasMore: false,
219
+ compact: void 0
220
+ };
221
+ }
222
+ return {
223
+ changes: [],
224
+ cursor: args.cursor,
225
+ hasMore: false,
226
+ compact: void 0
227
+ };
228
+ }
213
229
  });
214
230
  const getInitialState = query({
215
- args: {
216
- collection: v.string()
217
- },
218
- returns: v.union(v.object({
219
- crdtBytes: v.bytes(),
220
- checkpoint: v.object({
221
- lastModified: v.number()
222
- })
223
- }), v["null"]()),
224
- handler: async (ctx, args)=>{
225
- const logger = getLogger([
226
- 'ssr'
227
- ]);
228
- const snapshots = await ctx.db.query('snapshots').withIndex('by_document', (q)=>q.eq('collection', args.collection)).collect();
229
- const deltas = await ctx.db.query('documents').withIndex('by_collection', (q)=>q.eq('collection', args.collection)).collect();
230
- if (0 === snapshots.length && 0 === deltas.length) {
231
- logger.info('No initial state available - collection is empty', {
232
- collection: args.collection
233
- });
234
- return null;
235
- }
236
- const updates = [];
237
- let latestTimestamp = 0;
238
- for (const snapshot of snapshots){
239
- updates.push(new Uint8Array(snapshot.snapshotBytes));
240
- latestTimestamp = Math.max(latestTimestamp, snapshot.latestCompactionTimestamp);
241
- }
242
- const sorted = deltas.sort((a, b)=>a.timestamp - b.timestamp);
243
- for (const delta of sorted){
244
- updates.push(new Uint8Array(delta.crdtBytes));
245
- latestTimestamp = Math.max(latestTimestamp, delta.timestamp);
246
- }
247
- logger.info('Reconstructing initial state', {
248
- collection: args.collection,
249
- snapshotCount: snapshots.length,
250
- deltaCount: deltas.length
251
- });
252
- const merged = mergeUpdatesV2(updates);
253
- logger.info('Initial state reconstructed', {
254
- collection: args.collection,
255
- originalSize: updates.reduce((sum, u)=>sum + u.byteLength, 0),
256
- mergedSize: merged.byteLength
257
- });
258
- return {
259
- crdtBytes: merged.buffer,
260
- checkpoint: {
261
- lastModified: latestTimestamp
262
- }
263
- };
264
- }
231
+ args: { collection: v.string() },
232
+ returns: v.union(v.object({
233
+ crdtBytes: v.bytes(),
234
+ cursor: v.number()
235
+ }), v.null()),
236
+ handler: async (ctx, args) => {
237
+ const logger = getLogger(["ssr"]);
238
+ const snapshots = await ctx.db.query("snapshots").withIndex("by_document", (q) => q.eq("collection", args.collection)).collect();
239
+ const deltas = await ctx.db.query("documents").withIndex("by_collection", (q) => q.eq("collection", args.collection)).collect();
240
+ if (snapshots.length === 0 && deltas.length === 0) {
241
+ logger.info("No initial state available - collection is empty", { collection: args.collection });
242
+ return null;
243
+ }
244
+ const updates = [];
245
+ let latestSeq = 0;
246
+ for (const snapshot of snapshots) {
247
+ updates.push(new Uint8Array(snapshot.snapshotBytes));
248
+ latestSeq = Math.max(latestSeq, snapshot.snapshotSeq);
249
+ }
250
+ const sorted = deltas.sort((a, b) => a.seq - b.seq);
251
+ for (const delta of sorted) {
252
+ updates.push(new Uint8Array(delta.crdtBytes));
253
+ latestSeq = Math.max(latestSeq, delta.seq);
254
+ }
255
+ logger.info("Reconstructing initial state", {
256
+ collection: args.collection,
257
+ snapshotCount: snapshots.length,
258
+ deltaCount: deltas.length
259
+ });
260
+ const merged = Y.mergeUpdatesV2(updates);
261
+ logger.info("Initial state reconstructed", {
262
+ collection: args.collection,
263
+ originalSize: updates.reduce((sum, u) => sum + u.byteLength, 0),
264
+ mergedSize: merged.byteLength
265
+ });
266
+ return {
267
+ crdtBytes: merged.buffer,
268
+ cursor: latestSeq
269
+ };
270
+ }
265
271
  });
266
272
  const recovery = query({
267
- args: {
268
- collection: v.string(),
269
- clientStateVector: v.bytes()
270
- },
271
- returns: v.object({
272
- diff: v.optional(v.bytes()),
273
- serverStateVector: v.bytes()
274
- }),
275
- handler: async (ctx, args)=>{
276
- const logger = getLogger([
277
- 'recovery'
278
- ]);
279
- const snapshots = await ctx.db.query('snapshots').withIndex('by_document', (q)=>q.eq('collection', args.collection)).collect();
280
- const deltas = await ctx.db.query('documents').withIndex('by_collection', (q)=>q.eq('collection', args.collection)).collect();
281
- if (0 === snapshots.length && 0 === deltas.length) {
282
- const emptyDoc = new Doc();
283
- const emptyVector = encodeStateVector(emptyDoc);
284
- emptyDoc.destroy();
285
- return {
286
- serverStateVector: emptyVector.buffer
287
- };
288
- }
289
- const updates = [];
290
- for (const snapshot of snapshots)updates.push(new Uint8Array(snapshot.snapshotBytes));
291
- for (const delta of deltas)updates.push(new Uint8Array(delta.crdtBytes));
292
- const mergedState = mergeUpdatesV2(updates);
293
- const clientVector = new Uint8Array(args.clientStateVector);
294
- const diff = diffUpdateV2(mergedState, clientVector);
295
- const serverVector = encodeStateVectorFromUpdateV2(mergedState);
296
- logger.info('Recovery sync computed', {
297
- collection: args.collection,
298
- snapshotCount: snapshots.length,
299
- deltaCount: deltas.length,
300
- diffSize: diff.byteLength,
301
- hasDiff: diff.byteLength > 0
302
- });
303
- return {
304
- diff: diff.byteLength > 0 ? diff.buffer : void 0,
305
- serverStateVector: serverVector.buffer
306
- };
307
- }
273
+ args: {
274
+ collection: v.string(),
275
+ clientStateVector: v.bytes()
276
+ },
277
+ returns: v.object({
278
+ diff: v.optional(v.bytes()),
279
+ serverStateVector: v.bytes(),
280
+ cursor: v.number()
281
+ }),
282
+ handler: async (ctx, args) => {
283
+ const logger = getLogger(["recovery"]);
284
+ const snapshots = await ctx.db.query("snapshots").withIndex("by_document", (q) => q.eq("collection", args.collection)).collect();
285
+ const deltas = await ctx.db.query("documents").withIndex("by_collection", (q) => q.eq("collection", args.collection)).collect();
286
+ if (snapshots.length === 0 && deltas.length === 0) {
287
+ const emptyDoc = new Y.Doc();
288
+ const emptyVector = Y.encodeStateVector(emptyDoc);
289
+ emptyDoc.destroy();
290
+ return {
291
+ serverStateVector: emptyVector.buffer,
292
+ cursor: 0
293
+ };
294
+ }
295
+ const updates = [];
296
+ let latestSeq = 0;
297
+ for (const snapshot of snapshots) {
298
+ updates.push(new Uint8Array(snapshot.snapshotBytes));
299
+ latestSeq = Math.max(latestSeq, snapshot.snapshotSeq);
300
+ }
301
+ for (const delta of deltas) {
302
+ updates.push(new Uint8Array(delta.crdtBytes));
303
+ latestSeq = Math.max(latestSeq, delta.seq);
304
+ }
305
+ const mergedState = Y.mergeUpdatesV2(updates);
306
+ const clientVector = new Uint8Array(args.clientStateVector);
307
+ const diff = Y.diffUpdateV2(mergedState, clientVector);
308
+ const serverVector = Y.encodeStateVectorFromUpdateV2(mergedState);
309
+ logger.info("Recovery sync computed", {
310
+ collection: args.collection,
311
+ snapshotCount: snapshots.length,
312
+ deltaCount: deltas.length,
313
+ diffSize: diff.byteLength,
314
+ hasDiff: diff.byteLength > 0
315
+ });
316
+ return {
317
+ diff: diff.byteLength > 0 ? diff.buffer : void 0,
318
+ serverStateVector: serverVector.buffer,
319
+ cursor: latestSeq
320
+ };
321
+ }
308
322
  });
309
- export { OperationType, deleteDocument, getInitialState, insertDocument, recovery, stream, updateDocument };
323
+
324
+ //#endregion
325
+ export { OperationType, compact, deleteDocument, getInitialState, insertDocument, mark, recovery, stream, updateDocument };