@trestleinc/replicate 1.1.0 → 1.1.2-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +446 -260
- package/dist/client/index.d.ts +311 -19
- package/dist/client/index.js +4027 -0
- package/dist/component/_generated/api.d.ts +13 -17
- package/dist/component/_generated/api.js +24 -4
- package/dist/component/_generated/component.d.ts +79 -77
- package/dist/component/_generated/component.js +1 -0
- package/dist/component/_generated/dataModel.d.ts +12 -15
- package/dist/component/_generated/dataModel.js +1 -0
- package/dist/component/_generated/server.d.ts +19 -22
- package/dist/component/_generated/server.js +65 -1
- package/dist/component/_virtual/rolldown_runtime.js +18 -0
- package/dist/component/convex.config.d.ts +6 -2
- package/dist/component/convex.config.js +7 -3
- package/dist/component/logger.d.ts +10 -6
- package/dist/component/logger.js +25 -28
- package/dist/component/public.d.ts +70 -61
- package/dist/component/public.js +311 -295
- package/dist/component/schema.d.ts +53 -45
- package/dist/component/schema.js +26 -32
- package/dist/component/shared/types.d.ts +9 -0
- package/dist/component/shared/types.js +15 -0
- package/dist/server/index.d.ts +134 -13
- package/dist/server/index.js +368 -0
- package/dist/shared/index.d.ts +27 -3
- package/dist/shared/index.js +1 -2
- package/package.json +34 -29
- package/src/client/collection.ts +339 -306
- package/src/client/errors.ts +9 -9
- package/src/client/index.ts +13 -32
- package/src/client/logger.ts +2 -2
- package/src/client/merge.ts +37 -34
- package/src/client/persistence/custom.ts +84 -0
- package/src/client/persistence/index.ts +9 -46
- package/src/client/persistence/indexeddb.ts +111 -84
- package/src/client/persistence/memory.ts +3 -3
- package/src/client/persistence/sqlite/browser.ts +168 -0
- package/src/client/persistence/sqlite/native.ts +29 -0
- package/src/client/persistence/sqlite/schema.ts +124 -0
- package/src/client/persistence/types.ts +32 -28
- package/src/client/prose-schema.ts +55 -0
- package/src/client/prose.ts +28 -25
- package/src/client/replicate.ts +5 -5
- package/src/client/services/cursor.ts +109 -0
- package/src/component/_generated/component.ts +31 -29
- package/src/component/convex.config.ts +2 -2
- package/src/component/logger.ts +7 -7
- package/src/component/public.ts +225 -237
- package/src/component/schema.ts +18 -15
- package/src/server/builder.ts +20 -7
- package/src/server/index.ts +3 -5
- package/src/server/schema.ts +5 -5
- package/src/server/storage.ts +113 -59
- package/src/shared/index.ts +5 -5
- package/src/shared/types.ts +51 -14
- package/dist/client/collection.d.ts +0 -96
- package/dist/client/errors.d.ts +0 -59
- package/dist/client/logger.d.ts +0 -2
- package/dist/client/merge.d.ts +0 -77
- package/dist/client/persistence/adapters/index.d.ts +0 -8
- package/dist/client/persistence/adapters/opsqlite.d.ts +0 -46
- package/dist/client/persistence/adapters/sqljs.d.ts +0 -83
- package/dist/client/persistence/index.d.ts +0 -49
- package/dist/client/persistence/indexeddb.d.ts +0 -17
- package/dist/client/persistence/memory.d.ts +0 -16
- package/dist/client/persistence/sqlite-browser.d.ts +0 -51
- package/dist/client/persistence/sqlite-level.d.ts +0 -63
- package/dist/client/persistence/sqlite-rn.d.ts +0 -36
- package/dist/client/persistence/sqlite.d.ts +0 -47
- package/dist/client/persistence/types.d.ts +0 -42
- package/dist/client/prose.d.ts +0 -56
- package/dist/client/replicate.d.ts +0 -40
- package/dist/client/services/checkpoint.d.ts +0 -18
- package/dist/client/services/reconciliation.d.ts +0 -24
- package/dist/index.js +0 -1620
- package/dist/server/builder.d.ts +0 -94
- package/dist/server/schema.d.ts +0 -27
- package/dist/server/storage.d.ts +0 -80
- package/dist/server.js +0 -281
- package/dist/shared/types.d.ts +0 -50
- package/dist/shared/types.js +0 -6
- package/dist/shared.js +0 -6
- package/src/client/persistence/adapters/index.ts +0 -8
- package/src/client/persistence/adapters/opsqlite.ts +0 -54
- package/src/client/persistence/adapters/sqljs.ts +0 -128
- package/src/client/persistence/sqlite-browser.ts +0 -107
- package/src/client/persistence/sqlite-level.ts +0 -407
- package/src/client/persistence/sqlite-rn.ts +0 -44
- package/src/client/persistence/sqlite.ts +0 -161
- package/src/client/services/checkpoint.ts +0 -86
- package/src/client/services/reconciliation.ts +0 -108
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Universal SQLite persistence using a user-provided adapter.
|
|
3
|
-
*
|
|
4
|
-
* The consuming app is responsible for:
|
|
5
|
-
* 1. Installing the SQLite package (sql.js, op-sqlite, etc.)
|
|
6
|
-
* 2. Creating and initializing the database
|
|
7
|
-
* 3. Wrapping it with the appropriate adapter
|
|
8
|
-
* 4. Passing the adapter to sqlitePersistence()
|
|
9
|
-
*
|
|
10
|
-
* @example Browser (sql.js)
|
|
11
|
-
* ```typescript
|
|
12
|
-
* import initSqlJs from 'sql.js';
|
|
13
|
-
* import { sqlitePersistence, SqlJsAdapter } from '@trestleinc/replicate/client';
|
|
14
|
-
*
|
|
15
|
-
* const SQL = await initSqlJs({ locateFile: file => `/sql-wasm/${file}` });
|
|
16
|
-
* const db = new SQL.Database();
|
|
17
|
-
* const adapter = new SqlJsAdapter(db, {
|
|
18
|
-
* onPersist: async (data) => {
|
|
19
|
-
* // Persist to OPFS, localStorage, etc.
|
|
20
|
-
* }
|
|
21
|
-
* });
|
|
22
|
-
* const persistence = await sqlitePersistence({ adapter });
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @example React Native (op-sqlite)
|
|
26
|
-
* ```typescript
|
|
27
|
-
* import { open } from '@op-engineering/op-sqlite';
|
|
28
|
-
* import { sqlitePersistence, OPSqliteAdapter } from '@trestleinc/replicate/client';
|
|
29
|
-
*
|
|
30
|
-
* const db = open({ name: 'myapp.db' });
|
|
31
|
-
* const adapter = new OPSqliteAdapter(db);
|
|
32
|
-
* const persistence = await sqlitePersistence({ adapter });
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
import type * as Y from 'yjs';
|
|
36
|
-
import { LeveldbPersistence } from 'y-leveldb';
|
|
37
|
-
import { SqliteLevel, type SqliteAdapter } from './sqlite-level.js';
|
|
38
|
-
import type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* SQLite-backed key-value store using sqlite-level.
|
|
42
|
-
*/
|
|
43
|
-
class SqliteKeyValueStore implements KeyValueStore {
|
|
44
|
-
private db: SqliteLevel<string, string>;
|
|
45
|
-
private prefix = 'kv:';
|
|
46
|
-
|
|
47
|
-
constructor(db: SqliteLevel<string, string>) {
|
|
48
|
-
this.db = db;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async get<T>(key: string): Promise<T | undefined> {
|
|
52
|
-
try {
|
|
53
|
-
const value = await this.db.get(this.prefix + key);
|
|
54
|
-
if (value === undefined) {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
return JSON.parse(value) as T;
|
|
58
|
-
} catch {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async set<T>(key: string, value: T): Promise<void> {
|
|
64
|
-
await this.db.put(this.prefix + key, JSON.stringify(value));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async del(key: string): Promise<void> {
|
|
68
|
-
await this.db.del(this.prefix + key);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* SQLite persistence provider using y-leveldb.
|
|
74
|
-
*/
|
|
75
|
-
class SqlitePersistenceProvider implements PersistenceProvider {
|
|
76
|
-
private persistence: LeveldbPersistence;
|
|
77
|
-
readonly whenSynced: Promise<void>;
|
|
78
|
-
|
|
79
|
-
constructor(collection: string, _ydoc: Y.Doc, leveldb: LeveldbPersistence) {
|
|
80
|
-
this.persistence = leveldb;
|
|
81
|
-
// Load existing document state
|
|
82
|
-
this.whenSynced = this.persistence.getYDoc(collection).then((storedDoc: Y.Doc) => {
|
|
83
|
-
// Apply stored state to provided ydoc
|
|
84
|
-
const state = storedDoc.store;
|
|
85
|
-
if (state) {
|
|
86
|
-
// The stored doc and ydoc are merged via y-leveldb's internal mechanisms
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
destroy(): void {
|
|
92
|
-
this.persistence.destroy();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Options for SQLite persistence.
|
|
98
|
-
*/
|
|
99
|
-
export interface SqlitePersistenceOptions {
|
|
100
|
-
/**
|
|
101
|
-
* Pre-created SQLite adapter (required).
|
|
102
|
-
* Use SqlJsAdapter for browser or OPSqliteAdapter for React Native.
|
|
103
|
-
*/
|
|
104
|
-
adapter: SqliteAdapter;
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Database name for internal y-leveldb usage.
|
|
108
|
-
* @default 'replicate'
|
|
109
|
-
*/
|
|
110
|
-
dbName?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Create a universal SQLite persistence factory.
|
|
115
|
-
*
|
|
116
|
-
* Requires a pre-created SqliteAdapter - the replicate package does not
|
|
117
|
-
* import any SQLite packages directly, making it environment-agnostic.
|
|
118
|
-
*
|
|
119
|
-
* @param options - Configuration with required adapter
|
|
120
|
-
*
|
|
121
|
-
* @example Browser (sql.js)
|
|
122
|
-
* ```typescript
|
|
123
|
-
* import initSqlJs from 'sql.js';
|
|
124
|
-
* import { sqlitePersistence, SqlJsAdapter } from '@trestleinc/replicate/client';
|
|
125
|
-
*
|
|
126
|
-
* const SQL = await initSqlJs();
|
|
127
|
-
* const db = new SQL.Database();
|
|
128
|
-
* const adapter = new SqlJsAdapter(db);
|
|
129
|
-
* const persistence = await sqlitePersistence({ adapter });
|
|
130
|
-
* ```
|
|
131
|
-
*
|
|
132
|
-
* @example React Native (op-sqlite)
|
|
133
|
-
* ```typescript
|
|
134
|
-
* import { open } from '@op-engineering/op-sqlite';
|
|
135
|
-
* import { sqlitePersistence, OPSqliteAdapter } from '@trestleinc/replicate/client';
|
|
136
|
-
*
|
|
137
|
-
* const db = open({ name: 'myapp.db' });
|
|
138
|
-
* const adapter = new OPSqliteAdapter(db);
|
|
139
|
-
* const persistence = await sqlitePersistence({ adapter });
|
|
140
|
-
* ```
|
|
141
|
-
*/
|
|
142
|
-
export async function sqlitePersistence(options: SqlitePersistenceOptions): Promise<Persistence> {
|
|
143
|
-
const { adapter, dbName = 'replicate' } = options;
|
|
144
|
-
|
|
145
|
-
// Create sqlite-level database with the provided adapter
|
|
146
|
-
const db = new SqliteLevel<string, string>(dbName);
|
|
147
|
-
db.setAdapterFactory(() => Promise.resolve(adapter));
|
|
148
|
-
await db.open();
|
|
149
|
-
|
|
150
|
-
// Create y-leveldb persistence (reuses the sqlite-level database)
|
|
151
|
-
const leveldb = new LeveldbPersistence(dbName, { level: db as any });
|
|
152
|
-
|
|
153
|
-
// Create key-value store
|
|
154
|
-
const kv = new SqliteKeyValueStore(db);
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
createDocPersistence: (collection: string, ydoc: Y.Doc) =>
|
|
158
|
-
new SqlitePersistenceProvider(collection, ydoc, leveldb),
|
|
159
|
-
kv,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Effect, Context, Layer } from 'effect';
|
|
2
|
-
import { IDBError, IDBWriteError } from '$/client/errors.js';
|
|
3
|
-
import type { KeyValueStore } from '$/client/persistence/types.js';
|
|
4
|
-
|
|
5
|
-
export interface CheckpointData {
|
|
6
|
-
lastModified: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class Checkpoint extends Context.Tag('Checkpoint')<
|
|
10
|
-
Checkpoint,
|
|
11
|
-
{
|
|
12
|
-
readonly loadCheckpoint: (collection: string) => Effect.Effect<CheckpointData, IDBError>;
|
|
13
|
-
readonly saveCheckpoint: (
|
|
14
|
-
collection: string,
|
|
15
|
-
checkpoint: CheckpointData
|
|
16
|
-
) => Effect.Effect<void, IDBWriteError>;
|
|
17
|
-
readonly clearCheckpoint: (collection: string) => Effect.Effect<void, IDBError>;
|
|
18
|
-
}
|
|
19
|
-
>() {}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Create a Checkpoint service layer using the provided KeyValueStore.
|
|
23
|
-
*/
|
|
24
|
-
export function createCheckpointLayer(kv: KeyValueStore) {
|
|
25
|
-
return Layer.succeed(
|
|
26
|
-
Checkpoint,
|
|
27
|
-
Checkpoint.of({
|
|
28
|
-
loadCheckpoint: (collection) =>
|
|
29
|
-
Effect.gen(function* (_) {
|
|
30
|
-
const key = `checkpoint:${collection}`;
|
|
31
|
-
const stored = yield* _(
|
|
32
|
-
Effect.tryPromise({
|
|
33
|
-
try: () => kv.get<CheckpointData>(key),
|
|
34
|
-
catch: (cause) => new IDBError({ operation: 'get', key, cause }),
|
|
35
|
-
})
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
if (stored) {
|
|
39
|
-
yield* _(
|
|
40
|
-
Effect.logDebug('Loaded checkpoint from storage', {
|
|
41
|
-
collection,
|
|
42
|
-
checkpoint: stored,
|
|
43
|
-
})
|
|
44
|
-
);
|
|
45
|
-
return stored;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
yield* _(
|
|
49
|
-
Effect.logDebug('No stored checkpoint, using default', {
|
|
50
|
-
collection,
|
|
51
|
-
})
|
|
52
|
-
);
|
|
53
|
-
return { lastModified: 0 };
|
|
54
|
-
}),
|
|
55
|
-
|
|
56
|
-
saveCheckpoint: (collection, checkpoint) =>
|
|
57
|
-
Effect.gen(function* (_) {
|
|
58
|
-
const key = `checkpoint:${collection}`;
|
|
59
|
-
yield* _(
|
|
60
|
-
Effect.tryPromise({
|
|
61
|
-
try: () => kv.set(key, checkpoint),
|
|
62
|
-
catch: (cause) => new IDBWriteError({ key, value: checkpoint, cause }),
|
|
63
|
-
})
|
|
64
|
-
);
|
|
65
|
-
yield* _(
|
|
66
|
-
Effect.logDebug('Checkpoint saved', {
|
|
67
|
-
collection,
|
|
68
|
-
checkpoint,
|
|
69
|
-
})
|
|
70
|
-
);
|
|
71
|
-
}),
|
|
72
|
-
|
|
73
|
-
clearCheckpoint: (collection) =>
|
|
74
|
-
Effect.gen(function* (_) {
|
|
75
|
-
const key = `checkpoint:${collection}`;
|
|
76
|
-
yield* _(
|
|
77
|
-
Effect.tryPromise({
|
|
78
|
-
try: () => kv.del(key),
|
|
79
|
-
catch: (cause) => new IDBError({ operation: 'delete', key, cause }),
|
|
80
|
-
})
|
|
81
|
-
);
|
|
82
|
-
yield* _(Effect.logDebug('Checkpoint cleared', { collection }));
|
|
83
|
-
}),
|
|
84
|
-
})
|
|
85
|
-
);
|
|
86
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Effect, Context, Layer } from 'effect';
|
|
2
|
-
import * as Y from 'yjs';
|
|
3
|
-
import { yjsTransact, serializeYMap } from '$/client/merge.js';
|
|
4
|
-
import { ReconciliationError as ReconciliationErrorImport } from '$/client/errors.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Reconciliation handles removal of phantom documents -
|
|
8
|
-
* documents that exist locally but have been deleted on the server.
|
|
9
|
-
*/
|
|
10
|
-
export class Reconciliation extends Context.Tag('Reconciliation')<
|
|
11
|
-
Reconciliation,
|
|
12
|
-
{
|
|
13
|
-
/**
|
|
14
|
-
* Reconciles local Yjs state with server state by removing phantom documents.
|
|
15
|
-
* Uses an existing Yjs document and map instead of creating new ones.
|
|
16
|
-
*
|
|
17
|
-
* @param ydoc - Existing Yjs document
|
|
18
|
-
* @param ymap - Existing Yjs map within the document
|
|
19
|
-
* @param collection - Collection name for logging
|
|
20
|
-
* @param serverDocs - Documents from server
|
|
21
|
-
* @param getKey - Function to extract key from document
|
|
22
|
-
*/
|
|
23
|
-
readonly reconcile: <T>(
|
|
24
|
-
ydoc: Y.Doc,
|
|
25
|
-
ymap: Y.Map<unknown>,
|
|
26
|
-
collection: string,
|
|
27
|
-
serverDocs: readonly T[],
|
|
28
|
-
getKey: (doc: T) => string
|
|
29
|
-
) => Effect.Effect<T[], ReconciliationErrorImport>;
|
|
30
|
-
}
|
|
31
|
-
>() {}
|
|
32
|
-
|
|
33
|
-
export const ReconciliationLive = Layer.succeed(
|
|
34
|
-
Reconciliation,
|
|
35
|
-
Reconciliation.of({
|
|
36
|
-
reconcile: <T>(
|
|
37
|
-
ydoc: Y.Doc,
|
|
38
|
-
ymap: Y.Map<unknown>,
|
|
39
|
-
collection: string,
|
|
40
|
-
serverDocs: readonly T[],
|
|
41
|
-
getKey: (doc: T) => string
|
|
42
|
-
) =>
|
|
43
|
-
Effect.gen(function* (_) {
|
|
44
|
-
const serverDocIds = new Set(serverDocs.map(getKey));
|
|
45
|
-
const toDelete: string[] = [];
|
|
46
|
-
|
|
47
|
-
// Find phantom documents (exist locally but not on server)
|
|
48
|
-
ymap.forEach((_, key) => {
|
|
49
|
-
if (!serverDocIds.has(key)) {
|
|
50
|
-
toDelete.push(key);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (toDelete.length === 0) {
|
|
55
|
-
yield* _(Effect.logDebug('No phantom documents found', { collection }));
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
yield* _(
|
|
60
|
-
Effect.logWarning(`Found ${toDelete.length} phantom documents`, {
|
|
61
|
-
collection,
|
|
62
|
-
phantomDocs: toDelete.slice(0, 10), // Log first 10
|
|
63
|
-
})
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
// Extract items before deletion for TanStack DB sync
|
|
67
|
-
// Use serializeYMap for consistent ProseMirror JSON (not XML string from toJSON)
|
|
68
|
-
const removedItems: T[] = [];
|
|
69
|
-
for (const key of toDelete) {
|
|
70
|
-
const itemYMap = ymap.get(key);
|
|
71
|
-
if (itemYMap instanceof Y.Map) {
|
|
72
|
-
removedItems.push(serializeYMap(itemYMap) as T);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Remove from Yjs using plain function
|
|
77
|
-
yjsTransact(
|
|
78
|
-
ydoc,
|
|
79
|
-
() => {
|
|
80
|
-
for (const key of toDelete) {
|
|
81
|
-
ymap.delete(key);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
'reconciliation'
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
yield* _(
|
|
88
|
-
Effect.logInfo('Reconciliation completed', {
|
|
89
|
-
collection,
|
|
90
|
-
deletedCount: removedItems.length,
|
|
91
|
-
})
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Return removed items for TanStack DB sync
|
|
95
|
-
return removedItems;
|
|
96
|
-
}).pipe(
|
|
97
|
-
Effect.catchAll((cause) =>
|
|
98
|
-
Effect.fail(
|
|
99
|
-
new ReconciliationErrorImport({
|
|
100
|
-
collection,
|
|
101
|
-
reason: 'Reconciliation failed',
|
|
102
|
-
cause,
|
|
103
|
-
})
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
-
),
|
|
107
|
-
})
|
|
108
|
-
);
|