@trestleinc/replicate 0.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/LICENSE +201 -0
- package/LICENSE.package +201 -0
- package/README.md +871 -0
- package/dist/client/collection.d.ts +94 -0
- package/dist/client/index.d.ts +18 -0
- package/dist/client/logger.d.ts +3 -0
- package/dist/client/storage.d.ts +143 -0
- package/dist/component/_generated/api.js +5 -0
- package/dist/component/_generated/server.js +9 -0
- package/dist/component/convex.config.d.ts +2 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/public.d.ts +99 -0
- package/dist/component/public.js +135 -0
- package/dist/component/schema.d.ts +22 -0
- package/dist/component/schema.js +22 -0
- package/dist/index.js +375 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/replication.d.ts +122 -0
- package/dist/server/schema.d.ts +73 -0
- package/dist/server/ssr.d.ts +79 -0
- package/dist/server.js +96 -0
- package/dist/ssr.js +19 -0
- package/package.json +108 -0
- package/src/client/collection.ts +550 -0
- package/src/client/index.ts +31 -0
- package/src/client/logger.ts +31 -0
- package/src/client/storage.ts +206 -0
- package/src/component/_generated/api.d.ts +95 -0
- package/src/component/_generated/api.js +23 -0
- package/src/component/_generated/dataModel.d.ts +60 -0
- package/src/component/_generated/server.d.ts +149 -0
- package/src/component/_generated/server.js +90 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/public.ts +212 -0
- package/src/component/schema.ts +16 -0
- package/src/server/index.ts +26 -0
- package/src/server/replication.ts +244 -0
- package/src/server/schema.ts +97 -0
- package/src/server/ssr.ts +106 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ConvexClient } from 'convex/browser';
|
|
2
|
+
import type { FunctionReference } from 'convex/server';
|
|
3
|
+
import type { CollectionConfig, Collection } from '@tanstack/db';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for convexCollectionOptions (Step 1)
|
|
6
|
+
* All params go here - they'll be used to create the collection config
|
|
7
|
+
*/
|
|
8
|
+
export interface ConvexCollectionOptionsConfig<T extends object> {
|
|
9
|
+
/** Function to extract unique key from items */
|
|
10
|
+
getKey: (item: T) => string | number;
|
|
11
|
+
/** Optional initial data to populate collection */
|
|
12
|
+
initialData?: ReadonlyArray<T>;
|
|
13
|
+
/** Convex client instance */
|
|
14
|
+
convexClient: ConvexClient;
|
|
15
|
+
/** Convex API functions for this collection */
|
|
16
|
+
api: {
|
|
17
|
+
stream: FunctionReference<'query'>;
|
|
18
|
+
insertDocument: FunctionReference<'mutation'>;
|
|
19
|
+
updateDocument: FunctionReference<'mutation'>;
|
|
20
|
+
deleteDocument: FunctionReference<'mutation'>;
|
|
21
|
+
};
|
|
22
|
+
/** Unique collection name */
|
|
23
|
+
collectionName: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* ConvexCollection is now just a standard TanStack DB Collection!
|
|
27
|
+
* No custom wrapper, no special methods - uses built-in transaction system.
|
|
28
|
+
*/
|
|
29
|
+
export type ConvexCollection<T extends object> = Collection<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Step 1: Create TanStack DB CollectionConfig with REAL mutation handlers.
|
|
32
|
+
*
|
|
33
|
+
* This implements the CORRECT pattern:
|
|
34
|
+
* - Uses onInsert/onUpdate/onDelete handlers (not custom wrapper)
|
|
35
|
+
* - Yjs Y.Doc with 'update' event for delta encoding
|
|
36
|
+
* - Stores Y.Map instances (not plain objects) for field-level CRDT
|
|
37
|
+
* - Uses ydoc.transact() to batch changes into single 'update' event
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { createCollection } from '@tanstack/react-db'
|
|
42
|
+
* import { convexCollectionOptions } from '@trestleinc/convex-replicate-core'
|
|
43
|
+
*
|
|
44
|
+
* const rawCollection = createCollection(
|
|
45
|
+
* convexCollectionOptions<Task>({
|
|
46
|
+
* convexClient,
|
|
47
|
+
* api: api.tasks,
|
|
48
|
+
* collectionName: 'tasks',
|
|
49
|
+
* getKey: (task) => task.id,
|
|
50
|
+
* initialData,
|
|
51
|
+
* })
|
|
52
|
+
* )
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function convexCollectionOptions<T extends object>({ getKey, initialData, convexClient, api, collectionName, }: ConvexCollectionOptionsConfig<T>): CollectionConfig<T> & {
|
|
56
|
+
_convexClient: ConvexClient;
|
|
57
|
+
_collectionName: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Step 2: Wrap collection with offline support.
|
|
61
|
+
*
|
|
62
|
+
* This implements the CORRECT pattern:
|
|
63
|
+
* - Wraps collection ONCE with startOfflineExecutor
|
|
64
|
+
* - Returns raw collection (NO CUSTOM WRAPPER)
|
|
65
|
+
* - Uses beforeRetry filter for stale transactions
|
|
66
|
+
* - Connects to Convex connection state for retry triggers
|
|
67
|
+
*
|
|
68
|
+
* Config is automatically extracted from the rawCollection!
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { createCollection } from '@tanstack/react-db'
|
|
73
|
+
* import { convexCollectionOptions, createConvexCollection } from '@trestleinc/convex-replicate-core'
|
|
74
|
+
*
|
|
75
|
+
* // Step 1: Create raw collection with ALL config
|
|
76
|
+
* const rawCollection = createCollection(
|
|
77
|
+
* convexCollectionOptions<Task>({
|
|
78
|
+
* convexClient,
|
|
79
|
+
* api: api.tasks,
|
|
80
|
+
* collectionName: 'tasks',
|
|
81
|
+
* getKey: (task) => task.id,
|
|
82
|
+
* initialData,
|
|
83
|
+
* })
|
|
84
|
+
* )
|
|
85
|
+
*
|
|
86
|
+
* // Step 2: Wrap with offline support - params automatically extracted!
|
|
87
|
+
* const collection = createConvexCollection(rawCollection)
|
|
88
|
+
*
|
|
89
|
+
* // Use like a normal TanStack DB collection
|
|
90
|
+
* const tx = collection.insert({ id: '1', text: 'Buy milk', isCompleted: false })
|
|
91
|
+
* await tx.isPersisted.promise // Built-in promise (not custom awaitReplication)
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare function createConvexCollection<T extends object>(rawCollection: Collection<T>): ConvexCollection<T>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side utilities for browser/React code.
|
|
3
|
+
* Import this in your frontend components.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* // src/useTasks.ts
|
|
8
|
+
* import {
|
|
9
|
+
* convexCollectionOptions,
|
|
10
|
+
* createConvexCollection,
|
|
11
|
+
* type ConvexCollection,
|
|
12
|
+
* } from '@trestleinc/replicate/client';
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export { ReplicateStorage } from './storage.js';
|
|
16
|
+
export { convexCollectionOptions, createConvexCollection, type ConvexCollection, type ConvexCollectionOptionsConfig, } from './collection.js';
|
|
17
|
+
export * as Y from 'yjs';
|
|
18
|
+
export { NonRetriableError } from '@tanstack/offline-transactions';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Expand, FunctionReference, GenericDataModel, GenericMutationCtx, GenericQueryCtx } from 'convex/server';
|
|
2
|
+
import type { GenericId } from 'convex/values';
|
|
3
|
+
import { api } from '../component/_generated/api';
|
|
4
|
+
/**
|
|
5
|
+
* A client API for interacting with the Convex Replicate storage component.
|
|
6
|
+
*
|
|
7
|
+
* This class provides a type-safe, scoped interface for storing and retrieving
|
|
8
|
+
* CRDT document data from the replicate component. Each instance is scoped to
|
|
9
|
+
* a specific collection name, eliminating the need to pass collectionName to
|
|
10
|
+
* every method call.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { components } from "./_generated/api";
|
|
15
|
+
* import { ReplicateStorage } from "@trestleinc/convex-replicate-component";
|
|
16
|
+
*
|
|
17
|
+
* // Create a storage instance for the "tasks" collection
|
|
18
|
+
* const tasksStorage = new ReplicateStorage(components.replicate, "tasks");
|
|
19
|
+
*
|
|
20
|
+
* export const submitTask = mutation({
|
|
21
|
+
* handler: async (ctx, args) => {
|
|
22
|
+
* return await tasksStorage.submitDocument(
|
|
23
|
+
* ctx,
|
|
24
|
+
* args.id,
|
|
25
|
+
* args.document,
|
|
26
|
+
* args.version
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* export const getTasks = query({
|
|
32
|
+
* handler: async (ctx, args) => {
|
|
33
|
+
* return await tasksStorage.stream(ctx, args.checkpoint, args.limit);
|
|
34
|
+
* }
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @template TDocument - The document type being stored (must have an id field)
|
|
39
|
+
*/
|
|
40
|
+
export declare class ReplicateStorage<_TDocument extends {
|
|
41
|
+
id: string;
|
|
42
|
+
} = {
|
|
43
|
+
id: string;
|
|
44
|
+
}> {
|
|
45
|
+
private component;
|
|
46
|
+
private collectionName;
|
|
47
|
+
/**
|
|
48
|
+
* Create a new ReplicateStorage instance scoped to a specific collection.
|
|
49
|
+
*
|
|
50
|
+
* @param component - The replicate component from your generated API
|
|
51
|
+
* @param collectionName - The name of the collection to interact with
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const tasksStorage = new ReplicateStorage(
|
|
56
|
+
* components.replicate,
|
|
57
|
+
* "tasks"
|
|
58
|
+
* );
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
constructor(component: UseApi<typeof api>, collectionName: string);
|
|
62
|
+
/**
|
|
63
|
+
* Insert a new document into the replicate component storage.
|
|
64
|
+
*
|
|
65
|
+
* This stores the CRDT bytes in the component's internal storage table.
|
|
66
|
+
*
|
|
67
|
+
* @param ctx - Convex mutation context
|
|
68
|
+
* @param documentId - Unique identifier for the document
|
|
69
|
+
* @param crdtBytes - The CRDT binary data (from Automerge.save())
|
|
70
|
+
* @param version - Version number for conflict resolution
|
|
71
|
+
* @returns Success indicator
|
|
72
|
+
*/
|
|
73
|
+
insertDocument(ctx: RunMutationCtx, documentId: string, crdtBytes: ArrayBuffer, version: number): Promise<{
|
|
74
|
+
success: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Update an existing document in the replicate component storage.
|
|
78
|
+
*
|
|
79
|
+
* @param ctx - Convex mutation context
|
|
80
|
+
* @param documentId - Unique identifier for the document
|
|
81
|
+
* @param crdtBytes - The CRDT binary data (from Automerge.save())
|
|
82
|
+
* @param version - Version number for conflict resolution
|
|
83
|
+
* @returns Success indicator
|
|
84
|
+
*/
|
|
85
|
+
updateDocument(ctx: RunMutationCtx, documentId: string, crdtBytes: ArrayBuffer, version: number): Promise<{
|
|
86
|
+
success: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Delete a document from the replicate component storage.
|
|
90
|
+
* Appends deletion delta to event log.
|
|
91
|
+
*
|
|
92
|
+
* @param ctx - Convex mutation context
|
|
93
|
+
* @param documentId - Unique identifier for the document
|
|
94
|
+
* @param crdtBytes - Yjs deletion delta
|
|
95
|
+
* @param version - CRDT version number
|
|
96
|
+
* @returns Success indicator
|
|
97
|
+
*/
|
|
98
|
+
deleteDocument(ctx: RunMutationCtx, documentId: string, crdtBytes: ArrayBuffer, version: number): Promise<{
|
|
99
|
+
success: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Stream CRDT changes for incremental replication.
|
|
103
|
+
*
|
|
104
|
+
* Retrieves CRDT bytes for documents that have been modified since the
|
|
105
|
+
* provided checkpoint, enabling incremental replication.
|
|
106
|
+
* Can be used for both polling (awaitReplication) and subscriptions (live updates).
|
|
107
|
+
*
|
|
108
|
+
* @param ctx - Convex query context
|
|
109
|
+
* @param checkpoint - Last known modification timestamp
|
|
110
|
+
* @param limit - Maximum number of changes to retrieve (default: 100)
|
|
111
|
+
* @returns Array of changes with updated checkpoint
|
|
112
|
+
*/
|
|
113
|
+
stream(ctx: RunQueryCtx, checkpoint: {
|
|
114
|
+
lastModified: number;
|
|
115
|
+
}, limit?: number): Promise<{
|
|
116
|
+
changes: Array<{
|
|
117
|
+
documentId: string;
|
|
118
|
+
crdtBytes: ArrayBuffer;
|
|
119
|
+
version: number;
|
|
120
|
+
timestamp: number;
|
|
121
|
+
}>;
|
|
122
|
+
checkpoint: {
|
|
123
|
+
lastModified: number;
|
|
124
|
+
};
|
|
125
|
+
hasMore: boolean;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Re-export the component API for direct access if needed.
|
|
130
|
+
*/
|
|
131
|
+
export { api };
|
|
132
|
+
type RunQueryCtx = {
|
|
133
|
+
runQuery: GenericQueryCtx<GenericDataModel>['runQuery'];
|
|
134
|
+
};
|
|
135
|
+
type RunMutationCtx = {
|
|
136
|
+
runMutation: GenericMutationCtx<GenericDataModel>['runMutation'];
|
|
137
|
+
};
|
|
138
|
+
export type OpaqueIds<T> = T extends GenericId<infer _T> ? string : T extends (infer U)[] ? OpaqueIds<U>[] : T extends object ? {
|
|
139
|
+
[K in keyof T]: OpaqueIds<T[K]>;
|
|
140
|
+
} : T;
|
|
141
|
+
export type UseApi<API> = Expand<{
|
|
142
|
+
[mod in keyof API]: API[mod] extends FunctionReference<infer FType, 'public', infer FArgs, infer FReturnType, infer FComponentPath> ? FunctionReference<FType, 'internal', OpaqueIds<FArgs>, OpaqueIds<FReturnType>, FComponentPath> : UseApi<API[mod]>;
|
|
143
|
+
}>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { actionGeneric, httpActionGeneric, internalActionGeneric, internalMutationGeneric, internalQueryGeneric, mutationGeneric, queryGeneric } from "convex/server";
|
|
2
|
+
const query = queryGeneric;
|
|
3
|
+
const internalQuery = internalQueryGeneric;
|
|
4
|
+
const mutation = mutationGeneric;
|
|
5
|
+
const internalMutation = internalMutationGeneric;
|
|
6
|
+
const action = actionGeneric;
|
|
7
|
+
const internalAction = internalActionGeneric;
|
|
8
|
+
const httpAction = httpActionGeneric;
|
|
9
|
+
export { action, httpAction, internalAction, internalMutation, internalQuery, mutation, query };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insert a new document with CRDT bytes (Yjs format).
|
|
3
|
+
* Appends delta to event log (event sourcing pattern).
|
|
4
|
+
*
|
|
5
|
+
* @param collectionName - Collection identifier
|
|
6
|
+
* @param documentId - Unique document identifier
|
|
7
|
+
* @param crdtBytes - ArrayBuffer containing Yjs CRDT bytes (delta)
|
|
8
|
+
* @param version - CRDT version number
|
|
9
|
+
*/
|
|
10
|
+
export declare const insertDocument: import("convex/server").RegisteredMutation<"public", {
|
|
11
|
+
collectionName: string;
|
|
12
|
+
documentId: string;
|
|
13
|
+
crdtBytes: ArrayBuffer;
|
|
14
|
+
version: number;
|
|
15
|
+
}, Promise<{
|
|
16
|
+
success: boolean;
|
|
17
|
+
}>>;
|
|
18
|
+
/**
|
|
19
|
+
* Update an existing document with new CRDT bytes (Yjs format).
|
|
20
|
+
* Appends delta to event log (event sourcing pattern).
|
|
21
|
+
*
|
|
22
|
+
* @param collectionName - Collection identifier
|
|
23
|
+
* @param documentId - Unique document identifier
|
|
24
|
+
* @param crdtBytes - ArrayBuffer containing Yjs CRDT bytes (delta)
|
|
25
|
+
* @param version - CRDT version number
|
|
26
|
+
*/
|
|
27
|
+
export declare const updateDocument: import("convex/server").RegisteredMutation<"public", {
|
|
28
|
+
collectionName: string;
|
|
29
|
+
documentId: string;
|
|
30
|
+
crdtBytes: ArrayBuffer;
|
|
31
|
+
version: number;
|
|
32
|
+
}, Promise<{
|
|
33
|
+
success: boolean;
|
|
34
|
+
}>>;
|
|
35
|
+
/**
|
|
36
|
+
* Delete a document from CRDT storage.
|
|
37
|
+
* Appends deletion delta to event log (preserves history).
|
|
38
|
+
*
|
|
39
|
+
* @param collectionName - Collection identifier
|
|
40
|
+
* @param documentId - Unique document identifier
|
|
41
|
+
* @param crdtBytes - ArrayBuffer containing Yjs deletion delta
|
|
42
|
+
* @param version - CRDT version number
|
|
43
|
+
*/
|
|
44
|
+
export declare const deleteDocument: import("convex/server").RegisteredMutation<"public", {
|
|
45
|
+
collectionName: string;
|
|
46
|
+
documentId: string;
|
|
47
|
+
crdtBytes: ArrayBuffer;
|
|
48
|
+
version: number;
|
|
49
|
+
}, Promise<{
|
|
50
|
+
success: boolean;
|
|
51
|
+
}>>;
|
|
52
|
+
/**
|
|
53
|
+
* Get complete event history for a document.
|
|
54
|
+
* Returns all CRDT deltas in chronological order.
|
|
55
|
+
*
|
|
56
|
+
* Used for:
|
|
57
|
+
* - Future recovery features (client-side)
|
|
58
|
+
* - Audit trails
|
|
59
|
+
* - Debugging
|
|
60
|
+
*
|
|
61
|
+
* @param collectionName - Collection identifier
|
|
62
|
+
* @param documentId - Unique document identifier
|
|
63
|
+
*/
|
|
64
|
+
export declare const getDocumentHistory: import("convex/server").RegisteredQuery<"public", {
|
|
65
|
+
collectionName: string;
|
|
66
|
+
documentId: string;
|
|
67
|
+
}, Promise<{
|
|
68
|
+
crdtBytes: ArrayBuffer;
|
|
69
|
+
version: number;
|
|
70
|
+
timestamp: number;
|
|
71
|
+
operationType: string;
|
|
72
|
+
}[]>>;
|
|
73
|
+
/**
|
|
74
|
+
* Stream CRDT changes for incremental replication.
|
|
75
|
+
* Returns Yjs CRDT bytes for documents modified since the checkpoint.
|
|
76
|
+
* Can be used for both polling (awaitReplication) and subscriptions (live updates).
|
|
77
|
+
*
|
|
78
|
+
* @param collectionName - Collection identifier
|
|
79
|
+
* @param checkpoint - Last replication checkpoint
|
|
80
|
+
* @param limit - Maximum number of changes to return (default: 100)
|
|
81
|
+
*/
|
|
82
|
+
export declare const stream: import("convex/server").RegisteredQuery<"public", {
|
|
83
|
+
limit?: number | undefined;
|
|
84
|
+
collectionName: string;
|
|
85
|
+
checkpoint: {
|
|
86
|
+
lastModified: number;
|
|
87
|
+
};
|
|
88
|
+
}, Promise<{
|
|
89
|
+
changes: {
|
|
90
|
+
documentId: string;
|
|
91
|
+
crdtBytes: ArrayBuffer;
|
|
92
|
+
version: number;
|
|
93
|
+
timestamp: number;
|
|
94
|
+
}[];
|
|
95
|
+
checkpoint: {
|
|
96
|
+
lastModified: number;
|
|
97
|
+
};
|
|
98
|
+
hasMore: boolean;
|
|
99
|
+
}>>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server.js";
|
|
3
|
+
const insertDocument = mutation({
|
|
4
|
+
args: {
|
|
5
|
+
collectionName: v.string(),
|
|
6
|
+
documentId: v.string(),
|
|
7
|
+
crdtBytes: v.bytes(),
|
|
8
|
+
version: v.number()
|
|
9
|
+
},
|
|
10
|
+
returns: v.object({
|
|
11
|
+
success: v.boolean()
|
|
12
|
+
}),
|
|
13
|
+
handler: async (ctx, args)=>{
|
|
14
|
+
await ctx.db.insert('documents', {
|
|
15
|
+
collectionName: args.collectionName,
|
|
16
|
+
documentId: args.documentId,
|
|
17
|
+
crdtBytes: args.crdtBytes,
|
|
18
|
+
version: args.version,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
operationType: 'insert'
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
success: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const updateDocument = mutation({
|
|
28
|
+
args: {
|
|
29
|
+
collectionName: v.string(),
|
|
30
|
+
documentId: v.string(),
|
|
31
|
+
crdtBytes: v.bytes(),
|
|
32
|
+
version: v.number()
|
|
33
|
+
},
|
|
34
|
+
returns: v.object({
|
|
35
|
+
success: v.boolean()
|
|
36
|
+
}),
|
|
37
|
+
handler: async (ctx, args)=>{
|
|
38
|
+
await ctx.db.insert('documents', {
|
|
39
|
+
collectionName: args.collectionName,
|
|
40
|
+
documentId: args.documentId,
|
|
41
|
+
crdtBytes: args.crdtBytes,
|
|
42
|
+
version: args.version,
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
operationType: 'update'
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
success: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const deleteDocument = mutation({
|
|
52
|
+
args: {
|
|
53
|
+
collectionName: v.string(),
|
|
54
|
+
documentId: v.string(),
|
|
55
|
+
crdtBytes: v.bytes(),
|
|
56
|
+
version: v.number()
|
|
57
|
+
},
|
|
58
|
+
returns: v.object({
|
|
59
|
+
success: v.boolean()
|
|
60
|
+
}),
|
|
61
|
+
handler: async (ctx, args)=>{
|
|
62
|
+
await ctx.db.insert('documents', {
|
|
63
|
+
collectionName: args.collectionName,
|
|
64
|
+
documentId: args.documentId,
|
|
65
|
+
crdtBytes: args.crdtBytes,
|
|
66
|
+
version: args.version,
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
operationType: 'delete'
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
success: true
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const getDocumentHistory = query({
|
|
76
|
+
args: {
|
|
77
|
+
collectionName: v.string(),
|
|
78
|
+
documentId: v.string()
|
|
79
|
+
},
|
|
80
|
+
returns: v.array(v.object({
|
|
81
|
+
crdtBytes: v.bytes(),
|
|
82
|
+
version: v.number(),
|
|
83
|
+
timestamp: v.number(),
|
|
84
|
+
operationType: v.string()
|
|
85
|
+
})),
|
|
86
|
+
handler: async (ctx, args)=>{
|
|
87
|
+
const deltas = await ctx.db.query('documents').withIndex('by_collection_document_version', (q)=>q.eq('collectionName', args.collectionName).eq('documentId', args.documentId)).order('asc').collect();
|
|
88
|
+
return deltas.map((d)=>({
|
|
89
|
+
crdtBytes: d.crdtBytes,
|
|
90
|
+
version: d.version,
|
|
91
|
+
timestamp: d.timestamp,
|
|
92
|
+
operationType: d.operationType
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const stream = query({
|
|
97
|
+
args: {
|
|
98
|
+
collectionName: v.string(),
|
|
99
|
+
checkpoint: v.object({
|
|
100
|
+
lastModified: v.number()
|
|
101
|
+
}),
|
|
102
|
+
limit: v.optional(v.number())
|
|
103
|
+
},
|
|
104
|
+
returns: v.object({
|
|
105
|
+
changes: v.array(v.object({
|
|
106
|
+
documentId: v.string(),
|
|
107
|
+
crdtBytes: v.bytes(),
|
|
108
|
+
version: v.number(),
|
|
109
|
+
timestamp: v.number()
|
|
110
|
+
})),
|
|
111
|
+
checkpoint: v.object({
|
|
112
|
+
lastModified: v.number()
|
|
113
|
+
}),
|
|
114
|
+
hasMore: v.boolean()
|
|
115
|
+
}),
|
|
116
|
+
handler: async (ctx, args)=>{
|
|
117
|
+
const limit = args.limit ?? 100;
|
|
118
|
+
const documents = await ctx.db.query('documents').withIndex('by_timestamp', (q)=>q.eq('collectionName', args.collectionName).gt('timestamp', args.checkpoint.lastModified)).order('asc').take(limit);
|
|
119
|
+
const changes = documents.map((doc)=>({
|
|
120
|
+
documentId: doc.documentId,
|
|
121
|
+
crdtBytes: doc.crdtBytes,
|
|
122
|
+
version: doc.version,
|
|
123
|
+
timestamp: doc.timestamp
|
|
124
|
+
}));
|
|
125
|
+
const newCheckpoint = {
|
|
126
|
+
lastModified: documents.length > 0 ? documents[documents.length - 1]?.timestamp ?? args.checkpoint.lastModified : args.checkpoint.lastModified
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
changes,
|
|
130
|
+
checkpoint: newCheckpoint,
|
|
131
|
+
hasMore: documents.length === limit
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
export { deleteDocument, getDocumentHistory, insertDocument, stream, updateDocument };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
declare const _default: import("convex/server").SchemaDefinition<{
|
|
2
|
+
documents: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
3
|
+
collectionName: string;
|
|
4
|
+
documentId: string;
|
|
5
|
+
crdtBytes: ArrayBuffer;
|
|
6
|
+
version: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
operationType: string;
|
|
9
|
+
}, {
|
|
10
|
+
collectionName: import("convex/values").VString<string, "required">;
|
|
11
|
+
documentId: import("convex/values").VString<string, "required">;
|
|
12
|
+
crdtBytes: import("convex/values").VBytes<ArrayBuffer, "required">;
|
|
13
|
+
version: import("convex/values").VFloat64<number, "required">;
|
|
14
|
+
timestamp: import("convex/values").VFloat64<number, "required">;
|
|
15
|
+
operationType: import("convex/values").VString<string, "required">;
|
|
16
|
+
}, "required", "collectionName" | "documentId" | "crdtBytes" | "version" | "timestamp" | "operationType">, {
|
|
17
|
+
by_collection: ["collectionName", "_creationTime"];
|
|
18
|
+
by_collection_document_version: ["collectionName", "documentId", "version", "_creationTime"];
|
|
19
|
+
by_timestamp: ["collectionName", "timestamp", "_creationTime"];
|
|
20
|
+
}, {}, {}>;
|
|
21
|
+
}, true>;
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
const schema = defineSchema({
|
|
4
|
+
documents: defineTable({
|
|
5
|
+
collectionName: v.string(),
|
|
6
|
+
documentId: v.string(),
|
|
7
|
+
crdtBytes: v.bytes(),
|
|
8
|
+
version: v.number(),
|
|
9
|
+
timestamp: v.number(),
|
|
10
|
+
operationType: v.string()
|
|
11
|
+
}).index('by_collection', [
|
|
12
|
+
'collectionName'
|
|
13
|
+
]).index('by_collection_document_version', [
|
|
14
|
+
'collectionName',
|
|
15
|
+
'documentId',
|
|
16
|
+
'version'
|
|
17
|
+
]).index('by_timestamp', [
|
|
18
|
+
'collectionName',
|
|
19
|
+
'timestamp'
|
|
20
|
+
])
|
|
21
|
+
});
|
|
22
|
+
export { schema as default };
|