@trestleinc/replicate 0.1.0 → 1.1.0

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