@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.
- package/README.md +356 -420
- package/dist/client/collection.d.ts +78 -76
- package/dist/client/errors.d.ts +59 -0
- package/dist/client/index.d.ts +22 -18
- package/dist/client/logger.d.ts +0 -1
- package/dist/client/merge.d.ts +77 -0
- package/dist/client/persistence/adapters/index.d.ts +8 -0
- package/dist/client/persistence/adapters/opsqlite.d.ts +46 -0
- package/dist/client/persistence/adapters/sqljs.d.ts +83 -0
- package/dist/client/persistence/index.d.ts +49 -0
- package/dist/client/persistence/indexeddb.d.ts +17 -0
- package/dist/client/persistence/memory.d.ts +16 -0
- package/dist/client/persistence/sqlite-browser.d.ts +51 -0
- package/dist/client/persistence/sqlite-level.d.ts +63 -0
- package/dist/client/persistence/sqlite-rn.d.ts +36 -0
- package/dist/client/persistence/sqlite.d.ts +47 -0
- package/dist/client/persistence/types.d.ts +42 -0
- package/dist/client/prose.d.ts +56 -0
- package/dist/client/replicate.d.ts +40 -0
- package/dist/client/services/checkpoint.d.ts +18 -0
- package/dist/client/services/reconciliation.d.ts +24 -0
- package/dist/component/_generated/api.d.ts +35 -0
- package/dist/component/_generated/api.js +3 -3
- package/dist/component/_generated/component.d.ts +89 -0
- package/dist/component/_generated/component.js +0 -0
- package/dist/component/_generated/dataModel.d.ts +45 -0
- package/dist/component/_generated/dataModel.js +0 -0
- package/{src → dist}/component/_generated/server.d.ts +9 -38
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.js +2 -1
- package/dist/component/logger.d.ts +8 -0
- package/dist/component/logger.js +30 -0
- package/dist/component/public.d.ts +36 -61
- package/dist/component/public.js +232 -58
- package/dist/component/schema.d.ts +32 -8
- package/dist/component/schema.js +19 -6
- package/dist/index.js +1553 -308
- package/dist/server/builder.d.ts +94 -0
- package/dist/server/index.d.ts +14 -17
- package/dist/server/schema.d.ts +17 -63
- package/dist/server/storage.d.ts +80 -0
- package/dist/server.js +268 -83
- package/dist/shared/index.d.ts +5 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/types.d.ts +50 -0
- package/dist/shared/types.js +6 -0
- package/dist/shared.js +6 -0
- package/package.json +59 -49
- package/src/client/collection.ts +877 -450
- package/src/client/errors.ts +45 -0
- package/src/client/index.ts +52 -26
- package/src/client/logger.ts +2 -28
- package/src/client/merge.ts +374 -0
- package/src/client/persistence/adapters/index.ts +8 -0
- package/src/client/persistence/adapters/opsqlite.ts +54 -0
- package/src/client/persistence/adapters/sqljs.ts +128 -0
- package/src/client/persistence/index.ts +54 -0
- package/src/client/persistence/indexeddb.ts +110 -0
- package/src/client/persistence/memory.ts +61 -0
- package/src/client/persistence/sqlite-browser.ts +107 -0
- package/src/client/persistence/sqlite-level.ts +407 -0
- package/src/client/persistence/sqlite-rn.ts +44 -0
- package/src/client/persistence/sqlite.ts +161 -0
- package/src/client/persistence/types.ts +49 -0
- package/src/client/prose.ts +369 -0
- package/src/client/replicate.ts +80 -0
- package/src/client/services/checkpoint.ts +86 -0
- package/src/client/services/reconciliation.ts +108 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +103 -0
- package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +1 -1
- package/src/component/_generated/server.ts +161 -0
- package/src/component/convex.config.ts +3 -1
- package/src/component/logger.ts +36 -0
- package/src/component/public.ts +364 -111
- package/src/component/schema.ts +18 -5
- package/src/env.d.ts +31 -0
- package/src/server/builder.ts +85 -0
- package/src/server/index.ts +9 -24
- package/src/server/schema.ts +20 -76
- package/src/server/storage.ts +313 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/types.ts +52 -0
- package/LICENSE.package +0 -201
- package/dist/client/storage.d.ts +0 -143
- package/dist/server/replication.d.ts +0 -122
- package/dist/server/ssr.d.ts +0 -79
- package/dist/ssr.js +0 -19
- package/src/client/storage.ts +0 -206
- package/src/component/_generated/api.d.ts +0 -95
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/server/replication.ts +0 -244
- 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
|
+
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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';
|
package/dist/server/schema.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
*
|
|
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:
|
|
14
|
-
* {
|
|
15
|
-
*
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
const
|
|
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(
|
|
94
|
-
return
|
|
274
|
+
if (applyIndexes) return applyIndexes(tbl);
|
|
275
|
+
return tbl;
|
|
95
276
|
}
|
|
96
|
-
|
|
277
|
+
const schema = {
|
|
278
|
+
table: table,
|
|
279
|
+
prose: prose
|
|
280
|
+
};
|
|
281
|
+
export { replicate, schema };
|