@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.
@@ -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
- /** Utilities exposed on collection.utils */
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 memory: typeof memoryPersistence;
300
- readonly sqlite: {
301
- readonly browser: typeof createBrowserSqlitePersistence;
302
- readonly native: typeof createNativeSqlitePersistence;
317
+ readonly pglite: typeof createPGlitePersistence & {
318
+ once: typeof oncePGlitePersistence;
303
319
  };
304
- readonly indexeddb: typeof createIndexedDBPersistence;
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 };
@@ -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) return { profile: {
3131
- name: user.name,
3132
- color: user.color,
3133
- avatar: user.avatar
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 ?? "Anonymous",
3189
- color: remote.profile?.color ?? getColorForClient(remote.client),
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
- * Generate a color for a client based on their ID.
3317
- */
3318
- const DEFAULT_COLORS = [
3319
- "#F87171",
3320
- "#FB923C",
3321
- "#FBBF24",
3322
- "#A3E635",
3323
- "#34D399",
3324
- "#22D3EE",
3325
- "#60A5FA",
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
- function getColorForClient(clientId) {
3330
- return DEFAULT_COLORS[hashStringToNumber(clientId) % DEFAULT_COLORS.length];
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 item = extractDocumentFromSubdoc(subdocManager, document);
3660
- if (item) ops.upsert([item]);
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