@trestleinc/replicate 1.2.0-preview.1 → 1.2.0-preview.2
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/dist/client/index.d.ts +52 -35
- package/dist/client/index.js +191 -244
- package/dist/component/mutations.d.ts +13 -13
- package/dist/component/mutations.js +0 -9
- package/dist/component/schema.d.ts +5 -5
- package/dist/server/index.js +16 -3
- package/package.json +1 -1
- package/src/client/collection.ts +48 -19
- package/src/client/index.ts +3 -0
- package/src/client/ops.ts +0 -1
- package/src/client/persistence/index.ts +5 -7
- package/src/client/persistence/pglite.ts +168 -0
- package/src/client/services/awareness.ts +46 -21
- package/src/component/mutations.ts +0 -11
- package/src/server/replicate.ts +25 -2
- package/src/client/persistence/indexeddb.ts +0 -133
- package/src/client/persistence/sqlite/browser.ts +0 -168
package/dist/client/index.d.ts
CHANGED
|
@@ -117,6 +117,13 @@ declare class NonRetriableError extends Error {
|
|
|
117
117
|
//#region src/client/services/seq.d.ts
|
|
118
118
|
type Seq = number;
|
|
119
119
|
//#endregion
|
|
120
|
+
//#region src/client/services/awareness.d.ts
|
|
121
|
+
interface UserIdentity {
|
|
122
|
+
name?: string;
|
|
123
|
+
color?: string;
|
|
124
|
+
avatar?: string;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
120
127
|
//#region src/shared/types.d.ts
|
|
121
128
|
/** ProseMirror-compatible JSON for XmlFragment serialization */
|
|
122
129
|
interface XmlFragmentJSON {
|
|
@@ -206,16 +213,11 @@ interface EditorBinding {
|
|
|
206
213
|
/** Cleanup - call when unmounting editor */
|
|
207
214
|
destroy(): void;
|
|
208
215
|
}
|
|
209
|
-
|
|
216
|
+
interface ProseOptions {
|
|
217
|
+
user?: UserIdentity;
|
|
218
|
+
}
|
|
210
219
|
interface ConvexCollectionUtils<T extends object> {
|
|
211
|
-
|
|
212
|
-
* Get an editor binding for a prose field.
|
|
213
|
-
* Waits for Y.Doc to be ready (IndexedDB loaded) before returning.
|
|
214
|
-
* @param document - The document ID
|
|
215
|
-
* @param field - The prose field name (must be in `prose` config)
|
|
216
|
-
* @returns Promise resolving to EditorBinding
|
|
217
|
-
*/
|
|
218
|
-
prose(document: string, field: ProseFields<T>): Promise<EditorBinding>;
|
|
220
|
+
prose(document: string, field: ProseFields<T>, options?: ProseOptions): Promise<EditorBinding>;
|
|
219
221
|
}
|
|
220
222
|
type LazyCollectionConfig<TSchema extends z.ZodObject<z.ZodRawShape>> = Omit<ConvexCollectionConfig<z.infer<TSchema>, TSchema, string>, "persistence" | "material">;
|
|
221
223
|
interface LazyCollection<T extends object> {
|
|
@@ -262,23 +264,6 @@ declare namespace prose$1 {
|
|
|
262
264
|
*/
|
|
263
265
|
declare function memoryPersistence(): Persistence;
|
|
264
266
|
//#endregion
|
|
265
|
-
//#region src/client/persistence/sqlite/browser.d.ts
|
|
266
|
-
interface SqlJsDatabase {
|
|
267
|
-
run(sql: string, params?: unknown): unknown;
|
|
268
|
-
prepare(sql: string): {
|
|
269
|
-
bind(params?: unknown): void;
|
|
270
|
-
step(): boolean;
|
|
271
|
-
getAsObject(): Record<string, unknown>;
|
|
272
|
-
free(): void;
|
|
273
|
-
};
|
|
274
|
-
export(): Uint8Array;
|
|
275
|
-
close(): void;
|
|
276
|
-
}
|
|
277
|
-
interface SqlJsStatic {
|
|
278
|
-
Database: new (data?: ArrayLike<number> | Buffer | null) => SqlJsDatabase;
|
|
279
|
-
}
|
|
280
|
-
declare function createBrowserSqlitePersistence(SQL: SqlJsStatic, dbName: string): Promise<Persistence>;
|
|
281
|
-
//#endregion
|
|
282
267
|
//#region src/client/persistence/sqlite/native.d.ts
|
|
283
268
|
interface OPSQLiteDatabase {
|
|
284
269
|
execute(sql: string, params?: unknown[]): Promise<{
|
|
@@ -288,20 +273,52 @@ interface OPSQLiteDatabase {
|
|
|
288
273
|
}
|
|
289
274
|
declare function createNativeSqlitePersistence(db: OPSQLiteDatabase, _dbName: string): Promise<Persistence>;
|
|
290
275
|
//#endregion
|
|
291
|
-
//#region src/client/persistence/indexeddb.d.ts
|
|
292
|
-
declare function createIndexedDBPersistence(dbName: string): Promise<Persistence>;
|
|
293
|
-
//#endregion
|
|
294
276
|
//#region src/client/persistence/custom.d.ts
|
|
295
277
|
declare function createCustomPersistence(adapter: StorageAdapter): Persistence;
|
|
296
278
|
//#endregion
|
|
279
|
+
//#region src/client/persistence/pglite.d.ts
|
|
280
|
+
interface PGliteInterface {
|
|
281
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<{
|
|
282
|
+
rows: T[];
|
|
283
|
+
}>;
|
|
284
|
+
exec(sql: string): Promise<unknown>;
|
|
285
|
+
close(): Promise<void>;
|
|
286
|
+
}
|
|
287
|
+
declare function createPGlitePersistence(pg: PGliteInterface): Promise<Persistence>;
|
|
288
|
+
/**
|
|
289
|
+
* Creates a singleton PGlite persistence factory.
|
|
290
|
+
* Use this to ensure the PGlite WASM module is only loaded once,
|
|
291
|
+
* even when shared across multiple collections.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* // src/lib/pglite.ts
|
|
296
|
+
* import { persistence } from "@trestleinc/replicate/client";
|
|
297
|
+
*
|
|
298
|
+
* export const pglite = persistence.pglite.once(async () => {
|
|
299
|
+
* const { PGlite } = await import("@electric-sql/pglite");
|
|
300
|
+
* const { live } = await import("@electric-sql/pglite/live");
|
|
301
|
+
* return PGlite.create({ dataDir: "idb://app", extensions: { live } });
|
|
302
|
+
* });
|
|
303
|
+
*
|
|
304
|
+
* // src/collections/useIntervals.ts
|
|
305
|
+
* import { pglite } from "$lib/pglite";
|
|
306
|
+
*
|
|
307
|
+
* export const intervals = collection.create({
|
|
308
|
+
* persistence: pglite,
|
|
309
|
+
* config: () => ({ ... }),
|
|
310
|
+
* });
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
declare function oncePGlitePersistence(factory: () => Promise<PGliteInterface>): () => Promise<Persistence>;
|
|
314
|
+
//#endregion
|
|
297
315
|
//#region src/client/persistence/index.d.ts
|
|
298
316
|
declare const persistence: {
|
|
299
|
-
readonly
|
|
300
|
-
|
|
301
|
-
readonly browser: typeof createBrowserSqlitePersistence;
|
|
302
|
-
readonly native: typeof createNativeSqlitePersistence;
|
|
317
|
+
readonly pglite: typeof createPGlitePersistence & {
|
|
318
|
+
once: typeof oncePGlitePersistence;
|
|
303
319
|
};
|
|
304
|
-
readonly
|
|
320
|
+
readonly sqlite: typeof createNativeSqlitePersistence;
|
|
321
|
+
readonly memory: typeof memoryPersistence;
|
|
305
322
|
readonly custom: typeof createCustomPersistence;
|
|
306
323
|
};
|
|
307
324
|
//#endregion
|
|
@@ -319,4 +336,4 @@ declare const prose: typeof prose$1 & {
|
|
|
319
336
|
extract: typeof extract;
|
|
320
337
|
};
|
|
321
338
|
//#endregion
|
|
322
|
-
export { type ConvexCollection, type EditorBinding, type Materialized, type Persistence, type Seq, type StorageAdapter, collection, errors, persistence, prose };
|
|
339
|
+
export { type ConvexCollection, type EditorBinding, type Materialized, type Persistence, type ProseOptions, type Seq, type StorageAdapter, type UserIdentity, collection, errors, persistence, prose };
|
package/dist/client/index.js
CHANGED
|
@@ -3090,8 +3090,9 @@ const DEFAULT_THROTTLE_MS = 50;
|
|
|
3090
3090
|
* Compatible with TipTap's CollaborationCursor and BlockNote's collaboration.
|
|
3091
3091
|
*/
|
|
3092
3092
|
function createAwarenessProvider(config$1) {
|
|
3093
|
-
const { convexClient, api, document, client, ydoc, interval = DEFAULT_HEARTBEAT_INTERVAL, syncReady } = config$1;
|
|
3093
|
+
const { convexClient, api, document, client, ydoc, interval = DEFAULT_HEARTBEAT_INTERVAL, syncReady, user } = config$1;
|
|
3094
3094
|
const awareness = new Awareness(ydoc);
|
|
3095
|
+
if (user) awareness.setLocalStateField("user", user);
|
|
3095
3096
|
let destroyed = false;
|
|
3096
3097
|
let visible = true;
|
|
3097
3098
|
let heartbeatTimer = null;
|
|
@@ -3126,25 +3127,27 @@ function createAwarenessProvider(config$1) {
|
|
|
3126
3127
|
*/
|
|
3127
3128
|
const extractUserFromState = (state) => {
|
|
3128
3129
|
if (!state) return {};
|
|
3129
|
-
const user = state.user;
|
|
3130
|
-
if (user)
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3130
|
+
const user$1 = state.user;
|
|
3131
|
+
if (user$1) {
|
|
3132
|
+
const profile = {};
|
|
3133
|
+
if (typeof user$1.name === "string") profile.name = user$1.name;
|
|
3134
|
+
if (typeof user$1.color === "string") profile.color = user$1.color;
|
|
3135
|
+
if (typeof user$1.avatar === "string") profile.avatar = user$1.avatar;
|
|
3136
|
+
if (Object.keys(profile).length > 0) return { profile };
|
|
3137
|
+
}
|
|
3135
3138
|
return {};
|
|
3136
3139
|
};
|
|
3137
3140
|
const sendToServer = () => {
|
|
3138
3141
|
if (destroyed || !visible) return;
|
|
3139
3142
|
const localState = awareness.getLocalState();
|
|
3140
3143
|
const cursor = extractCursorFromState(localState);
|
|
3141
|
-
const { user, profile } = extractUserFromState(localState);
|
|
3144
|
+
const { user: user$1, profile } = extractUserFromState(localState);
|
|
3142
3145
|
const vector = getVector();
|
|
3143
3146
|
convexClient.mutation(api.mark, {
|
|
3144
3147
|
document,
|
|
3145
3148
|
client,
|
|
3146
3149
|
cursor,
|
|
3147
|
-
user,
|
|
3150
|
+
user: user$1,
|
|
3148
3151
|
profile,
|
|
3149
3152
|
interval,
|
|
3150
3153
|
vector
|
|
@@ -3185,8 +3188,8 @@ function createAwarenessProvider(config$1) {
|
|
|
3185
3188
|
remoteClientIds.set(remote.client, remoteClientId);
|
|
3186
3189
|
}
|
|
3187
3190
|
const remoteState = { user: {
|
|
3188
|
-
name: remote.profile?.name ?? remote.user ??
|
|
3189
|
-
color: remote.profile?.color ??
|
|
3191
|
+
name: remote.profile?.name ?? remote.user ?? getStableAnonName(remote.client),
|
|
3192
|
+
color: remote.profile?.color ?? getStableAnonColor(remote.client),
|
|
3190
3193
|
avatar: remote.profile?.avatar,
|
|
3191
3194
|
clientId: remote.client
|
|
3192
3195
|
} };
|
|
@@ -3300,9 +3303,6 @@ function createAwarenessProvider(config$1) {
|
|
|
3300
3303
|
}
|
|
3301
3304
|
};
|
|
3302
3305
|
}
|
|
3303
|
-
/**
|
|
3304
|
-
* Hash a string to a positive number for use as clientId.
|
|
3305
|
-
*/
|
|
3306
3306
|
function hashStringToNumber(str) {
|
|
3307
3307
|
let hash = 0;
|
|
3308
3308
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -3312,22 +3312,48 @@ function hashStringToNumber(str) {
|
|
|
3312
3312
|
}
|
|
3313
3313
|
return Math.abs(hash);
|
|
3314
3314
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
"
|
|
3320
|
-
"
|
|
3321
|
-
"
|
|
3322
|
-
"
|
|
3323
|
-
"
|
|
3324
|
-
"
|
|
3325
|
-
"
|
|
3326
|
-
"#A78BFA",
|
|
3327
|
-
"#F472B6"
|
|
3315
|
+
const ANONYMOUS_ADJECTIVES = [
|
|
3316
|
+
"Swift",
|
|
3317
|
+
"Bright",
|
|
3318
|
+
"Calm",
|
|
3319
|
+
"Bold",
|
|
3320
|
+
"Keen",
|
|
3321
|
+
"Quick",
|
|
3322
|
+
"Warm",
|
|
3323
|
+
"Cool",
|
|
3324
|
+
"Sharp",
|
|
3325
|
+
"Gentle"
|
|
3328
3326
|
];
|
|
3329
|
-
|
|
3330
|
-
|
|
3327
|
+
const ANONYMOUS_NOUNS = [
|
|
3328
|
+
"Fox",
|
|
3329
|
+
"Owl",
|
|
3330
|
+
"Bear",
|
|
3331
|
+
"Wolf",
|
|
3332
|
+
"Hawk",
|
|
3333
|
+
"Deer",
|
|
3334
|
+
"Lynx",
|
|
3335
|
+
"Crow",
|
|
3336
|
+
"Hare",
|
|
3337
|
+
"Seal"
|
|
3338
|
+
];
|
|
3339
|
+
const ANONYMOUS_COLORS = [
|
|
3340
|
+
"#9F5944",
|
|
3341
|
+
"#A9704D",
|
|
3342
|
+
"#B08650",
|
|
3343
|
+
"#8A7D3F",
|
|
3344
|
+
"#6E7644",
|
|
3345
|
+
"#8C4A42",
|
|
3346
|
+
"#9E7656",
|
|
3347
|
+
"#9A5240",
|
|
3348
|
+
"#987C4A",
|
|
3349
|
+
"#7A8B6E"
|
|
3350
|
+
];
|
|
3351
|
+
function getStableAnonName(clientId) {
|
|
3352
|
+
const hash = hashStringToNumber(clientId);
|
|
3353
|
+
return `${ANONYMOUS_ADJECTIVES[hash % ANONYMOUS_ADJECTIVES.length]} ${ANONYMOUS_NOUNS[(hash >> 4) % ANONYMOUS_NOUNS.length]}`;
|
|
3354
|
+
}
|
|
3355
|
+
function getStableAnonColor(clientId) {
|
|
3356
|
+
return ANONYMOUS_COLORS[(hashStringToNumber(clientId) >> 8) % ANONYMOUS_COLORS.length];
|
|
3331
3357
|
}
|
|
3332
3358
|
|
|
3333
3359
|
//#endregion
|
|
@@ -3352,7 +3378,7 @@ function convexCollectionOptions(config$1) {
|
|
|
3352
3378
|
if (!collection$1) throw new Error("Could not extract collection name from api.stream function reference");
|
|
3353
3379
|
const proseFields = schema && schema instanceof ZodObject ? extractProseFields(schema) : [];
|
|
3354
3380
|
const proseFieldSet = new Set(proseFields);
|
|
3355
|
-
const utils = { async prose(document, field) {
|
|
3381
|
+
const utils = { async prose(document, field, options) {
|
|
3356
3382
|
const fieldStr = field;
|
|
3357
3383
|
if (!proseFieldSet.has(fieldStr)) throw new ProseError({
|
|
3358
3384
|
document,
|
|
@@ -3423,7 +3449,8 @@ function convexCollectionOptions(config$1) {
|
|
|
3423
3449
|
document,
|
|
3424
3450
|
client: storedClientId,
|
|
3425
3451
|
ydoc: subdoc,
|
|
3426
|
-
syncReady: ctx.synced
|
|
3452
|
+
syncReady: ctx.synced,
|
|
3453
|
+
user: options?.user
|
|
3427
3454
|
});
|
|
3428
3455
|
return {
|
|
3429
3456
|
fragment,
|
|
@@ -3650,21 +3677,25 @@ function convexCollectionOptions(config$1) {
|
|
|
3650
3677
|
}).pipe(Effect.provide(seqLayer)));
|
|
3651
3678
|
const cursor = ssrCursor ?? persistedCursor;
|
|
3652
3679
|
const mux = getContext(collection$1).mutex;
|
|
3653
|
-
const handleSnapshotChange = (bytes, document) => {
|
|
3680
|
+
const handleSnapshotChange = (bytes, document, exists) => {
|
|
3681
|
+
if (!exists && !subdocManager.has(document)) return;
|
|
3654
3682
|
cancelAllPending(collection$1);
|
|
3655
3683
|
mux(() => {
|
|
3656
3684
|
try {
|
|
3685
|
+
const itemBefore = extractDocumentFromSubdoc(subdocManager, document);
|
|
3657
3686
|
const update = new Uint8Array(bytes);
|
|
3658
3687
|
subdocManager.applyUpdate(document, update, YjsOrigin.Server);
|
|
3659
|
-
const
|
|
3660
|
-
if (
|
|
3688
|
+
const itemAfter = extractDocumentFromSubdoc(subdocManager, document);
|
|
3689
|
+
if (itemAfter) if (itemBefore) ops.upsert([itemAfter]);
|
|
3690
|
+
else ops.insert([itemAfter]);
|
|
3661
3691
|
} catch (error) {
|
|
3662
3692
|
throw new Error(`Snapshot application failed: ${error}`);
|
|
3663
3693
|
}
|
|
3664
3694
|
});
|
|
3665
3695
|
};
|
|
3666
|
-
const handleDeltaChange = (bytes, document) => {
|
|
3696
|
+
const handleDeltaChange = (bytes, document, exists) => {
|
|
3667
3697
|
if (!document) return;
|
|
3698
|
+
if (!exists && !subdocManager.has(document)) return;
|
|
3668
3699
|
cancelPending(collection$1, document);
|
|
3669
3700
|
setApplyingFromServer(collection$1, document, true);
|
|
3670
3701
|
mux(() => {
|
|
@@ -3673,7 +3704,8 @@ function convexCollectionOptions(config$1) {
|
|
|
3673
3704
|
const update = new Uint8Array(bytes);
|
|
3674
3705
|
subdocManager.applyUpdate(document, update, YjsOrigin.Server);
|
|
3675
3706
|
const itemAfter = extractDocumentFromSubdoc(subdocManager, document);
|
|
3676
|
-
if (itemAfter) ops.upsert([itemAfter]);
|
|
3707
|
+
if (itemAfter) if (itemBefore) ops.upsert([itemAfter]);
|
|
3708
|
+
else ops.insert([itemAfter]);
|
|
3677
3709
|
else if (itemBefore) ops.delete([itemBefore]);
|
|
3678
3710
|
} catch (error) {
|
|
3679
3711
|
throw new Error(`Delta application failed for ${document}: ${error}`);
|
|
@@ -3687,11 +3719,11 @@ function convexCollectionOptions(config$1) {
|
|
|
3687
3719
|
const { changes, seq: newSeq, compact } = response;
|
|
3688
3720
|
const syncedDocuments = /* @__PURE__ */ new Set();
|
|
3689
3721
|
for (const change of changes) {
|
|
3690
|
-
const { type, bytes, document } = change;
|
|
3722
|
+
const { type, bytes, document, exists } = change;
|
|
3691
3723
|
if (!bytes || !document) continue;
|
|
3692
3724
|
syncedDocuments.add(document);
|
|
3693
|
-
if (type === "snapshot") handleSnapshotChange(bytes, document);
|
|
3694
|
-
else handleDeltaChange(bytes, document);
|
|
3725
|
+
if (type === "snapshot") handleSnapshotChange(bytes, document, exists ?? true);
|
|
3726
|
+
else handleDeltaChange(bytes, document, exists ?? true);
|
|
3695
3727
|
}
|
|
3696
3728
|
if (newSeq !== void 0) {
|
|
3697
3729
|
const key = `cursor:${collection$1}`;
|
|
@@ -3806,7 +3838,7 @@ function memoryPersistence() {
|
|
|
3806
3838
|
|
|
3807
3839
|
//#endregion
|
|
3808
3840
|
//#region src/client/persistence/sqlite/schema.ts
|
|
3809
|
-
async function initSchema(executor) {
|
|
3841
|
+
async function initSchema$1(executor) {
|
|
3810
3842
|
await executor.execute(`
|
|
3811
3843
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
3812
3844
|
collection TEXT PRIMARY KEY,
|
|
@@ -3889,108 +3921,6 @@ function createPersistenceFromExecutor(executor) {
|
|
|
3889
3921
|
};
|
|
3890
3922
|
}
|
|
3891
3923
|
|
|
3892
|
-
//#endregion
|
|
3893
|
-
//#region src/client/persistence/sqlite/browser.ts
|
|
3894
|
-
function hasOPFS() {
|
|
3895
|
-
return typeof navigator !== "undefined" && "storage" in navigator && "getDirectory" in navigator.storage;
|
|
3896
|
-
}
|
|
3897
|
-
async function loadFromOPFS(dbName) {
|
|
3898
|
-
try {
|
|
3899
|
-
const file = await (await (await navigator.storage.getDirectory()).getFileHandle(`${dbName}.sqlite`)).getFile();
|
|
3900
|
-
return new Uint8Array(await file.arrayBuffer());
|
|
3901
|
-
} catch {
|
|
3902
|
-
return null;
|
|
3903
|
-
}
|
|
3904
|
-
}
|
|
3905
|
-
async function saveToOPFS(dbName, data) {
|
|
3906
|
-
const writable = await (await (await navigator.storage.getDirectory()).getFileHandle(`${dbName}.sqlite`, { create: true })).createWritable();
|
|
3907
|
-
await writable.write(new Uint8Array(data));
|
|
3908
|
-
await writable.close();
|
|
3909
|
-
}
|
|
3910
|
-
const IDB_STORE = "sqlite-db";
|
|
3911
|
-
function openIDB(dbName) {
|
|
3912
|
-
return new Promise((resolve, reject) => {
|
|
3913
|
-
const request = indexedDB.open(`replicate-sqlite-${dbName}`, 1);
|
|
3914
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB open failed"));
|
|
3915
|
-
request.onsuccess = () => resolve(request.result);
|
|
3916
|
-
request.onupgradeneeded = () => {
|
|
3917
|
-
request.result.createObjectStore(IDB_STORE);
|
|
3918
|
-
};
|
|
3919
|
-
});
|
|
3920
|
-
}
|
|
3921
|
-
async function loadFromIDB(dbName) {
|
|
3922
|
-
try {
|
|
3923
|
-
const db = await openIDB(dbName);
|
|
3924
|
-
return new Promise((resolve) => {
|
|
3925
|
-
const request = db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get("data");
|
|
3926
|
-
request.onsuccess = () => {
|
|
3927
|
-
db.close();
|
|
3928
|
-
resolve(request.result ?? null);
|
|
3929
|
-
};
|
|
3930
|
-
request.onerror = () => {
|
|
3931
|
-
db.close();
|
|
3932
|
-
resolve(null);
|
|
3933
|
-
};
|
|
3934
|
-
});
|
|
3935
|
-
} catch {
|
|
3936
|
-
return null;
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
async function saveToIDB(dbName, data) {
|
|
3940
|
-
const db = await openIDB(dbName);
|
|
3941
|
-
return new Promise((resolve, reject) => {
|
|
3942
|
-
const request = db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).put(data, "data");
|
|
3943
|
-
request.onsuccess = () => {
|
|
3944
|
-
db.close();
|
|
3945
|
-
resolve();
|
|
3946
|
-
};
|
|
3947
|
-
request.onerror = () => {
|
|
3948
|
-
db.close();
|
|
3949
|
-
reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB put failed"));
|
|
3950
|
-
};
|
|
3951
|
-
});
|
|
3952
|
-
}
|
|
3953
|
-
function createStorageBackend(dbName) {
|
|
3954
|
-
if (hasOPFS()) return {
|
|
3955
|
-
load: () => loadFromOPFS(dbName),
|
|
3956
|
-
save: (data) => saveToOPFS(dbName, data)
|
|
3957
|
-
};
|
|
3958
|
-
return {
|
|
3959
|
-
load: () => loadFromIDB(dbName),
|
|
3960
|
-
save: (data) => saveToIDB(dbName, data)
|
|
3961
|
-
};
|
|
3962
|
-
}
|
|
3963
|
-
var SqlJsExecutor = class {
|
|
3964
|
-
constructor(db, storage) {
|
|
3965
|
-
this.db = db;
|
|
3966
|
-
this.storage = storage;
|
|
3967
|
-
}
|
|
3968
|
-
async execute(sql, params) {
|
|
3969
|
-
const rows = [];
|
|
3970
|
-
const trimmed = sql.trim().toUpperCase();
|
|
3971
|
-
if (trimmed.startsWith("CREATE") || trimmed.startsWith("INSERT") || trimmed.startsWith("UPDATE") || trimmed.startsWith("DELETE") || trimmed.startsWith("BEGIN") || trimmed.startsWith("COMMIT") || trimmed.startsWith("ROLLBACK")) {
|
|
3972
|
-
this.db.run(sql, params);
|
|
3973
|
-
await this.storage.save(this.db.export());
|
|
3974
|
-
return { rows };
|
|
3975
|
-
}
|
|
3976
|
-
const stmt = this.db.prepare(sql);
|
|
3977
|
-
if (params?.length) stmt.bind(params);
|
|
3978
|
-
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
3979
|
-
stmt.free();
|
|
3980
|
-
return { rows };
|
|
3981
|
-
}
|
|
3982
|
-
close() {
|
|
3983
|
-
this.db.close();
|
|
3984
|
-
}
|
|
3985
|
-
};
|
|
3986
|
-
async function createBrowserSqlitePersistence(SQL, dbName) {
|
|
3987
|
-
const storage = createStorageBackend(dbName);
|
|
3988
|
-
const existingData = await storage.load();
|
|
3989
|
-
const executor = new SqlJsExecutor(existingData ? new SQL.Database(existingData) : new SQL.Database(), storage);
|
|
3990
|
-
await initSchema(executor);
|
|
3991
|
-
return createPersistenceFromExecutor(executor);
|
|
3992
|
-
}
|
|
3993
|
-
|
|
3994
3924
|
//#endregion
|
|
3995
3925
|
//#region src/client/persistence/sqlite/native.ts
|
|
3996
3926
|
var OPSqliteExecutor = class {
|
|
@@ -4006,106 +3936,10 @@ var OPSqliteExecutor = class {
|
|
|
4006
3936
|
};
|
|
4007
3937
|
async function createNativeSqlitePersistence(db, _dbName) {
|
|
4008
3938
|
const executor = new OPSqliteExecutor(db);
|
|
4009
|
-
await initSchema(executor);
|
|
3939
|
+
await initSchema$1(executor);
|
|
4010
3940
|
return createPersistenceFromExecutor(executor);
|
|
4011
3941
|
}
|
|
4012
3942
|
|
|
4013
|
-
//#endregion
|
|
4014
|
-
//#region src/client/persistence/indexeddb.ts
|
|
4015
|
-
const UPDATES_STORE = "updates";
|
|
4016
|
-
const SNAPSHOTS_STORE = "snapshots";
|
|
4017
|
-
const KV_STORE = "kv";
|
|
4018
|
-
function openDatabase(dbName) {
|
|
4019
|
-
return new Promise((resolve, reject) => {
|
|
4020
|
-
const request = indexedDB.open(`replicate-${dbName}`, 1);
|
|
4021
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB open failed"));
|
|
4022
|
-
request.onsuccess = () => resolve(request.result);
|
|
4023
|
-
request.onupgradeneeded = () => {
|
|
4024
|
-
const db = request.result;
|
|
4025
|
-
db.createObjectStore(SNAPSHOTS_STORE);
|
|
4026
|
-
db.createObjectStore(KV_STORE);
|
|
4027
|
-
db.createObjectStore(UPDATES_STORE, { autoIncrement: true }).createIndex("by_collection", "collection", { unique: false });
|
|
4028
|
-
};
|
|
4029
|
-
});
|
|
4030
|
-
}
|
|
4031
|
-
var IDBKeyValueStore = class {
|
|
4032
|
-
constructor(db) {
|
|
4033
|
-
this.db = db;
|
|
4034
|
-
}
|
|
4035
|
-
get(key) {
|
|
4036
|
-
return new Promise((resolve, reject) => {
|
|
4037
|
-
const request = this.db.transaction(KV_STORE, "readonly").objectStore(KV_STORE).get(key);
|
|
4038
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB get failed"));
|
|
4039
|
-
request.onsuccess = () => resolve(request.result);
|
|
4040
|
-
});
|
|
4041
|
-
}
|
|
4042
|
-
set(key, value) {
|
|
4043
|
-
return new Promise((resolve, reject) => {
|
|
4044
|
-
const request = this.db.transaction(KV_STORE, "readwrite").objectStore(KV_STORE).put(value, key);
|
|
4045
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB set failed"));
|
|
4046
|
-
request.onsuccess = () => resolve();
|
|
4047
|
-
});
|
|
4048
|
-
}
|
|
4049
|
-
del(key) {
|
|
4050
|
-
return new Promise((resolve, reject) => {
|
|
4051
|
-
const request = this.db.transaction(KV_STORE, "readwrite").objectStore(KV_STORE).delete(key);
|
|
4052
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB delete failed"));
|
|
4053
|
-
request.onsuccess = () => resolve();
|
|
4054
|
-
});
|
|
4055
|
-
}
|
|
4056
|
-
};
|
|
4057
|
-
var IDBPersistenceProvider = class {
|
|
4058
|
-
updateHandler;
|
|
4059
|
-
whenSynced;
|
|
4060
|
-
constructor(db, collection$1, ydoc) {
|
|
4061
|
-
this.db = db;
|
|
4062
|
-
this.collection = collection$1;
|
|
4063
|
-
this.ydoc = ydoc;
|
|
4064
|
-
this.whenSynced = this.loadState();
|
|
4065
|
-
this.updateHandler = (update, origin) => {
|
|
4066
|
-
if (origin !== "idb") this.saveUpdate(update);
|
|
4067
|
-
};
|
|
4068
|
-
this.ydoc.on("update", this.updateHandler);
|
|
4069
|
-
}
|
|
4070
|
-
loadState() {
|
|
4071
|
-
return new Promise((resolve, reject) => {
|
|
4072
|
-
const tx = this.db.transaction([SNAPSHOTS_STORE, UPDATES_STORE], "readonly");
|
|
4073
|
-
const snapshotRequest = tx.objectStore(SNAPSHOTS_STORE).get(this.collection);
|
|
4074
|
-
snapshotRequest.onsuccess = () => {
|
|
4075
|
-
if (snapshotRequest.result) Y.applyUpdate(this.ydoc, snapshotRequest.result, "idb");
|
|
4076
|
-
const updatesRequest = tx.objectStore(UPDATES_STORE).index("by_collection").getAll(this.collection);
|
|
4077
|
-
updatesRequest.onsuccess = () => {
|
|
4078
|
-
const records = updatesRequest.result;
|
|
4079
|
-
for (const record of records) Y.applyUpdate(this.ydoc, record.data, "idb");
|
|
4080
|
-
resolve();
|
|
4081
|
-
};
|
|
4082
|
-
updatesRequest.onerror = () => reject(updatesRequest.error ?? /* @__PURE__ */ new Error("IndexedDB updates load failed"));
|
|
4083
|
-
};
|
|
4084
|
-
snapshotRequest.onerror = () => reject(snapshotRequest.error ?? /* @__PURE__ */ new Error("IndexedDB snapshot load failed"));
|
|
4085
|
-
});
|
|
4086
|
-
}
|
|
4087
|
-
saveUpdate(update) {
|
|
4088
|
-
return new Promise((resolve, reject) => {
|
|
4089
|
-
const request = this.db.transaction(UPDATES_STORE, "readwrite").objectStore(UPDATES_STORE).add({
|
|
4090
|
-
collection: this.collection,
|
|
4091
|
-
data: update
|
|
4092
|
-
});
|
|
4093
|
-
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB save update failed"));
|
|
4094
|
-
request.onsuccess = () => resolve();
|
|
4095
|
-
});
|
|
4096
|
-
}
|
|
4097
|
-
destroy() {
|
|
4098
|
-
this.ydoc.off("update", this.updateHandler);
|
|
4099
|
-
}
|
|
4100
|
-
};
|
|
4101
|
-
async function createIndexedDBPersistence(dbName) {
|
|
4102
|
-
const db = await openDatabase(dbName);
|
|
4103
|
-
return {
|
|
4104
|
-
createDocPersistence: (collection$1, ydoc) => new IDBPersistenceProvider(db, collection$1, ydoc),
|
|
4105
|
-
kv: new IDBKeyValueStore(db)
|
|
4106
|
-
};
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
3943
|
//#endregion
|
|
4110
3944
|
//#region src/client/persistence/custom.ts
|
|
4111
3945
|
const SNAPSHOT_PREFIX = "snapshot:";
|
|
@@ -4170,15 +4004,128 @@ function createCustomPersistence(adapter) {
|
|
|
4170
4004
|
};
|
|
4171
4005
|
}
|
|
4172
4006
|
|
|
4007
|
+
//#endregion
|
|
4008
|
+
//#region src/client/persistence/pglite.ts
|
|
4009
|
+
async function initSchema(pg) {
|
|
4010
|
+
await pg.exec(`
|
|
4011
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
4012
|
+
collection TEXT PRIMARY KEY,
|
|
4013
|
+
data BYTEA NOT NULL,
|
|
4014
|
+
state_vector BYTEA,
|
|
4015
|
+
seq INTEGER DEFAULT 0
|
|
4016
|
+
)
|
|
4017
|
+
`);
|
|
4018
|
+
await pg.exec(`
|
|
4019
|
+
CREATE TABLE IF NOT EXISTS updates (
|
|
4020
|
+
id SERIAL PRIMARY KEY,
|
|
4021
|
+
collection TEXT NOT NULL,
|
|
4022
|
+
data BYTEA NOT NULL
|
|
4023
|
+
)
|
|
4024
|
+
`);
|
|
4025
|
+
await pg.exec(`
|
|
4026
|
+
CREATE INDEX IF NOT EXISTS updates_collection_idx ON updates (collection)
|
|
4027
|
+
`);
|
|
4028
|
+
await pg.exec(`
|
|
4029
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
4030
|
+
key TEXT PRIMARY KEY,
|
|
4031
|
+
value TEXT NOT NULL
|
|
4032
|
+
)
|
|
4033
|
+
`);
|
|
4034
|
+
}
|
|
4035
|
+
var PGliteKeyValueStore = class {
|
|
4036
|
+
constructor(pg) {
|
|
4037
|
+
this.pg = pg;
|
|
4038
|
+
}
|
|
4039
|
+
async get(key) {
|
|
4040
|
+
const result = await this.pg.query("SELECT value FROM kv WHERE key = $1", [key]);
|
|
4041
|
+
if (result.rows.length === 0) return void 0;
|
|
4042
|
+
return JSON.parse(result.rows[0].value);
|
|
4043
|
+
}
|
|
4044
|
+
async set(key, value) {
|
|
4045
|
+
await this.pg.query(`INSERT INTO kv (key, value) VALUES ($1, $2)
|
|
4046
|
+
ON CONFLICT (key) DO UPDATE SET value = $2`, [key, JSON.stringify(value)]);
|
|
4047
|
+
}
|
|
4048
|
+
async del(key) {
|
|
4049
|
+
await this.pg.query("DELETE FROM kv WHERE key = $1", [key]);
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
var PGlitePersistenceProvider = class {
|
|
4053
|
+
updateHandler;
|
|
4054
|
+
whenSynced;
|
|
4055
|
+
constructor(pg, collection$1, ydoc) {
|
|
4056
|
+
this.pg = pg;
|
|
4057
|
+
this.collection = collection$1;
|
|
4058
|
+
this.ydoc = ydoc;
|
|
4059
|
+
this.whenSynced = this.loadState();
|
|
4060
|
+
this.updateHandler = (update, origin) => {
|
|
4061
|
+
if (origin !== "pglite") this.saveUpdate(update);
|
|
4062
|
+
};
|
|
4063
|
+
this.ydoc.on("update", this.updateHandler);
|
|
4064
|
+
}
|
|
4065
|
+
async loadState() {
|
|
4066
|
+
const snapshotResult = await this.pg.query("SELECT data FROM snapshots WHERE collection = $1", [this.collection]);
|
|
4067
|
+
if (snapshotResult.rows.length > 0) {
|
|
4068
|
+
const raw = snapshotResult.rows[0].data;
|
|
4069
|
+
const snapshotData = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
|
|
4070
|
+
Y.applyUpdate(this.ydoc, snapshotData, "pglite");
|
|
4071
|
+
}
|
|
4072
|
+
const updatesResult = await this.pg.query("SELECT data FROM updates WHERE collection = $1 ORDER BY id ASC", [this.collection]);
|
|
4073
|
+
for (const row of updatesResult.rows) {
|
|
4074
|
+
const raw = row.data;
|
|
4075
|
+
const updateData = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
|
|
4076
|
+
Y.applyUpdate(this.ydoc, updateData, "pglite");
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
async saveUpdate(update) {
|
|
4080
|
+
await this.pg.query("INSERT INTO updates (collection, data) VALUES ($1, $2)", [this.collection, update]);
|
|
4081
|
+
}
|
|
4082
|
+
destroy() {
|
|
4083
|
+
this.ydoc.off("update", this.updateHandler);
|
|
4084
|
+
}
|
|
4085
|
+
};
|
|
4086
|
+
async function createPGlitePersistence(pg) {
|
|
4087
|
+
await initSchema(pg);
|
|
4088
|
+
return {
|
|
4089
|
+
createDocPersistence: (collection$1, ydoc) => new PGlitePersistenceProvider(pg, collection$1, ydoc),
|
|
4090
|
+
kv: new PGliteKeyValueStore(pg)
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
/**
|
|
4094
|
+
* Creates a singleton PGlite persistence factory.
|
|
4095
|
+
* Use this to ensure the PGlite WASM module is only loaded once,
|
|
4096
|
+
* even when shared across multiple collections.
|
|
4097
|
+
*
|
|
4098
|
+
* @example
|
|
4099
|
+
* ```typescript
|
|
4100
|
+
* // src/lib/pglite.ts
|
|
4101
|
+
* import { persistence } from "@trestleinc/replicate/client";
|
|
4102
|
+
*
|
|
4103
|
+
* export const pglite = persistence.pglite.once(async () => {
|
|
4104
|
+
* const { PGlite } = await import("@electric-sql/pglite");
|
|
4105
|
+
* const { live } = await import("@electric-sql/pglite/live");
|
|
4106
|
+
* return PGlite.create({ dataDir: "idb://app", extensions: { live } });
|
|
4107
|
+
* });
|
|
4108
|
+
*
|
|
4109
|
+
* // src/collections/useIntervals.ts
|
|
4110
|
+
* import { pglite } from "$lib/pglite";
|
|
4111
|
+
*
|
|
4112
|
+
* export const intervals = collection.create({
|
|
4113
|
+
* persistence: pglite,
|
|
4114
|
+
* config: () => ({ ... }),
|
|
4115
|
+
* });
|
|
4116
|
+
* ```
|
|
4117
|
+
*/
|
|
4118
|
+
function oncePGlitePersistence(factory) {
|
|
4119
|
+
let instance = null;
|
|
4120
|
+
return () => instance ??= factory().then(createPGlitePersistence);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4173
4123
|
//#endregion
|
|
4174
4124
|
//#region src/client/persistence/index.ts
|
|
4175
4125
|
const persistence = {
|
|
4126
|
+
pglite: Object.assign(createPGlitePersistence, { once: oncePGlitePersistence }),
|
|
4127
|
+
sqlite: createNativeSqlitePersistence,
|
|
4176
4128
|
memory: memoryPersistence,
|
|
4177
|
-
sqlite: {
|
|
4178
|
-
browser: createBrowserSqlitePersistence,
|
|
4179
|
-
native: createNativeSqlitePersistence
|
|
4180
|
-
},
|
|
4181
|
-
indexeddb: createIndexedDBPersistence,
|
|
4182
4129
|
custom: createCustomPersistence
|
|
4183
4130
|
};
|
|
4184
4131
|
|