@trestleinc/replicate 1.1.1 → 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 +395 -146
- 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 -1618
- 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 -160
- package/src/client/services/checkpoint.ts +0 -86
- package/src/client/services/reconciliation.ts +0 -108
package/src/client/errors.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { Data } from
|
|
1
|
+
import { Data } from "effect";
|
|
2
2
|
|
|
3
|
-
export class NetworkError extends Data.TaggedError(
|
|
3
|
+
export class NetworkError extends Data.TaggedError("NetworkError")<{
|
|
4
4
|
readonly cause: unknown;
|
|
5
5
|
readonly retryable: true;
|
|
6
6
|
readonly operation: string;
|
|
7
7
|
}> {}
|
|
8
8
|
|
|
9
|
-
export class IDBError extends Data.TaggedError(
|
|
10
|
-
readonly operation:
|
|
9
|
+
export class IDBError extends Data.TaggedError("IDBError")<{
|
|
10
|
+
readonly operation: "get" | "set" | "delete" | "clear";
|
|
11
11
|
readonly store?: string;
|
|
12
12
|
readonly key?: string;
|
|
13
13
|
readonly cause: unknown;
|
|
14
14
|
}> {}
|
|
15
15
|
|
|
16
|
-
export class IDBWriteError extends Data.TaggedError(
|
|
16
|
+
export class IDBWriteError extends Data.TaggedError("IDBWriteError")<{
|
|
17
17
|
readonly key: string;
|
|
18
18
|
readonly value: unknown;
|
|
19
19
|
readonly cause: unknown;
|
|
20
20
|
}> {}
|
|
21
21
|
|
|
22
|
-
export class ReconciliationError extends Data.TaggedError(
|
|
22
|
+
export class ReconciliationError extends Data.TaggedError("ReconciliationError")<{
|
|
23
23
|
readonly collection: string;
|
|
24
24
|
readonly reason: string;
|
|
25
25
|
readonly cause?: unknown;
|
|
26
26
|
}> {}
|
|
27
27
|
|
|
28
|
-
export class ProseError extends Data.TaggedError(
|
|
28
|
+
export class ProseError extends Data.TaggedError("ProseError")<{
|
|
29
29
|
readonly documentId: string;
|
|
30
30
|
readonly field: string;
|
|
31
31
|
readonly collection: string;
|
|
32
32
|
}> {}
|
|
33
33
|
|
|
34
|
-
export class CollectionNotReadyError extends Data.TaggedError(
|
|
34
|
+
export class CollectionNotReadyError extends Data.TaggedError("CollectionNotReadyError")<{
|
|
35
35
|
readonly collection: string;
|
|
36
36
|
readonly reason: string;
|
|
37
37
|
}> {}
|
|
@@ -40,6 +40,6 @@ export class CollectionNotReadyError extends Data.TaggedError('CollectionNotRead
|
|
|
40
40
|
export class NonRetriableError extends Error {
|
|
41
41
|
constructor(message: string) {
|
|
42
42
|
super(message);
|
|
43
|
-
this.name =
|
|
43
|
+
this.name = "NonRetriableError";
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
type ConvexCollection,
|
|
2
|
+
collection,
|
|
4
3
|
type EditorBinding,
|
|
5
|
-
|
|
4
|
+
type ConvexCollection,
|
|
5
|
+
type Materialized,
|
|
6
|
+
} from "$/client/collection";
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
NetworkError,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
ProseError,
|
|
13
14
|
CollectionNotReadyError,
|
|
14
15
|
NonRetriableError,
|
|
15
|
-
} from
|
|
16
|
+
} from "$/client/errors";
|
|
16
17
|
|
|
17
18
|
export const errors = {
|
|
18
19
|
Network: NetworkError,
|
|
@@ -24,34 +25,14 @@ export const errors = {
|
|
|
24
25
|
NonRetriable: NonRetriableError,
|
|
25
26
|
} as const;
|
|
26
27
|
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
extract,
|
|
31
|
-
} as const;
|
|
32
|
-
|
|
33
|
-
export {
|
|
34
|
-
persistence,
|
|
35
|
-
type Persistence,
|
|
36
|
-
type PersistenceProvider,
|
|
37
|
-
type KeyValueStore,
|
|
38
|
-
type SqlitePersistenceOptions,
|
|
39
|
-
type SqliteAdapter,
|
|
40
|
-
type SqlJsStatic,
|
|
41
|
-
} from '$/client/persistence/index.js';
|
|
28
|
+
import type { ProseValue } from "$/shared/types";
|
|
29
|
+
import { extract } from "$/client/merge";
|
|
30
|
+
import { prose as proseSchema } from "$/client/prose-schema";
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} from '$/client/persistence/adapters/index.js';
|
|
32
|
+
function empty(): ProseValue {
|
|
33
|
+
return { type: "doc", content: [] } as unknown as ProseValue;
|
|
34
|
+
}
|
|
47
35
|
|
|
48
|
-
export const
|
|
49
|
-
sqljs: SqlJsAdapter,
|
|
50
|
-
opsqlite: OPSqliteAdapter,
|
|
51
|
-
} as const;
|
|
36
|
+
export const prose = Object.assign(proseSchema, { extract, empty });
|
|
52
37
|
|
|
53
|
-
export type
|
|
54
|
-
SqlJsDatabase,
|
|
55
|
-
SqlJsAdapterOptions,
|
|
56
|
-
OPSQLiteDatabase,
|
|
57
|
-
} from '$/client/persistence/adapters/index.js';
|
|
38
|
+
export { persistence, type StorageAdapter, type Persistence } from "$/client/persistence/index";
|
package/src/client/logger.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Logger, getLogger as getLogTapeLogger } from
|
|
1
|
+
import { type Logger, getLogger as getLogTapeLogger } from "@logtape/logtape";
|
|
2
2
|
|
|
3
3
|
export function getLogger(category: string[]): Logger {
|
|
4
|
-
return getLogTapeLogger([
|
|
4
|
+
return getLogTapeLogger(["replicate", ...category]);
|
|
5
5
|
}
|
package/src/client/merge.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Provides document creation, state encoding, and merge operations.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as Y from
|
|
8
|
-
import { getLogger } from
|
|
9
|
-
import type { KeyValueStore } from
|
|
7
|
+
import * as Y from "yjs";
|
|
8
|
+
import { getLogger } from "$/client/logger";
|
|
9
|
+
import type { KeyValueStore } from "$/client/persistence/types";
|
|
10
10
|
|
|
11
|
-
const logger = getLogger([
|
|
11
|
+
const logger = getLogger(["replicate", "merge"]);
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Create a Yjs document with a persistent clientId.
|
|
@@ -24,7 +24,7 @@ export async function createYjsDocument(collection: string, kv: KeyValueStore):
|
|
|
24
24
|
if (!clientId) {
|
|
25
25
|
clientId = Math.floor(Math.random() * 2147483647);
|
|
26
26
|
await kv.set(clientIdKey, clientId);
|
|
27
|
-
logger.info(
|
|
27
|
+
logger.info("Generated new Yjs clientID", { collection, clientId });
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const ydoc = new Y.Doc({
|
|
@@ -32,7 +32,7 @@ export async function createYjsDocument(collection: string, kv: KeyValueStore):
|
|
|
32
32
|
clientID: clientId,
|
|
33
33
|
} as any);
|
|
34
34
|
|
|
35
|
-
logger.info(
|
|
35
|
+
logger.info("Created Yjs document", { collection, clientId });
|
|
36
36
|
return ydoc;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -65,7 +65,7 @@ export function yjsTransact<A>(doc: Y.Doc, fn: () => A, origin?: string): A {
|
|
|
65
65
|
export function transactWithDelta<A>(
|
|
66
66
|
doc: Y.Doc,
|
|
67
67
|
fn: () => A,
|
|
68
|
-
origin?: string
|
|
68
|
+
origin?: string,
|
|
69
69
|
): { result: A; delta: Uint8Array } {
|
|
70
70
|
const beforeVector = Y.encodeStateVector(doc);
|
|
71
71
|
const result = doc.transact(fn, origin);
|
|
@@ -88,10 +88,10 @@ export function transactWithDelta<A>(
|
|
|
88
88
|
* and have these properties regardless of which module instance created them.
|
|
89
89
|
*/
|
|
90
90
|
function isYjsAbstractType(value: unknown): boolean {
|
|
91
|
-
if (value === null || typeof value !==
|
|
91
|
+
if (value === null || typeof value !== "object") return false;
|
|
92
92
|
const v = value as Record<string, unknown>;
|
|
93
93
|
// AbstractType has: doc (Doc|null), _map (Map), _eH (event handler)
|
|
94
|
-
return
|
|
94
|
+
return "_map" in v && "_eH" in v && "doc" in v;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
@@ -101,7 +101,7 @@ function isYjsAbstractType(value: unknown): boolean {
|
|
|
101
101
|
function isYMap(value: unknown): boolean {
|
|
102
102
|
if (!isYjsAbstractType(value)) return false;
|
|
103
103
|
const v = value as Record<string, unknown>;
|
|
104
|
-
return typeof v.keys ===
|
|
104
|
+
return typeof v.keys === "function" && typeof v.get === "function";
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
@@ -110,7 +110,7 @@ function isYMap(value: unknown): boolean {
|
|
|
110
110
|
function isYArray(value: unknown): boolean {
|
|
111
111
|
if (!isYjsAbstractType(value)) return false;
|
|
112
112
|
const v = value as Record<string, unknown>;
|
|
113
|
-
return typeof v.toArray ===
|
|
113
|
+
return typeof v.toArray === "function" && typeof v.get !== "function";
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
@@ -121,7 +121,7 @@ function isYXmlFragment(value: unknown): value is Y.XmlFragment {
|
|
|
121
121
|
if (!isYjsAbstractType(value)) return false;
|
|
122
122
|
const v = value as Record<string, unknown>;
|
|
123
123
|
// XmlFragment has toArray() but NOT keys() - keys() is unique to Y.Map
|
|
124
|
-
return typeof v.toArray ===
|
|
124
|
+
return typeof v.toArray === "function" && typeof v.keys !== "function";
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
@@ -131,7 +131,7 @@ function isYXmlFragment(value: unknown): value is Y.XmlFragment {
|
|
|
131
131
|
function serialize(value: unknown): unknown {
|
|
132
132
|
// Primitives pass through
|
|
133
133
|
if (value === null || value === undefined) return value;
|
|
134
|
-
if (typeof value !==
|
|
134
|
+
if (typeof value !== "object") return value;
|
|
135
135
|
|
|
136
136
|
// Check for XmlFragment first (converts to ProseMirror JSON)
|
|
137
137
|
if (isYXmlFragment(value)) {
|
|
@@ -188,7 +188,7 @@ export function extractItem<T>(ymap: Y.Map<unknown>, key: string): T | null {
|
|
|
188
188
|
return null;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
import type { XmlFragmentJSON, XmlNodeJSON } from
|
|
191
|
+
import type { XmlFragmentJSON, XmlNodeJSON } from "$/shared/types";
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
194
|
* Check if a value looks like ProseMirror/BlockNote JSON document.
|
|
@@ -196,10 +196,10 @@ import type { XmlFragmentJSON, XmlNodeJSON } from '$/shared/types.js';
|
|
|
196
196
|
*/
|
|
197
197
|
export function isDoc(value: unknown): value is XmlFragmentJSON {
|
|
198
198
|
return (
|
|
199
|
-
typeof value ===
|
|
200
|
-
value !== null
|
|
201
|
-
|
|
202
|
-
(value as { type: unknown }).type ===
|
|
199
|
+
typeof value === "object"
|
|
200
|
+
&& value !== null
|
|
201
|
+
&& "type" in value
|
|
202
|
+
&& (value as { type: unknown }).type === "doc"
|
|
203
203
|
);
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -212,11 +212,12 @@ export function fragmentToJSON(fragment: Y.XmlFragment): XmlFragmentJSON {
|
|
|
212
212
|
for (const child of fragment.toArray()) {
|
|
213
213
|
if (child instanceof Y.XmlElement) {
|
|
214
214
|
content.push(xmlElementToJSON(child));
|
|
215
|
-
}
|
|
215
|
+
}
|
|
216
|
+
else if (child instanceof Y.XmlText) {
|
|
216
217
|
const textContent = xmlTextToJSON(child);
|
|
217
218
|
if (textContent.length > 0) {
|
|
218
219
|
content.push({
|
|
219
|
-
type:
|
|
220
|
+
type: "paragraph",
|
|
220
221
|
content: textContent,
|
|
221
222
|
});
|
|
222
223
|
}
|
|
@@ -224,8 +225,8 @@ export function fragmentToJSON(fragment: Y.XmlFragment): XmlFragmentJSON {
|
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
return {
|
|
227
|
-
type:
|
|
228
|
-
content: content.length > 0 ? content : [{ type:
|
|
228
|
+
type: "doc",
|
|
229
|
+
content: content.length > 0 ? content : [{ type: "paragraph" }],
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -243,7 +244,8 @@ function xmlElementToJSON(element: Y.XmlElement): XmlNodeJSON {
|
|
|
243
244
|
for (const child of element.toArray()) {
|
|
244
245
|
if (child instanceof Y.XmlElement) {
|
|
245
246
|
content.push(xmlElementToJSON(child));
|
|
246
|
-
}
|
|
247
|
+
}
|
|
248
|
+
else if (child instanceof Y.XmlText) {
|
|
247
249
|
content.push(...xmlTextToJSON(child));
|
|
248
250
|
}
|
|
249
251
|
}
|
|
@@ -260,16 +262,16 @@ function xmlTextToJSON(text: Y.XmlText): XmlNodeJSON[] {
|
|
|
260
262
|
const delta = text.toDelta();
|
|
261
263
|
|
|
262
264
|
for (const op of delta) {
|
|
263
|
-
if (typeof op.insert ===
|
|
265
|
+
if (typeof op.insert === "string") {
|
|
264
266
|
const node: XmlNodeJSON = {
|
|
265
|
-
type:
|
|
267
|
+
type: "text",
|
|
266
268
|
text: op.insert,
|
|
267
269
|
};
|
|
268
270
|
|
|
269
271
|
if (op.attributes && Object.keys(op.attributes).length > 0) {
|
|
270
272
|
node.marks = Object.entries(op.attributes).map(([type, attrs]) => ({
|
|
271
273
|
type,
|
|
272
|
-
attrs: typeof attrs ===
|
|
274
|
+
attrs: typeof attrs === "object" ? (attrs as Record<string, unknown>) : undefined,
|
|
273
275
|
}));
|
|
274
276
|
}
|
|
275
277
|
|
|
@@ -296,23 +298,23 @@ export function fragmentFromJSON(fragment: Y.XmlFragment, json: XmlFragmentJSON)
|
|
|
296
298
|
* Handles various content structures defensively for search and display.
|
|
297
299
|
*/
|
|
298
300
|
export function extract(content: unknown): string {
|
|
299
|
-
if (!content || typeof content !==
|
|
301
|
+
if (!content || typeof content !== "object") return "";
|
|
300
302
|
|
|
301
303
|
const doc = content as { content?: unknown; type?: string };
|
|
302
304
|
|
|
303
305
|
// Handle XmlFragmentJSON format - content must be an array
|
|
304
|
-
if (!doc.content || !Array.isArray(doc.content)) return
|
|
306
|
+
if (!doc.content || !Array.isArray(doc.content)) return "";
|
|
305
307
|
|
|
306
308
|
return doc.content
|
|
307
309
|
.map((block: { content?: unknown }) => {
|
|
308
|
-
if (!block.content || !Array.isArray(block.content)) return
|
|
309
|
-
return block.content.map((node: { text?: string }) => node.text ||
|
|
310
|
+
if (!block.content || !Array.isArray(block.content)) return "";
|
|
311
|
+
return block.content.map((node: { text?: string }) => node.text || "").join("");
|
|
310
312
|
})
|
|
311
|
-
.join(
|
|
313
|
+
.join(" ");
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
function appendNodeToFragment(parent: Y.XmlFragment | Y.XmlElement, node: XmlNodeJSON): void {
|
|
315
|
-
if (node.type ===
|
|
317
|
+
if (node.type === "text") {
|
|
316
318
|
const text = new Y.XmlText();
|
|
317
319
|
if (node.text) {
|
|
318
320
|
const attrs: Record<string, unknown> = {};
|
|
@@ -324,7 +326,8 @@ function appendNodeToFragment(parent: Y.XmlFragment | Y.XmlElement, node: XmlNod
|
|
|
324
326
|
text.insert(0, node.text, Object.keys(attrs).length > 0 ? attrs : undefined);
|
|
325
327
|
}
|
|
326
328
|
parent.insert(parent.length, [text]);
|
|
327
|
-
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
328
331
|
const element = new Y.XmlElement(node.type);
|
|
329
332
|
|
|
330
333
|
if (node.attrs) {
|
|
@@ -358,7 +361,7 @@ export function serializeYMapValue(value: unknown): unknown {
|
|
|
358
361
|
export function getFragmentFromYMap(
|
|
359
362
|
ymap: Y.Map<unknown>,
|
|
360
363
|
documentId: string,
|
|
361
|
-
field: string
|
|
364
|
+
field: string,
|
|
362
365
|
): Y.XmlFragment | null {
|
|
363
366
|
const doc = ymap.get(documentId);
|
|
364
367
|
if (!isYMap(doc)) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import type { StorageAdapter, Persistence, PersistenceProvider, KeyValueStore } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const SNAPSHOT_PREFIX = "snapshot:";
|
|
5
|
+
const UPDATE_PREFIX = "update:";
|
|
6
|
+
const META_PREFIX = "meta:";
|
|
7
|
+
|
|
8
|
+
class AdapterKeyValueStore implements KeyValueStore {
|
|
9
|
+
constructor(private adapter: StorageAdapter) {}
|
|
10
|
+
|
|
11
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
12
|
+
const data = await this.adapter.get(`${META_PREFIX}${key}`);
|
|
13
|
+
if (!data) return undefined;
|
|
14
|
+
return JSON.parse(new TextDecoder().decode(data)) as T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async set<T>(key: string, value: T): Promise<void> {
|
|
18
|
+
await this.adapter.set(`${META_PREFIX}${key}`, new TextEncoder().encode(JSON.stringify(value)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async del(key: string): Promise<void> {
|
|
22
|
+
await this.adapter.delete(`${META_PREFIX}${key}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class AdapterPersistenceProvider implements PersistenceProvider {
|
|
27
|
+
private updateHandler: (update: Uint8Array, origin: unknown) => void;
|
|
28
|
+
private updateCounter = 0;
|
|
29
|
+
readonly whenSynced: Promise<void>;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private adapter: StorageAdapter,
|
|
33
|
+
private collection: string,
|
|
34
|
+
private ydoc: Y.Doc,
|
|
35
|
+
) {
|
|
36
|
+
this.whenSynced = this.loadState();
|
|
37
|
+
|
|
38
|
+
this.updateHandler = (update: Uint8Array, origin: unknown) => {
|
|
39
|
+
if (origin !== "custom") {
|
|
40
|
+
void this.saveUpdate(update);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
this.ydoc.on("update", this.updateHandler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async loadState(): Promise<void> {
|
|
47
|
+
const snapshotData = await this.adapter.get(`${SNAPSHOT_PREFIX}${this.collection}`);
|
|
48
|
+
if (snapshotData) {
|
|
49
|
+
Y.applyUpdate(this.ydoc, snapshotData, "custom");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updateKeys = await this.adapter.keys(`${UPDATE_PREFIX}${this.collection}:`);
|
|
53
|
+
const sortedKeys = updateKeys.sort();
|
|
54
|
+
|
|
55
|
+
for (const key of sortedKeys) {
|
|
56
|
+
const updateData = await this.adapter.get(key);
|
|
57
|
+
if (updateData) {
|
|
58
|
+
Y.applyUpdate(this.ydoc, updateData, "custom");
|
|
59
|
+
const seq = parseInt(key.split(":").pop() || "0", 10);
|
|
60
|
+
if (seq > this.updateCounter) {
|
|
61
|
+
this.updateCounter = seq;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async saveUpdate(update: Uint8Array): Promise<void> {
|
|
68
|
+
this.updateCounter++;
|
|
69
|
+
const paddedCounter = String(this.updateCounter).padStart(10, "0");
|
|
70
|
+
await this.adapter.set(`${UPDATE_PREFIX}${this.collection}:${paddedCounter}`, update);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
destroy(): void {
|
|
74
|
+
this.ydoc.off("update", this.updateHandler);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createCustomPersistence(adapter: StorageAdapter): Persistence {
|
|
79
|
+
return {
|
|
80
|
+
createDocPersistence: (collection: string, ydoc: Y.Doc) =>
|
|
81
|
+
new AdapterPersistenceProvider(adapter, collection, ydoc),
|
|
82
|
+
kv: new AdapterKeyValueStore(adapter),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -1,54 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
* Persistence layer exports.
|
|
3
|
-
*
|
|
4
|
-
* Provides swappable storage backends for Y.Doc and key-value data.
|
|
5
|
-
*/
|
|
6
|
-
export type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
|
|
7
|
-
export type { SqlitePersistenceOptions } from './sqlite.js';
|
|
8
|
-
export type { SqlJsStatic } from './sqlite-browser.js';
|
|
9
|
-
export type { SqliteAdapter } from './sqlite-level.js';
|
|
1
|
+
export type { StorageAdapter, Persistence } from "./types.js";
|
|
10
2
|
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { createReactNativeSqlitePersistence } from './sqlite-rn.js';
|
|
3
|
+
import { memoryPersistence } from "./memory.js";
|
|
4
|
+
import { createBrowserSqlitePersistence } from "./sqlite/browser.js";
|
|
5
|
+
import { createNativeSqlitePersistence } from "./sqlite/native.js";
|
|
6
|
+
import { createIndexedDBPersistence } from "./indexeddb.js";
|
|
7
|
+
import { createCustomPersistence } from "./custom.js";
|
|
17
8
|
|
|
18
|
-
/**
|
|
19
|
-
* Persistence API - nested object pattern for ergonomic access.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```typescript
|
|
23
|
-
* import { persistence } from '@trestleinc/replicate/client';
|
|
24
|
-
*
|
|
25
|
-
* // Browser SQLite (recommended for web)
|
|
26
|
-
* const p = await persistence.sqlite.browser(SQL, 'myapp');
|
|
27
|
-
*
|
|
28
|
-
* // React Native SQLite
|
|
29
|
-
* const p = await persistence.sqlite.native(db, 'myapp');
|
|
30
|
-
*
|
|
31
|
-
* // IndexedDB fallback
|
|
32
|
-
* const p = persistence.indexeddb('myapp');
|
|
33
|
-
*
|
|
34
|
-
* // In-memory (testing)
|
|
35
|
-
* const p = persistence.memory();
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
9
|
export const persistence = {
|
|
39
|
-
/** IndexedDB-backed persistence (browser) */
|
|
40
|
-
indexeddb: indexeddbPersistence,
|
|
41
|
-
|
|
42
|
-
/** In-memory persistence (testing/ephemeral) */
|
|
43
10
|
memory: memoryPersistence,
|
|
44
|
-
|
|
45
|
-
/** SQLite persistence variants */
|
|
46
11
|
sqlite: {
|
|
47
|
-
/** Browser SQLite with OPFS (sql.js) */
|
|
48
12
|
browser: createBrowserSqlitePersistence,
|
|
49
|
-
|
|
50
|
-
native: createReactNativeSqlitePersistence,
|
|
51
|
-
/** Custom SQLite adapter */
|
|
52
|
-
create: sqlitePersistence,
|
|
13
|
+
native: createNativeSqlitePersistence,
|
|
53
14
|
},
|
|
15
|
+
indexeddb: createIndexedDBPersistence,
|
|
16
|
+
custom: createCustomPersistence,
|
|
54
17
|
} as const;
|