@trestleinc/replicate 1.1.2 → 1.2.0-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 +40 -41
- package/package.json +3 -1
- package/src/client/collection.ts +334 -523
- package/src/client/errors.ts +1 -1
- package/src/client/index.ts +4 -7
- package/src/client/merge.ts +2 -2
- package/src/client/persistence/indexeddb.ts +10 -14
- package/src/client/prose.ts +147 -203
- package/src/client/services/awareness.ts +373 -0
- package/src/client/services/context.ts +114 -0
- package/src/client/services/seq.ts +78 -0
- package/src/client/services/session.ts +20 -0
- package/src/client/services/sync.ts +122 -0
- package/src/client/subdocs.ts +263 -0
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +73 -28
- package/src/component/mutations.ts +734 -0
- package/src/component/schema.ts +31 -14
- package/src/server/collection.ts +98 -0
- package/src/server/index.ts +2 -2
- package/src/server/{storage.ts → replicate.ts} +214 -75
- package/dist/client/index.d.ts +0 -314
- package/dist/client/index.js +0 -4027
- package/dist/component/_generated/api.d.ts +0 -31
- package/dist/component/_generated/api.js +0 -25
- package/dist/component/_generated/component.d.ts +0 -91
- package/dist/component/_generated/component.js +0 -1
- package/dist/component/_generated/dataModel.d.ts +0 -42
- package/dist/component/_generated/dataModel.js +0 -1
- package/dist/component/_generated/server.d.ts +0 -117
- package/dist/component/_generated/server.js +0 -73
- package/dist/component/_virtual/rolldown_runtime.js +0 -18
- package/dist/component/convex.config.d.ts +0 -6
- package/dist/component/convex.config.js +0 -8
- package/dist/component/logger.d.ts +0 -12
- package/dist/component/logger.js +0 -27
- package/dist/component/public.d.ts +0 -83
- package/dist/component/public.js +0 -325
- package/dist/component/schema.d.ts +0 -54
- package/dist/component/schema.js +0 -29
- package/dist/component/shared/types.d.ts +0 -9
- package/dist/component/shared/types.js +0 -15
- package/dist/server/index.d.ts +0 -135
- package/dist/server/index.js +0 -368
- package/dist/shared/index.d.ts +0 -29
- package/dist/shared/index.js +0 -1
- package/src/client/prose-schema.ts +0 -55
- package/src/client/services/cursor.ts +0 -109
- package/src/component/public.ts +0 -453
- package/src/server/builder.ts +0 -98
- /package/src/client/{replicate.ts → ops.ts} +0 -0
package/dist/server/index.js
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import { v } from "convex/values";
|
|
2
|
-
import { defineTable, mutationGeneric, queryGeneric } from "convex/server";
|
|
3
|
-
|
|
4
|
-
//#region src/shared/types.ts
|
|
5
|
-
const SIZE_MULTIPLIERS = {
|
|
6
|
-
kb: 1024,
|
|
7
|
-
mb: 1024 ** 2,
|
|
8
|
-
gb: 1024 ** 3
|
|
9
|
-
};
|
|
10
|
-
const DURATION_MULTIPLIERS = {
|
|
11
|
-
m: 6e4,
|
|
12
|
-
h: 36e5,
|
|
13
|
-
d: 864e5
|
|
14
|
-
};
|
|
15
|
-
function parseSize(s) {
|
|
16
|
-
const match = /^(\d+)(kb|mb|gb)$/i.exec(s);
|
|
17
|
-
if (!match) throw new Error(`Invalid size: ${s}`);
|
|
18
|
-
const [, num, unit] = match;
|
|
19
|
-
return parseInt(num) * SIZE_MULTIPLIERS[unit.toLowerCase()];
|
|
20
|
-
}
|
|
21
|
-
function parseDuration(s) {
|
|
22
|
-
const match = /^(\d+)(m|h|d)$/i.exec(s);
|
|
23
|
-
if (!match) throw new Error(`Invalid duration: ${s}`);
|
|
24
|
-
const [, num, unit] = match;
|
|
25
|
-
return parseInt(num) * DURATION_MULTIPLIERS[unit.toLowerCase()];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
//#endregion
|
|
29
|
-
//#region src/server/storage.ts
|
|
30
|
-
const BYTES_PER_MB = 1024 * 1024;
|
|
31
|
-
const MS_PER_HOUR = 3600 * 1e3;
|
|
32
|
-
const DEFAULT_SIZE_THRESHOLD_5MB = 5 * BYTES_PER_MB;
|
|
33
|
-
const DEFAULT_PEER_TIMEOUT_24H = 24 * MS_PER_HOUR;
|
|
34
|
-
var Replicate = class {
|
|
35
|
-
sizeThreshold;
|
|
36
|
-
peerTimeout;
|
|
37
|
-
constructor(component, collectionName, compaction) {
|
|
38
|
-
this.component = component;
|
|
39
|
-
this.collectionName = collectionName;
|
|
40
|
-
this.sizeThreshold = compaction?.sizeThreshold ? parseSize(compaction.sizeThreshold) : DEFAULT_SIZE_THRESHOLD_5MB;
|
|
41
|
-
this.peerTimeout = compaction?.peerTimeout ? parseDuration(compaction.peerTimeout) : DEFAULT_PEER_TIMEOUT_24H;
|
|
42
|
-
}
|
|
43
|
-
createStreamQuery(opts) {
|
|
44
|
-
const component = this.component;
|
|
45
|
-
const collection = this.collectionName;
|
|
46
|
-
return queryGeneric({
|
|
47
|
-
args: {
|
|
48
|
-
cursor: v.number(),
|
|
49
|
-
limit: v.optional(v.number()),
|
|
50
|
-
sizeThreshold: v.optional(v.number())
|
|
51
|
-
},
|
|
52
|
-
returns: v.object({
|
|
53
|
-
changes: v.array(v.object({
|
|
54
|
-
documentId: v.string(),
|
|
55
|
-
crdtBytes: v.bytes(),
|
|
56
|
-
seq: v.number(),
|
|
57
|
-
operationType: v.string()
|
|
58
|
-
})),
|
|
59
|
-
cursor: v.number(),
|
|
60
|
-
hasMore: v.boolean(),
|
|
61
|
-
compact: v.optional(v.string())
|
|
62
|
-
}),
|
|
63
|
-
handler: async (ctx, args) => {
|
|
64
|
-
if (opts?.evalRead) await opts.evalRead(ctx, collection);
|
|
65
|
-
const result = await ctx.runQuery(component.public.stream, {
|
|
66
|
-
collection,
|
|
67
|
-
cursor: args.cursor,
|
|
68
|
-
limit: args.limit,
|
|
69
|
-
sizeThreshold: args.sizeThreshold
|
|
70
|
-
});
|
|
71
|
-
if (opts?.onStream) await opts.onStream(ctx, result);
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
createSSRQuery(opts) {
|
|
77
|
-
const collection = this.collectionName;
|
|
78
|
-
const component = this.component;
|
|
79
|
-
return queryGeneric({
|
|
80
|
-
args: {},
|
|
81
|
-
returns: v.object({
|
|
82
|
-
documents: v.any(),
|
|
83
|
-
cursor: v.optional(v.number()),
|
|
84
|
-
count: v.number(),
|
|
85
|
-
crdtBytes: v.optional(v.bytes())
|
|
86
|
-
}),
|
|
87
|
-
handler: async (ctx) => {
|
|
88
|
-
if (opts?.evalRead) await opts.evalRead(ctx, collection);
|
|
89
|
-
let docs = await ctx.db.query(collection).collect();
|
|
90
|
-
if (opts?.transform) docs = await opts.transform(docs);
|
|
91
|
-
const response = {
|
|
92
|
-
documents: docs,
|
|
93
|
-
count: docs.length
|
|
94
|
-
};
|
|
95
|
-
if (opts?.includeCRDTState) {
|
|
96
|
-
const crdtState = await ctx.runQuery(component.public.getInitialState, { collection });
|
|
97
|
-
if (crdtState) {
|
|
98
|
-
response.crdtBytes = crdtState.crdtBytes;
|
|
99
|
-
response.cursor = crdtState.cursor;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return response;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
createInsertMutation(opts) {
|
|
107
|
-
const component = this.component;
|
|
108
|
-
const collection = this.collectionName;
|
|
109
|
-
return mutationGeneric({
|
|
110
|
-
args: {
|
|
111
|
-
documentId: v.string(),
|
|
112
|
-
crdtBytes: v.bytes(),
|
|
113
|
-
materializedDoc: v.any()
|
|
114
|
-
},
|
|
115
|
-
returns: v.object({
|
|
116
|
-
success: v.boolean(),
|
|
117
|
-
seq: v.number()
|
|
118
|
-
}),
|
|
119
|
-
handler: async (ctx, args) => {
|
|
120
|
-
const doc = args.materializedDoc;
|
|
121
|
-
if (opts?.evalWrite) await opts.evalWrite(ctx, doc);
|
|
122
|
-
const result = await ctx.runMutation(component.public.insertDocument, {
|
|
123
|
-
collection,
|
|
124
|
-
documentId: args.documentId,
|
|
125
|
-
crdtBytes: args.crdtBytes
|
|
126
|
-
});
|
|
127
|
-
await ctx.db.insert(collection, {
|
|
128
|
-
id: args.documentId,
|
|
129
|
-
...args.materializedDoc,
|
|
130
|
-
timestamp: Date.now()
|
|
131
|
-
});
|
|
132
|
-
if (opts?.onInsert) await opts.onInsert(ctx, doc);
|
|
133
|
-
return {
|
|
134
|
-
success: true,
|
|
135
|
-
seq: result.seq
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
createUpdateMutation(opts) {
|
|
141
|
-
const component = this.component;
|
|
142
|
-
const collection = this.collectionName;
|
|
143
|
-
return mutationGeneric({
|
|
144
|
-
args: {
|
|
145
|
-
documentId: v.string(),
|
|
146
|
-
crdtBytes: v.bytes(),
|
|
147
|
-
materializedDoc: v.any()
|
|
148
|
-
},
|
|
149
|
-
returns: v.object({
|
|
150
|
-
success: v.boolean(),
|
|
151
|
-
seq: v.number()
|
|
152
|
-
}),
|
|
153
|
-
handler: async (ctx, args) => {
|
|
154
|
-
const doc = args.materializedDoc;
|
|
155
|
-
if (opts?.evalWrite) await opts.evalWrite(ctx, doc);
|
|
156
|
-
const result = await ctx.runMutation(component.public.updateDocument, {
|
|
157
|
-
collection,
|
|
158
|
-
documentId: args.documentId,
|
|
159
|
-
crdtBytes: args.crdtBytes
|
|
160
|
-
});
|
|
161
|
-
const existing = await ctx.db.query(collection).withIndex("by_doc_id", (q) => q.eq("id", args.documentId)).first();
|
|
162
|
-
if (existing) await ctx.db.patch(existing._id, {
|
|
163
|
-
...args.materializedDoc,
|
|
164
|
-
timestamp: Date.now()
|
|
165
|
-
});
|
|
166
|
-
if (opts?.onUpdate) await opts.onUpdate(ctx, doc);
|
|
167
|
-
return {
|
|
168
|
-
success: true,
|
|
169
|
-
seq: result.seq
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
createRemoveMutation(opts) {
|
|
175
|
-
const component = this.component;
|
|
176
|
-
const collection = this.collectionName;
|
|
177
|
-
return mutationGeneric({
|
|
178
|
-
args: {
|
|
179
|
-
documentId: v.string(),
|
|
180
|
-
crdtBytes: v.bytes()
|
|
181
|
-
},
|
|
182
|
-
returns: v.object({
|
|
183
|
-
success: v.boolean(),
|
|
184
|
-
seq: v.number()
|
|
185
|
-
}),
|
|
186
|
-
handler: async (ctx, args) => {
|
|
187
|
-
const documentId = args.documentId;
|
|
188
|
-
if (opts?.evalRemove) await opts.evalRemove(ctx, documentId);
|
|
189
|
-
const result = await ctx.runMutation(component.public.deleteDocument, {
|
|
190
|
-
collection,
|
|
191
|
-
documentId,
|
|
192
|
-
crdtBytes: args.crdtBytes
|
|
193
|
-
});
|
|
194
|
-
const existing = await ctx.db.query(collection).withIndex("by_doc_id", (q) => q.eq("id", documentId)).first();
|
|
195
|
-
if (existing) await ctx.db.delete(existing._id);
|
|
196
|
-
if (opts?.onRemove) await opts.onRemove(ctx, documentId);
|
|
197
|
-
return {
|
|
198
|
-
success: true,
|
|
199
|
-
seq: result.seq
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
createMarkMutation(opts) {
|
|
205
|
-
const component = this.component;
|
|
206
|
-
const collection = this.collectionName;
|
|
207
|
-
return mutationGeneric({
|
|
208
|
-
args: {
|
|
209
|
-
peerId: v.string(),
|
|
210
|
-
syncedSeq: v.number()
|
|
211
|
-
},
|
|
212
|
-
returns: v.null(),
|
|
213
|
-
handler: async (ctx, args) => {
|
|
214
|
-
if (opts?.evalWrite) await opts.evalWrite(ctx, args.peerId);
|
|
215
|
-
await ctx.runMutation(component.public.mark, {
|
|
216
|
-
collection,
|
|
217
|
-
peerId: args.peerId,
|
|
218
|
-
syncedSeq: args.syncedSeq
|
|
219
|
-
});
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
createCompactMutation(opts) {
|
|
225
|
-
const component = this.component;
|
|
226
|
-
const collection = this.collectionName;
|
|
227
|
-
return mutationGeneric({
|
|
228
|
-
args: {
|
|
229
|
-
documentId: v.string(),
|
|
230
|
-
snapshotBytes: v.bytes(),
|
|
231
|
-
stateVector: v.bytes(),
|
|
232
|
-
peerTimeout: v.optional(v.number())
|
|
233
|
-
},
|
|
234
|
-
returns: v.object({
|
|
235
|
-
success: v.boolean(),
|
|
236
|
-
removed: v.number(),
|
|
237
|
-
retained: v.number()
|
|
238
|
-
}),
|
|
239
|
-
handler: async (ctx, args) => {
|
|
240
|
-
if (opts?.evalWrite) await opts.evalWrite(ctx, args.documentId);
|
|
241
|
-
return await ctx.runMutation(component.public.compact, {
|
|
242
|
-
collection,
|
|
243
|
-
documentId: args.documentId,
|
|
244
|
-
snapshotBytes: args.snapshotBytes,
|
|
245
|
-
stateVector: args.stateVector,
|
|
246
|
-
peerTimeout: args.peerTimeout
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
createRecoveryQuery(opts) {
|
|
252
|
-
const component = this.component;
|
|
253
|
-
const collection = this.collectionName;
|
|
254
|
-
return queryGeneric({
|
|
255
|
-
args: { clientStateVector: v.bytes() },
|
|
256
|
-
returns: v.object({
|
|
257
|
-
diff: v.optional(v.bytes()),
|
|
258
|
-
serverStateVector: v.bytes(),
|
|
259
|
-
cursor: v.number()
|
|
260
|
-
}),
|
|
261
|
-
handler: async (ctx, args) => {
|
|
262
|
-
if (opts?.evalRead) await opts.evalRead(ctx, collection);
|
|
263
|
-
return await ctx.runQuery(component.public.recovery, {
|
|
264
|
-
collection,
|
|
265
|
-
clientStateVector: args.clientStateVector
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
//#endregion
|
|
273
|
-
//#region src/server/builder.ts
|
|
274
|
-
/**
|
|
275
|
-
* Create a replicate function bound to your component. Call this once in your
|
|
276
|
-
* convex/replicate.ts file, then use the returned function for all collections.
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* ```typescript
|
|
280
|
-
* // convex/replicate.ts (create once)
|
|
281
|
-
* import { replicate } from '@trestleinc/replicate/server';
|
|
282
|
-
* import { components } from './_generated/api';
|
|
283
|
-
*
|
|
284
|
-
* export const tasks = replicate(components.replicate)<Task>({ collection: 'tasks' });
|
|
285
|
-
*
|
|
286
|
-
* // Or bind once and reuse:
|
|
287
|
-
* const r = replicate(components.replicate);
|
|
288
|
-
* export const tasks = r<Task>({ collection: 'tasks' });
|
|
289
|
-
* export const notebooks = r<Notebook>({ collection: 'notebooks' });
|
|
290
|
-
* ```
|
|
291
|
-
*/
|
|
292
|
-
function replicate(component) {
|
|
293
|
-
return function boundReplicate(config) {
|
|
294
|
-
return replicateInternal(component, config);
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Internal implementation for replicate.
|
|
299
|
-
*/
|
|
300
|
-
function replicateInternal(component, config) {
|
|
301
|
-
const storage = new Replicate(component, config.collection, config.compaction);
|
|
302
|
-
return {
|
|
303
|
-
__collection: config.collection,
|
|
304
|
-
stream: storage.createStreamQuery({
|
|
305
|
-
evalRead: config.hooks?.evalRead,
|
|
306
|
-
onStream: config.hooks?.onStream
|
|
307
|
-
}),
|
|
308
|
-
material: storage.createSSRQuery({
|
|
309
|
-
evalRead: config.hooks?.evalRead,
|
|
310
|
-
transform: config.hooks?.transform
|
|
311
|
-
}),
|
|
312
|
-
recovery: storage.createRecoveryQuery({ evalRead: config.hooks?.evalRead }),
|
|
313
|
-
insert: storage.createInsertMutation({
|
|
314
|
-
evalWrite: config.hooks?.evalWrite,
|
|
315
|
-
onInsert: config.hooks?.onInsert
|
|
316
|
-
}),
|
|
317
|
-
update: storage.createUpdateMutation({
|
|
318
|
-
evalWrite: config.hooks?.evalWrite,
|
|
319
|
-
onUpdate: config.hooks?.onUpdate
|
|
320
|
-
}),
|
|
321
|
-
remove: storage.createRemoveMutation({
|
|
322
|
-
evalRemove: config.hooks?.evalRemove,
|
|
323
|
-
onRemove: config.hooks?.onRemove
|
|
324
|
-
}),
|
|
325
|
-
mark: storage.createMarkMutation({ evalWrite: config.hooks?.evalMark }),
|
|
326
|
-
compact: storage.createCompactMutation({ evalWrite: config.hooks?.evalCompact })
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
//#endregion
|
|
331
|
-
//#region src/server/schema.ts
|
|
332
|
-
const prose = () => v.object({
|
|
333
|
-
type: v.literal("doc"),
|
|
334
|
-
content: v.optional(v.array(v.any()))
|
|
335
|
-
});
|
|
336
|
-
/**
|
|
337
|
-
* Define a table with automatic timestamp field for replication.
|
|
338
|
-
* All replicated tables must have an `id` field and define a `by_doc_id` index.
|
|
339
|
-
*
|
|
340
|
-
* @example
|
|
341
|
-
* ```typescript
|
|
342
|
-
* // convex/schema.ts
|
|
343
|
-
* export default defineSchema({
|
|
344
|
-
* tasks: table(
|
|
345
|
-
* { id: v.string(), text: v.string(), isCompleted: v.boolean() },
|
|
346
|
-
* (t) => t.index('by_doc_id', ['id']).index('by_completed', ['isCompleted'])
|
|
347
|
-
* ),
|
|
348
|
-
* });
|
|
349
|
-
* ```
|
|
350
|
-
*/
|
|
351
|
-
function table(userFields, applyIndexes) {
|
|
352
|
-
const tbl = defineTable({
|
|
353
|
-
...userFields,
|
|
354
|
-
timestamp: v.number()
|
|
355
|
-
});
|
|
356
|
-
if (applyIndexes) return applyIndexes(tbl);
|
|
357
|
-
return tbl;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
//#endregion
|
|
361
|
-
//#region src/server/index.ts
|
|
362
|
-
const schema = {
|
|
363
|
-
table,
|
|
364
|
-
prose
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
//#endregion
|
|
368
|
-
export { replicate, schema };
|
package/dist/shared/index.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
//#region src/shared/types.d.ts
|
|
2
|
-
|
|
3
|
-
/** ProseMirror-compatible JSON for XmlFragment serialization */
|
|
4
|
-
interface XmlFragmentJSON {
|
|
5
|
-
type: "doc";
|
|
6
|
-
content?: XmlNodeJSON[];
|
|
7
|
-
}
|
|
8
|
-
declare const PROSE_BRAND: unique symbol;
|
|
9
|
-
/**
|
|
10
|
-
* Branded prose type for Zod schemas.
|
|
11
|
-
* Extends XmlFragmentJSON with a unique brand for type-level detection.
|
|
12
|
-
* Use the `prose()` helper from `@trestleinc/replicate/client` to create this type.
|
|
13
|
-
*/
|
|
14
|
-
interface ProseValue extends XmlFragmentJSON {
|
|
15
|
-
readonly [PROSE_BRAND]: typeof PROSE_BRAND;
|
|
16
|
-
}
|
|
17
|
-
/** ProseMirror node structure */
|
|
18
|
-
interface XmlNodeJSON {
|
|
19
|
-
type: string;
|
|
20
|
-
attrs?: Record<string, unknown>;
|
|
21
|
-
content?: XmlNodeJSON[];
|
|
22
|
-
text?: string;
|
|
23
|
-
marks?: {
|
|
24
|
-
type: string;
|
|
25
|
-
attrs?: Record<string, unknown>;
|
|
26
|
-
}[];
|
|
27
|
-
}
|
|
28
|
-
//#endregion
|
|
29
|
-
export { type ProseValue, type XmlFragmentJSON, type XmlNodeJSON };
|
package/dist/shared/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { ProseValue } from "$/shared/types";
|
|
3
|
-
|
|
4
|
-
const PROSE_MARKER = Symbol.for("replicate:prose");
|
|
5
|
-
|
|
6
|
-
function createProseSchema(): z.ZodType<ProseValue> {
|
|
7
|
-
const schema = z.custom<ProseValue>(
|
|
8
|
-
(val) => {
|
|
9
|
-
if (val == null) return true;
|
|
10
|
-
if (typeof val !== "object") return false;
|
|
11
|
-
return (val as { type?: string }).type === "doc";
|
|
12
|
-
},
|
|
13
|
-
{ message: "Expected prose document with type \"doc\"" },
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
Object.defineProperty(schema, PROSE_MARKER, { value: true, writable: false });
|
|
17
|
-
|
|
18
|
-
return schema;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function emptyProse(): ProseValue {
|
|
22
|
-
return { type: "doc", content: [] } as unknown as ProseValue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function prose(): z.ZodType<ProseValue> {
|
|
26
|
-
return createProseSchema();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
prose.empty = emptyProse;
|
|
30
|
-
|
|
31
|
-
export function isProseSchema(schema: unknown): boolean {
|
|
32
|
-
return (
|
|
33
|
-
schema != null
|
|
34
|
-
&& typeof schema === "object"
|
|
35
|
-
&& PROSE_MARKER in schema
|
|
36
|
-
&& (schema as Record<symbol, unknown>)[PROSE_MARKER] === true
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function extractProseFields(schema: z.ZodObject<z.ZodRawShape>): string[] {
|
|
41
|
-
const fields: string[] = [];
|
|
42
|
-
|
|
43
|
-
for (const [key, fieldSchema] of Object.entries(schema.shape)) {
|
|
44
|
-
let unwrapped = fieldSchema;
|
|
45
|
-
while (unwrapped instanceof z.ZodOptional || unwrapped instanceof z.ZodNullable) {
|
|
46
|
-
unwrapped = unwrapped.unwrap();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (isProseSchema(unwrapped)) {
|
|
50
|
-
fields.push(key);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return fields;
|
|
55
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { Effect, Context, Layer } from "effect";
|
|
2
|
-
import { IDBError, IDBWriteError } from "$/client/errors";
|
|
3
|
-
import type { KeyValueStore } from "$/client/persistence/types";
|
|
4
|
-
|
|
5
|
-
export type Cursor = number;
|
|
6
|
-
|
|
7
|
-
export class CursorService extends Context.Tag("CursorService")<
|
|
8
|
-
CursorService,
|
|
9
|
-
{
|
|
10
|
-
readonly loadCursor: (collection: string) => Effect.Effect<Cursor, IDBError>;
|
|
11
|
-
readonly saveCursor: (collection: string, cursor: Cursor) => Effect.Effect<void, IDBWriteError>;
|
|
12
|
-
readonly clearCursor: (collection: string) => Effect.Effect<void, IDBError>;
|
|
13
|
-
readonly loadPeerId: (collection: string) => Effect.Effect<string, IDBError | IDBWriteError>;
|
|
14
|
-
}
|
|
15
|
-
>() {}
|
|
16
|
-
|
|
17
|
-
function generatePeerId(): string {
|
|
18
|
-
return crypto.randomUUID();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function createCursorLayer(kv: KeyValueStore) {
|
|
22
|
-
return Layer.succeed(
|
|
23
|
-
CursorService,
|
|
24
|
-
CursorService.of({
|
|
25
|
-
loadCursor: collection =>
|
|
26
|
-
Effect.gen(function* (_) {
|
|
27
|
-
const key = `cursor:${collection}`;
|
|
28
|
-
const stored = yield* _(
|
|
29
|
-
Effect.tryPromise({
|
|
30
|
-
try: () => kv.get<Cursor>(key),
|
|
31
|
-
catch: cause => new IDBError({ operation: "get", key, cause }),
|
|
32
|
-
}),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
if (stored !== undefined) {
|
|
36
|
-
yield* _(
|
|
37
|
-
Effect.logDebug("Loaded cursor from storage", {
|
|
38
|
-
collection,
|
|
39
|
-
cursor: stored,
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
42
|
-
return stored;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
yield* _(
|
|
46
|
-
Effect.logDebug("No stored cursor, using default", {
|
|
47
|
-
collection,
|
|
48
|
-
}),
|
|
49
|
-
);
|
|
50
|
-
return 0;
|
|
51
|
-
}),
|
|
52
|
-
|
|
53
|
-
saveCursor: (collection, cursor) =>
|
|
54
|
-
Effect.gen(function* (_) {
|
|
55
|
-
const key = `cursor:${collection}`;
|
|
56
|
-
yield* _(
|
|
57
|
-
Effect.tryPromise({
|
|
58
|
-
try: () => kv.set(key, cursor),
|
|
59
|
-
catch: cause => new IDBWriteError({ key, value: cursor, cause }),
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
62
|
-
yield* _(
|
|
63
|
-
Effect.logDebug("Cursor saved", {
|
|
64
|
-
collection,
|
|
65
|
-
cursor,
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
}),
|
|
69
|
-
|
|
70
|
-
clearCursor: collection =>
|
|
71
|
-
Effect.gen(function* (_) {
|
|
72
|
-
const key = `cursor:${collection}`;
|
|
73
|
-
yield* _(
|
|
74
|
-
Effect.tryPromise({
|
|
75
|
-
try: () => kv.del(key),
|
|
76
|
-
catch: cause => new IDBError({ operation: "delete", key, cause }),
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
yield* _(Effect.logDebug("Cursor cleared", { collection }));
|
|
80
|
-
}),
|
|
81
|
-
|
|
82
|
-
loadPeerId: collection =>
|
|
83
|
-
Effect.gen(function* (_) {
|
|
84
|
-
const key = `peerId:${collection}`;
|
|
85
|
-
const stored = yield* _(
|
|
86
|
-
Effect.tryPromise({
|
|
87
|
-
try: () => kv.get<string>(key),
|
|
88
|
-
catch: cause => new IDBError({ operation: "get", key, cause }),
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
if (stored) {
|
|
93
|
-
yield* _(Effect.logDebug("Loaded peerId from storage", { collection, peerId: stored }));
|
|
94
|
-
return stored;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const newPeerId = generatePeerId();
|
|
98
|
-
yield* _(
|
|
99
|
-
Effect.tryPromise({
|
|
100
|
-
try: () => kv.set(key, newPeerId),
|
|
101
|
-
catch: cause => new IDBWriteError({ key, value: newPeerId, cause }),
|
|
102
|
-
}),
|
|
103
|
-
);
|
|
104
|
-
yield* _(Effect.logDebug("Generated new peerId", { collection, peerId: newPeerId }));
|
|
105
|
-
return newPeerId;
|
|
106
|
-
}),
|
|
107
|
-
}),
|
|
108
|
-
);
|
|
109
|
-
}
|