@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.
- package/README.md +446 -260
- package/dist/client/index.d.ts +311 -19
- package/dist/client/index.js +4027 -0
- package/dist/component/_generated/api.d.ts +13 -17
- package/dist/component/_generated/api.js +24 -4
- package/dist/component/_generated/component.d.ts +79 -77
- package/dist/component/_generated/component.js +1 -0
- package/dist/component/_generated/dataModel.d.ts +12 -15
- package/dist/component/_generated/dataModel.js +1 -0
- package/dist/component/_generated/server.d.ts +19 -22
- package/dist/component/_generated/server.js +65 -1
- package/dist/component/_virtual/rolldown_runtime.js +18 -0
- package/dist/component/convex.config.d.ts +6 -2
- package/dist/component/convex.config.js +7 -3
- package/dist/component/logger.d.ts +10 -6
- package/dist/component/logger.js +25 -28
- package/dist/component/public.d.ts +70 -61
- package/dist/component/public.js +311 -295
- package/dist/component/schema.d.ts +53 -45
- package/dist/component/schema.js +26 -32
- package/dist/component/shared/types.d.ts +9 -0
- package/dist/component/shared/types.js +15 -0
- package/dist/server/index.d.ts +134 -13
- package/dist/server/index.js +368 -0
- package/dist/shared/index.d.ts +27 -3
- package/dist/shared/index.js +1 -2
- package/package.json +34 -29
- package/src/client/collection.ts +339 -306
- package/src/client/errors.ts +9 -9
- package/src/client/index.ts +13 -32
- package/src/client/logger.ts +2 -2
- package/src/client/merge.ts +37 -34
- package/src/client/persistence/custom.ts +84 -0
- package/src/client/persistence/index.ts +9 -46
- package/src/client/persistence/indexeddb.ts +111 -84
- package/src/client/persistence/memory.ts +3 -3
- package/src/client/persistence/sqlite/browser.ts +168 -0
- package/src/client/persistence/sqlite/native.ts +29 -0
- package/src/client/persistence/sqlite/schema.ts +124 -0
- package/src/client/persistence/types.ts +32 -28
- package/src/client/prose-schema.ts +55 -0
- package/src/client/prose.ts +28 -25
- package/src/client/replicate.ts +5 -5
- package/src/client/services/cursor.ts +109 -0
- package/src/component/_generated/component.ts +31 -29
- package/src/component/convex.config.ts +2 -2
- package/src/component/logger.ts +7 -7
- package/src/component/public.ts +225 -237
- package/src/component/schema.ts +18 -15
- package/src/server/builder.ts +20 -7
- package/src/server/index.ts +3 -5
- package/src/server/schema.ts +5 -5
- package/src/server/storage.ts +113 -59
- package/src/shared/index.ts +5 -5
- package/src/shared/types.ts +51 -14
- package/dist/client/collection.d.ts +0 -96
- package/dist/client/errors.d.ts +0 -59
- package/dist/client/logger.d.ts +0 -2
- package/dist/client/merge.d.ts +0 -77
- package/dist/client/persistence/adapters/index.d.ts +0 -8
- package/dist/client/persistence/adapters/opsqlite.d.ts +0 -46
- package/dist/client/persistence/adapters/sqljs.d.ts +0 -83
- package/dist/client/persistence/index.d.ts +0 -49
- package/dist/client/persistence/indexeddb.d.ts +0 -17
- package/dist/client/persistence/memory.d.ts +0 -16
- package/dist/client/persistence/sqlite-browser.d.ts +0 -51
- package/dist/client/persistence/sqlite-level.d.ts +0 -63
- package/dist/client/persistence/sqlite-rn.d.ts +0 -36
- package/dist/client/persistence/sqlite.d.ts +0 -47
- package/dist/client/persistence/types.d.ts +0 -42
- package/dist/client/prose.d.ts +0 -56
- package/dist/client/replicate.d.ts +0 -40
- package/dist/client/services/checkpoint.d.ts +0 -18
- package/dist/client/services/reconciliation.d.ts +0 -24
- package/dist/index.js +0 -1620
- package/dist/server/builder.d.ts +0 -94
- package/dist/server/schema.d.ts +0 -27
- package/dist/server/storage.d.ts +0 -80
- package/dist/server.js +0 -281
- package/dist/shared/types.d.ts +0 -50
- package/dist/shared/types.js +0 -6
- package/dist/shared.js +0 -6
- package/src/client/persistence/adapters/index.ts +0 -8
- package/src/client/persistence/adapters/opsqlite.ts +0 -54
- package/src/client/persistence/adapters/sqljs.ts +0 -128
- package/src/client/persistence/sqlite-browser.ts +0 -107
- package/src/client/persistence/sqlite-level.ts +0 -407
- package/src/client/persistence/sqlite-rn.ts +0 -44
- package/src/client/persistence/sqlite.ts +0 -161
- package/src/client/services/checkpoint.ts +0 -86
- package/src/client/services/reconciliation.ts +0 -108
package/src/component/schema.ts
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
import { defineSchema, defineTable } from
|
|
2
|
-
import { v } from
|
|
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
|
-
|
|
10
|
-
timestamp: v.number(),
|
|
9
|
+
seq: v.number(),
|
|
11
10
|
})
|
|
12
|
-
.index(
|
|
13
|
-
.index(
|
|
14
|
-
.index(
|
|
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
|
-
|
|
19
|
+
stateVector: v.bytes(),
|
|
20
|
+
snapshotSeq: v.number(),
|
|
21
21
|
createdAt: v.number(),
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
),
|
|
28
|
-
|
|
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
|
});
|
package/src/server/builder.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from
|
|
2
|
-
import { Replicate } from
|
|
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
|
-
|
|
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
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
export { replicate } from
|
|
2
|
-
export type { ReplicateConfig } from
|
|
1
|
+
export { replicate } from "$/server/builder";
|
|
2
|
+
export type { ReplicateConfig } from "$/server/builder";
|
|
3
3
|
|
|
4
|
-
import { table, prose } from
|
|
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';
|
package/src/server/schema.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { defineTable } from
|
|
2
|
-
import { v } from
|
|
1
|
+
import { defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
3
|
|
|
4
4
|
/** Fields automatically added to replicated tables */
|
|
5
|
-
export
|
|
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(
|
|
11
|
+
type: v.literal("doc"),
|
|
12
12
|
content: v.optional(v.array(v.any())),
|
|
13
13
|
});
|
|
14
14
|
|
package/src/server/storage.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import { v } from
|
|
2
|
-
import type { GenericMutationCtx, GenericQueryCtx, GenericDataModel } from
|
|
3
|
-
import { queryGeneric, mutationGeneric } from
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
cursor: v.number(),
|
|
22
38
|
limit: v.optional(v.number()),
|
|
23
|
-
|
|
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.
|
|
44
|
+
documentId: v.string(),
|
|
29
45
|
crdtBytes: v.bytes(),
|
|
30
|
-
|
|
31
|
-
timestamp: v.number(),
|
|
46
|
+
seq: v.number(),
|
|
32
47
|
operationType: v.string(),
|
|
33
|
-
})
|
|
48
|
+
}),
|
|
34
49
|
),
|
|
35
|
-
|
|
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
|
-
|
|
60
|
+
cursor: args.cursor,
|
|
45
61
|
limit: args.limit,
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
240
|
+
seq: v.number(),
|
|
246
241
|
}),
|
|
247
242
|
handler: async (ctx, args) => {
|
|
248
|
-
const documentId = args.documentId
|
|
243
|
+
const documentId = args.documentId;
|
|
249
244
|
if (opts?.evalRemove) {
|
|
250
245
|
await opts.evalRemove(ctx, documentId);
|
|
251
246
|
}
|
|
252
247
|
|
|
253
|
-
const
|
|
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(
|
|
256
|
+
.withIndex("by_doc_id", q => q.eq("id", documentId))
|
|
265
257
|
.first();
|
|
266
258
|
|
|
267
259
|
if (existing) {
|
|
268
|
-
await ctx.db.delete(
|
|
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
|
-
|
|
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) {
|
package/src/shared/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export type {
|
|
2
|
+
ProseValue,
|
|
3
|
+
XmlFragmentJSON,
|
|
4
|
+
XmlNodeJSON,
|
|
5
|
+
} from "./types.js";
|
package/src/shared/types.ts
CHANGED
|
@@ -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
|
|
37
|
-
* Used for type-safe prose field
|
|
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
|
|
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 {};
|