@tldraw/sync-core 4.3.0-canary.bf87ebaf143a → 4.3.0-canary.c08047039e53

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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +252 -4
  3. package/dist-cjs/index.js +7 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js +55 -0
  6. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js.map +7 -0
  7. package/dist-cjs/lib/InMemorySyncStorage.js +32 -34
  8. package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
  9. package/dist-cjs/lib/MicrotaskNotifier.js +50 -0
  10. package/dist-cjs/lib/MicrotaskNotifier.js.map +7 -0
  11. package/dist-cjs/lib/NodeSqliteWrapper.js +48 -0
  12. package/dist-cjs/lib/NodeSqliteWrapper.js.map +7 -0
  13. package/dist-cjs/lib/SQLiteSyncStorage.js +428 -0
  14. package/dist-cjs/lib/SQLiteSyncStorage.js.map +7 -0
  15. package/dist-cjs/lib/TLSyncClient.js +34 -20
  16. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  17. package/dist-esm/index.d.mts +252 -4
  18. package/dist-esm/index.mjs +9 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs +35 -0
  21. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs.map +7 -0
  22. package/dist-esm/lib/InMemorySyncStorage.mjs +32 -34
  23. package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
  24. package/dist-esm/lib/MicrotaskNotifier.mjs +30 -0
  25. package/dist-esm/lib/MicrotaskNotifier.mjs.map +7 -0
  26. package/dist-esm/lib/NodeSqliteWrapper.mjs +28 -0
  27. package/dist-esm/lib/NodeSqliteWrapper.mjs.map +7 -0
  28. package/dist-esm/lib/SQLiteSyncStorage.mjs +414 -0
  29. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +7 -0
  30. package/dist-esm/lib/TLSyncClient.mjs +35 -21
  31. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  32. package/package.json +12 -11
  33. package/src/index.ts +11 -0
  34. package/src/lib/ClientWebSocketAdapter.test.ts +3 -0
  35. package/src/lib/DurableObjectSqliteSyncWrapper.ts +95 -0
  36. package/src/lib/InMemorySyncStorage.ts +74 -44
  37. package/src/lib/MicrotaskNotifier.test.ts +429 -0
  38. package/src/lib/MicrotaskNotifier.ts +38 -0
  39. package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +270 -0
  40. package/src/lib/NodeSqliteSyncWrapper.test.ts +272 -0
  41. package/src/lib/NodeSqliteWrapper.ts +99 -0
  42. package/src/lib/SQLiteSyncStorage.ts +627 -0
  43. package/src/lib/TLSyncClient.ts +55 -23
  44. package/src/lib/computeTombstonePruning.test.ts +352 -0
  45. package/src/test/InMemorySyncStorage.test.ts +105 -0
  46. package/src/test/SQLiteSyncStorage.test.ts +1484 -0
  47. package/src/test/TLSyncRoom.test.ts +2 -0
  48. package/src/test/upgradeDowngrade.test.ts +8 -0
  49. package/src/test/validation.test.ts +5 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@ This project contains core functionality for [tldraw sync](https://tldraw.dev/do
6
6
 
7
7
  This project is part of the tldraw SDK. It is provided under the [tldraw SDK license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
8
8
 
9
- You can use the tldraw SDK in commercial or non-commercial projects so long as you preserve the "Made with tldraw" watermark on the canvas. To remove the watermark, you can purchase a [business license](https://tldraw.dev#pricing). Visit [tldraw.dev](https://tldraw.dev) to learn more.
9
+ You can use the tldraw SDK in commercial or non-commercial projects so long as you preserve the "Made with tldraw" watermark on the canvas. To remove the watermark, you can purchase a [business license](https://tldraw.dev/pricing). Visit [tldraw.dev](https://tldraw.dev) to learn more.
10
10
 
11
11
  ## Trademarks
12
12
 
@@ -9,6 +9,7 @@ import { SerializedSchemaV2 } from '@tldraw/store';
9
9
  import { Signal } from '@tldraw/state';
10
10
  import { Store } from '@tldraw/store';
11
11
  import { StoreSchema } from '@tldraw/store';
12
+ import { StoreSnapshot } from '@tldraw/store';
12
13
  import { SynchronousStorage } from '@tldraw/store';
13
14
  import { TLDocument } from '@tldraw/tlschema';
14
15
  import { TLPage } from '@tldraw/tlschema';
@@ -46,6 +47,54 @@ export declare const DEFAULT_INITIAL_SNAPSHOT: {
46
47
 
47
48
  /* Excluded from this release type: diffRecord */
48
49
 
50
+ /**
51
+ * A wrapper around Cloudflare Durable Object's SqlStorage that implements TLSyncSqliteWrapper.
52
+ *
53
+ * Use this wrapper with SQLiteSyncStorage to persist tldraw sync state using
54
+ * Cloudflare Durable Object's built-in SQLite storage. This provides automatic
55
+ * persistence that survives Durable Object hibernation and restarts.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'
60
+ *
61
+ * // In your Durable Object class:
62
+ * class MyDurableObject extends DurableObject {
63
+ * private storage: SQLiteSyncStorage
64
+ *
65
+ * constructor(ctx: DurableObjectState, env: Env) {
66
+ * super(ctx, env)
67
+ * const sql = new DurableObjectSqliteSyncWrapper(ctx.storage)
68
+ * this.storage = new SQLiteSyncStorage({ sql })
69
+ * }
70
+ * }
71
+ * ```
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // With table prefix to avoid conflicts with other tables
76
+ * const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage, { tablePrefix: 'tldraw_' })
77
+ * // Creates tables: tldraw_documents, tldraw_tombstones, tldraw_metadata
78
+ * ```
79
+ *
80
+ * @public
81
+ */
82
+ export declare class DurableObjectSqliteSyncWrapper implements TLSyncSqliteWrapper {
83
+ private storage;
84
+ config?: TLSyncSqliteWrapperConfig | undefined;
85
+ constructor(storage: {
86
+ sql: {
87
+ exec(sql: string, ...bindings: unknown[]): Iterable<any> & {
88
+ toArray(): any[];
89
+ };
90
+ };
91
+ transactionSync(callback: () => any): any;
92
+ }, config?: TLSyncSqliteWrapperConfig | undefined);
93
+ exec(sql: string): void;
94
+ prepare<TResult extends TLSqliteRow | void = void, TParams extends TLSqliteInputValue[] = []>(sql: string): TLSyncSqliteStatement<TResult, TParams>;
95
+ transaction<T>(callback: () => T): T;
96
+ }
97
+
49
98
  /* Excluded from this release type: getNetworkDiff */
50
99
 
51
100
  /* Excluded from this release type: getTlsyncProtocolVersion */
@@ -62,7 +111,7 @@ export declare class InMemorySyncStorage<R extends UnknownRecord> implements TLS
62
111
  /* Excluded from this release type: schema */
63
112
  /* Excluded from this release type: documentClock */
64
113
  /* Excluded from this release type: tombstoneHistoryStartsAtClock */
65
- private listeners;
114
+ private notifier;
66
115
  onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void;
67
116
  constructor({ snapshot, onChange, }?: {
68
117
  onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown;
@@ -153,6 +202,53 @@ export declare function loadSnapshotIntoStorage<R extends UnknownRecord>(txn: TL
153
202
 
154
203
  /* Excluded from this release type: NetworkDiff */
155
204
 
205
+ /**
206
+ * A wrapper around synchronous SQLite databases that implements TLSyncSqliteWrapper.
207
+ * Works with both `node:sqlite` DatabaseSync (Node.js 22.5+) and `better-sqlite3` Database.
208
+ *
209
+ * Use this wrapper with SQLiteSyncStorage to persist tldraw sync state to a SQLite database
210
+ * in Node.js environments.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * // With node:sqlite (Node.js 22.5+)
215
+ * import { DatabaseSync } from 'node:sqlite'
216
+ * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'
217
+ *
218
+ * const db = new DatabaseSync(':memory:')
219
+ * const sql = new NodeSqliteWrapper(db)
220
+ * const storage = new SQLiteSyncStorage({ sql })
221
+ * ```
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * // With better-sqlite3
226
+ * import Database from 'better-sqlite3'
227
+ * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'
228
+ *
229
+ * const db = new Database(':memory:')
230
+ * const sql = new NodeSqliteWrapper(db)
231
+ * const storage = new SQLiteSyncStorage({ sql })
232
+ * ```
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * // With table prefix to avoid conflicts with other tables
237
+ * const sql = new NodeSqliteWrapper(db, { tablePrefix: 'tldraw_' })
238
+ * // Creates tables: tldraw_documents, tldraw_tombstones, tldraw_metadata
239
+ * ```
240
+ *
241
+ * @public
242
+ */
243
+ export declare class NodeSqliteWrapper implements TLSyncSqliteWrapper {
244
+ private db;
245
+ config?: TLSyncSqliteWrapperConfig | undefined;
246
+ constructor(db: SyncSqliteDatabase, config?: TLSyncSqliteWrapperConfig | undefined);
247
+ exec(sql: string): void;
248
+ prepare<TResult extends TLSqliteRow | void = void, TParams extends TLSqliteInputValue[] = TLSqliteInputValue[]>(sql: string): TLSyncSqliteStatement<TResult, TParams>;
249
+ transaction<T>(callback: () => T): T;
250
+ }
251
+
156
252
  /* Excluded from this release type: ObjectDiff */
157
253
 
158
254
  /**
@@ -274,6 +370,73 @@ export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecor
274
370
  getAll(): R[];
275
371
  }
276
372
 
373
+ /**
374
+ * SQLite-based implementation of TLSyncStorage.
375
+ * Stores documents, tombstones, metadata, and clock values in SQLite tables.
376
+ *
377
+ * This storage backend provides persistent synchronization state that survives
378
+ * process restarts, unlike InMemorySyncStorage which loses data when the process ends.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * // With Cloudflare Durable Objects
383
+ * import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'
384
+ *
385
+ * const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)
386
+ * const storage = new SQLiteSyncStorage({ sql })
387
+ * ```
388
+ *
389
+ * @example
390
+ * ```ts
391
+ * // With Node.js sqlite (Node 22.5+)
392
+ * import { DatabaseSync } from 'node:sqlite'
393
+ * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'
394
+ *
395
+ * const db = new DatabaseSync('sync-state.db')
396
+ * const sql = new NodeSqliteWrapper(db)
397
+ * const storage = new SQLiteSyncStorage({ sql })
398
+ * ```
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * // Initialize with an existing snapshot
403
+ * const storage = new SQLiteSyncStorage({ sql, snapshot: existingSnapshot })
404
+ * ```
405
+ *
406
+ * @public
407
+ */
408
+ export declare class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {
409
+ /**
410
+ * Check if the storage has been initialized (has data in the clock table).
411
+ * Useful for determining whether to load from an external source on first access.
412
+ */
413
+ static hasBeenInitialized(storage: TLSyncSqliteWrapper): boolean;
414
+ /**
415
+ * Get the current document clock value from storage without fully initializing.
416
+ * Returns null if storage has not been initialized.
417
+ * Useful for comparing storage freshness against external sources.
418
+ */
419
+ static getDocumentClock(storage: TLSyncSqliteWrapper): null | number;
420
+ private readonly stmts;
421
+ private readonly sql;
422
+ constructor({ sql, snapshot, onChange, }: {
423
+ onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown;
424
+ snapshot?: RoomSnapshot | StoreSnapshot<R>;
425
+ sql: TLSyncSqliteWrapper;
426
+ });
427
+ private notifier;
428
+ onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => void): () => void;
429
+ transaction<T>(callback: TLSyncStorageTransactionCallback<R, T>, opts?: TLSyncStorageTransactionOptions): TLSyncStorageTransactionResult<T, R>;
430
+ getClock(): number;
431
+ /* Excluded from this release type: _getTombstoneHistoryStartsAtClock */
432
+ /* Excluded from this release type: _getSchema */
433
+ /* Excluded from this release type: _setSchema */
434
+ /* Excluded from this release type: pruneTombstones */
435
+ getSnapshot(): RoomSnapshot;
436
+ private _iterateDocuments;
437
+ private _iterateTombstones;
438
+ }
439
+
277
440
  /**
278
441
  * Function type for subscribing to events with a callback.
279
442
  * Returns an unsubscribe function to clean up the listener.
@@ -285,6 +448,29 @@ export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecor
285
448
  */
286
449
  export declare type SubscribingFn<T> = (cb: (val: T) => void) => () => void;
287
450
 
451
+ /**
452
+ * Minimal interface for a synchronous SQLite database.
453
+ *
454
+ * This interface is compatible with:
455
+ * - `node:sqlite` DatabaseSync (Node.js 22.5+)
456
+ * - `better-sqlite3` Database
457
+ *
458
+ * Any SQLite library that provides synchronous `exec` and `prepare` methods
459
+ * with the signatures below can be used with {@link NodeSqliteWrapper}.
460
+ *
461
+ * @public
462
+ */
463
+ export declare interface SyncSqliteDatabase {
464
+ /** Execute raw SQL without returning results */
465
+ exec(sql: string): void;
466
+ /** Prepare a statement for execution */
467
+ prepare(sql: string): {
468
+ all(...params: unknown[]): unknown[];
469
+ iterate(...params: unknown[]): IterableIterator<unknown>;
470
+ run(...params: unknown[]): unknown;
471
+ };
472
+ }
473
+
288
474
  /* Excluded from this release type: TLConnectRequest */
289
475
 
290
476
  /**
@@ -932,6 +1118,26 @@ export declare type TLSocketStatusChangeEvent = {
932
1118
 
933
1119
  /* Excluded from this release type: TLSocketStatusListener */
934
1120
 
1121
+ /**
1122
+ * Valid input value types for SQLite query parameters.
1123
+ * These are the types that can be passed as bindings to prepared statements.
1124
+ * @public
1125
+ */
1126
+ export declare type TLSqliteInputValue = bigint | null | number | string | Uint8Array;
1127
+
1128
+ /**
1129
+ * Possible output value types returned from SQLite queries.
1130
+ * Includes all input types plus Uint8Array for BLOB columns.
1131
+ * @public
1132
+ */
1133
+ export declare type TLSqliteOutputValue = bigint | null | number | string | Uint8Array;
1134
+
1135
+ /**
1136
+ * A row returned from a SQLite query, mapping column names to their values.
1137
+ * @public
1138
+ */
1139
+ export declare type TLSqliteRow = Record<string, TLSqliteOutputValue>;
1140
+
935
1141
  /**
936
1142
  * Main client-side synchronization engine for collaborative tldraw applications.
937
1143
  *
@@ -1004,6 +1210,12 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
1004
1210
  */
1005
1211
  private speculativeChanges;
1006
1212
  private disposables;
1213
+ /** Separate scheduler instance for network sync operations */
1214
+ private readonly fpsScheduler;
1215
+ /** Send any unsent push requests to the server */
1216
+ private readonly flushPendingPushRequests;
1217
+ /** Schedule a rebase operation */
1218
+ private readonly scheduleRebase;
1007
1219
  /* Excluded from this release type: store */
1008
1220
  /* Excluded from this release type: socket */
1009
1221
  /* Excluded from this release type: presenceState */
@@ -1099,8 +1311,8 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
1099
1311
  private pushPresence;
1100
1312
  /** Push a change to the server, or stash it locally if we're offline */
1101
1313
  private push;
1102
- /** Send any unsent push requests to the server */
1103
- private flushPendingPushRequests;
1314
+ /** Get the target FPS for network operations based on presence mode */
1315
+ private getSyncFps;
1104
1316
  /**
1105
1317
  * Applies a 'network' diff to the store this does value-based equality checking so that if the
1106
1318
  * data is the same (as opposed to merely identical with ===), then no change is made and no
@@ -1108,7 +1320,6 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
1108
1320
  */
1109
1321
  private applyNetworkDiff;
1110
1322
  private rebase;
1111
- private scheduleRebase;
1112
1323
  }
1113
1324
 
1114
1325
  /**
@@ -1235,6 +1446,43 @@ export declare interface TLSyncLog {
1235
1446
 
1236
1447
  /* Excluded from this release type: TLSyncRoom */
1237
1448
 
1449
+ /**
1450
+ * A prepared statement that can be executed multiple times with different bindings.
1451
+ * @public
1452
+ */
1453
+ export declare interface TLSyncSqliteStatement<TResult extends TLSqliteRow | void, TParams extends TLSqliteInputValue[] = []> {
1454
+ /** Execute the statement and iterate over results one at a time */
1455
+ iterate(...bindings: TParams): IterableIterator<TResult>;
1456
+ /** Execute the statement and return all results as an array */
1457
+ all(...bindings: TParams): TResult[];
1458
+ /** Execute the statement without returning results (for DML) */
1459
+ run(...bindings: TParams): void;
1460
+ }
1461
+
1462
+ /**
1463
+ * Interface for SQLite storage with prepare, exec and transaction capabilities.
1464
+ * @public
1465
+ */
1466
+ export declare interface TLSyncSqliteWrapper {
1467
+ /** Optional configuration for table names. If not provided, defaults are used. */
1468
+ readonly config?: TLSyncSqliteWrapperConfig;
1469
+ /** Prepare a SQL statement for execution */
1470
+ prepare<TResult extends TLSqliteRow | void, TParams extends TLSqliteInputValue[] = []>(sql: string): TLSyncSqliteStatement<TResult, TParams>;
1471
+ /** Execute raw SQL (for DDL, multi-statement scripts) */
1472
+ exec(sql: string): void;
1473
+ /** Execute a callback within a transaction */
1474
+ transaction<T>(callback: () => T): T;
1475
+ }
1476
+
1477
+ /**
1478
+ * Configuration for SQLiteSyncStorage.
1479
+ * @public
1480
+ */
1481
+ export declare interface TLSyncSqliteWrapperConfig {
1482
+ /** Prefix for all table names (default: ''). E.g. 'sync_' creates tables 'sync_documents', 'sync_tombstones', 'sync_metadata' */
1483
+ tablePrefix?: string;
1484
+ }
1485
+
1238
1486
  /**
1239
1487
  * Pluggable synchronous transactional storage layer for TLSyncRoom.
1240
1488
  * Provides methods for managing documents, tombstones, and clocks within transactions.
package/dist-cjs/index.js CHANGED
@@ -20,11 +20,14 @@ var index_exports = {};
20
20
  __export(index_exports, {
21
21
  ClientWebSocketAdapter: () => import_ClientWebSocketAdapter.ClientWebSocketAdapter,
22
22
  DEFAULT_INITIAL_SNAPSHOT: () => import_InMemorySyncStorage.DEFAULT_INITIAL_SNAPSHOT,
23
+ DurableObjectSqliteSyncWrapper: () => import_DurableObjectSqliteSyncWrapper.DurableObjectSqliteSyncWrapper,
23
24
  InMemorySyncStorage: () => import_InMemorySyncStorage.InMemorySyncStorage,
24
25
  JsonChunkAssembler: () => import_chunk.JsonChunkAssembler,
26
+ NodeSqliteWrapper: () => import_NodeSqliteWrapper.NodeSqliteWrapper,
25
27
  ReconnectManager: () => import_ClientWebSocketAdapter.ReconnectManager,
26
28
  RecordOpType: () => import_diff.RecordOpType,
27
29
  RoomSessionState: () => import_RoomSession.RoomSessionState,
30
+ SQLiteSyncStorage: () => import_SQLiteSyncStorage.SQLiteSyncStorage,
28
31
  TLIncompatibilityReason: () => import_protocol.TLIncompatibilityReason,
29
32
  TLRemoteSyncError: () => import_TLRemoteSyncError.TLRemoteSyncError,
30
33
  TLSocketRoom: () => import_TLSocketRoom.TLSocketRoom,
@@ -45,9 +48,12 @@ var import_utils = require("@tldraw/utils");
45
48
  var import_chunk = require("./lib/chunk");
46
49
  var import_ClientWebSocketAdapter = require("./lib/ClientWebSocketAdapter");
47
50
  var import_diff = require("./lib/diff");
51
+ var import_DurableObjectSqliteSyncWrapper = require("./lib/DurableObjectSqliteSyncWrapper");
48
52
  var import_InMemorySyncStorage = require("./lib/InMemorySyncStorage");
53
+ var import_NodeSqliteWrapper = require("./lib/NodeSqliteWrapper");
49
54
  var import_protocol = require("./lib/protocol");
50
55
  var import_RoomSession = require("./lib/RoomSession");
56
+ var import_SQLiteSyncStorage = require("./lib/SQLiteSyncStorage");
51
57
  var import_TLRemoteSyncError = require("./lib/TLRemoteSyncError");
52
58
  var import_TLSocketRoom = require("./lib/TLSocketRoom");
53
59
  var import_TLSyncClient = require("./lib/TLSyncClient");
@@ -55,7 +61,7 @@ var import_TLSyncRoom = require("./lib/TLSyncRoom");
55
61
  var import_TLSyncStorage = require("./lib/TLSyncStorage");
56
62
  (0, import_utils.registerTldrawLibraryVersion)(
57
63
  "@tldraw/sync-core",
58
- "4.3.0-canary.bf87ebaf143a",
64
+ "4.3.0-canary.c08047039e53",
59
65
  "cjs"
60
66
  );
61
67
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './lib/InMemorySyncStorage'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport {\n\tTLSocketRoom,\n\ttype OmitVoid,\n\ttype RoomStoreMethods,\n\ttype TLSocketRoomOptions,\n\ttype TLSyncLog,\n} from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tTLSyncRoom,\n\ttype MinimalDocStore,\n\ttype PresenceStore,\n\ttype RoomSnapshot,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\nexport {\n\tloadSnapshotIntoStorage,\n\ttype TLSyncForwardDiff,\n\ttype TLSyncStorage,\n\ttype TLSyncStorageGetChangesSinceResult,\n\ttype TLSyncStorageOnChangeCallbackProps,\n\ttype TLSyncStorageTransaction,\n\ttype TLSyncStorageTransactionCallback,\n\ttype TLSyncStorageTransactionOptions,\n\ttype TLSyncStorageTransactionResult,\n} from './lib/TLSyncStorage'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAA0C;AAC1C,oCAAyD;AACzD,kBAcO;AACP,iCAA8D;AAC9D,sBASO;AACP,yBAAyE;AAGzE,+BAAkC;AAClC,0BAMO;AACP,0BAWO;AACP,wBAMO;AACP,2BAUO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport { DurableObjectSqliteSyncWrapper } from './lib/DurableObjectSqliteSyncWrapper'\nexport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './lib/InMemorySyncStorage'\nexport { NodeSqliteWrapper, type SyncSqliteDatabase } from './lib/NodeSqliteWrapper'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport {\n\tSQLiteSyncStorage,\n\ttype TLSqliteInputValue,\n\ttype TLSqliteOutputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './lib/SQLiteSyncStorage'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport {\n\tTLSocketRoom,\n\ttype OmitVoid,\n\ttype RoomStoreMethods,\n\ttype TLSocketRoomOptions,\n\ttype TLSyncLog,\n} from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tTLSyncRoom,\n\ttype MinimalDocStore,\n\ttype PresenceStore,\n\ttype RoomSnapshot,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\nexport {\n\tloadSnapshotIntoStorage,\n\ttype TLSyncForwardDiff,\n\ttype TLSyncStorage,\n\ttype TLSyncStorageGetChangesSinceResult,\n\ttype TLSyncStorageOnChangeCallbackProps,\n\ttype TLSyncStorageTransaction,\n\ttype TLSyncStorageTransactionCallback,\n\ttype TLSyncStorageTransactionOptions,\n\ttype TLSyncStorageTransactionResult,\n} from './lib/TLSyncStorage'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAA0C;AAC1C,oCAAyD;AACzD,kBAcO;AACP,4CAA+C;AAC/C,iCAA8D;AAC9D,+BAA2D;AAC3D,sBASO;AACP,yBAAyE;AAGzE,+BAQO;AACP,+BAAkC;AAClC,0BAMO;AACP,0BAWO;AACP,wBAMO;AACP,2BAUO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var DurableObjectSqliteSyncWrapper_exports = {};
20
+ __export(DurableObjectSqliteSyncWrapper_exports, {
21
+ DurableObjectSqliteSyncWrapper: () => DurableObjectSqliteSyncWrapper
22
+ });
23
+ module.exports = __toCommonJS(DurableObjectSqliteSyncWrapper_exports);
24
+ class DurableObjectStatement {
25
+ constructor(sql, query) {
26
+ this.sql = sql;
27
+ this.query = query;
28
+ }
29
+ iterate(...bindings) {
30
+ const result = this.sql.exec(this.query, ...bindings);
31
+ return result[Symbol.iterator]();
32
+ }
33
+ all(...bindings) {
34
+ return this.sql.exec(this.query, ...bindings).toArray();
35
+ }
36
+ run(...bindings) {
37
+ this.sql.exec(this.query, ...bindings);
38
+ }
39
+ }
40
+ class DurableObjectSqliteSyncWrapper {
41
+ constructor(storage, config) {
42
+ this.storage = storage;
43
+ this.config = config;
44
+ }
45
+ exec(sql) {
46
+ this.storage.sql.exec(sql);
47
+ }
48
+ prepare(sql) {
49
+ return new DurableObjectStatement(this.storage.sql, sql);
50
+ }
51
+ transaction(callback) {
52
+ return this.storage.transactionSync(callback);
53
+ }
54
+ }
55
+ //# sourceMappingURL=DurableObjectSqliteSyncWrapper.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/DurableObjectSqliteSyncWrapper.ts"],
4
+ "sourcesContent": ["import {\n\ttype TLSqliteInputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './SQLiteSyncStorage'\n\n/**\n * Mimics a prepared statement interface for Durable Objects SQLite.\n * Rather than actually preparing the statement, it just stores the SQL and\n * executes it fresh each time. This is still fast because DO SQLite maintains\n * an internal LRU cache of prepared statements.\n */\nclass DurableObjectStatement<\n\tTResult extends TLSqliteRow | void,\n\tTParams extends TLSqliteInputValue[],\n> implements TLSyncSqliteStatement<TResult, TParams>\n{\n\tconstructor(\n\t\tprivate sql: {\n\t\t\texec(sql: string, ...bindings: unknown[]): Iterable<any> & { toArray(): any[] }\n\t\t},\n\t\tprivate query: string\n\t) {}\n\n\titerate(...bindings: TParams): IterableIterator<TResult> {\n\t\tconst result = this.sql.exec(this.query, ...bindings)\n\t\treturn result[Symbol.iterator]() as IterableIterator<TResult>\n\t}\n\n\tall(...bindings: TParams): TResult[] {\n\t\treturn this.sql.exec(this.query, ...bindings).toArray()\n\t}\n\n\trun(...bindings: TParams): void {\n\t\tthis.sql.exec(this.query, ...bindings)\n\t}\n}\n\n/**\n * A wrapper around Cloudflare Durable Object's SqlStorage that implements TLSyncSqliteWrapper.\n *\n * Use this wrapper with SQLiteSyncStorage to persist tldraw sync state using\n * Cloudflare Durable Object's built-in SQLite storage. This provides automatic\n * persistence that survives Durable Object hibernation and restarts.\n *\n * @example\n * ```ts\n * import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'\n *\n * // In your Durable Object class:\n * class MyDurableObject extends DurableObject {\n * private storage: SQLiteSyncStorage\n *\n * constructor(ctx: DurableObjectState, env: Env) {\n * super(ctx, env)\n * const sql = new DurableObjectSqliteSyncWrapper(ctx.storage)\n * this.storage = new SQLiteSyncStorage({ sql })\n * }\n * }\n * ```\n *\n * @example\n * ```ts\n * // With table prefix to avoid conflicts with other tables\n * const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage, { tablePrefix: 'tldraw_' })\n * // Creates tables: tldraw_documents, tldraw_tombstones, tldraw_metadata\n * ```\n *\n * @public\n */\nexport class DurableObjectSqliteSyncWrapper implements TLSyncSqliteWrapper {\n\tconstructor(\n\t\tprivate storage: {\n\t\t\tsql: { exec(sql: string, ...bindings: unknown[]): Iterable<any> & { toArray(): any[] } }\n\t\t\ttransactionSync(callback: () => any): any\n\t\t},\n\t\tpublic config?: TLSyncSqliteWrapperConfig\n\t) {}\n\n\texec(sql: string): void {\n\t\tthis.storage.sql.exec(sql)\n\t}\n\n\tprepare<TResult extends TLSqliteRow | void = void, TParams extends TLSqliteInputValue[] = []>(\n\t\tsql: string\n\t): TLSyncSqliteStatement<TResult, TParams> {\n\t\treturn new DurableObjectStatement<TResult, TParams>(this.storage.sql, sql)\n\t}\n\n\ttransaction<T>(callback: () => T): T {\n\t\treturn this.storage.transactionSync(callback)\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,MAAM,uBAIN;AAAA,EACC,YACS,KAGA,OACP;AAJO;AAGA;AAAA,EACN;AAAA,EAEH,WAAW,UAA8C;AACxD,UAAM,SAAS,KAAK,IAAI,KAAK,KAAK,OAAO,GAAG,QAAQ;AACpD,WAAO,OAAO,OAAO,QAAQ,EAAE;AAAA,EAChC;AAAA,EAEA,OAAO,UAA8B;AACpC,WAAO,KAAK,IAAI,KAAK,KAAK,OAAO,GAAG,QAAQ,EAAE,QAAQ;AAAA,EACvD;AAAA,EAEA,OAAO,UAAyB;AAC/B,SAAK,IAAI,KAAK,KAAK,OAAO,GAAG,QAAQ;AAAA,EACtC;AACD;AAkCO,MAAM,+BAA8D;AAAA,EAC1E,YACS,SAID,QACN;AALO;AAID;AAAA,EACL;AAAA,EAEH,KAAK,KAAmB;AACvB,SAAK,QAAQ,IAAI,KAAK,GAAG;AAAA,EAC1B;AAAA,EAEA,QACC,KAC0C;AAC1C,WAAO,IAAI,uBAAyC,KAAK,QAAQ,KAAK,GAAG;AAAA,EAC1E;AAAA,EAEA,YAAe,UAAsB;AACpC,WAAO,KAAK,QAAQ,gBAAgB,QAAQ;AAAA,EAC7C;AACD;",
6
+ "names": []
7
+ }
@@ -21,15 +21,35 @@ __export(InMemorySyncStorage_exports, {
21
21
  DEFAULT_INITIAL_SNAPSHOT: () => DEFAULT_INITIAL_SNAPSHOT,
22
22
  InMemorySyncStorage: () => InMemorySyncStorage,
23
23
  MAX_TOMBSTONES: () => MAX_TOMBSTONES,
24
- TOMBSTONE_PRUNE_BUFFER_SIZE: () => TOMBSTONE_PRUNE_BUFFER_SIZE
24
+ TOMBSTONE_PRUNE_BUFFER_SIZE: () => TOMBSTONE_PRUNE_BUFFER_SIZE,
25
+ computeTombstonePruning: () => computeTombstonePruning
25
26
  });
26
27
  module.exports = __toCommonJS(InMemorySyncStorage_exports);
27
28
  var import_state = require("@tldraw/state");
28
29
  var import_store = require("@tldraw/store");
29
30
  var import_tlschema = require("@tldraw/tlschema");
30
31
  var import_utils = require("@tldraw/utils");
32
+ var import_MicrotaskNotifier = require("./MicrotaskNotifier");
31
33
  const TOMBSTONE_PRUNE_BUFFER_SIZE = 1e3;
32
34
  const MAX_TOMBSTONES = 5e3;
35
+ function computeTombstonePruning({
36
+ tombstones,
37
+ documentClock,
38
+ maxTombstones = MAX_TOMBSTONES,
39
+ pruneBufferSize = TOMBSTONE_PRUNE_BUFFER_SIZE
40
+ }) {
41
+ if (tombstones.length <= maxTombstones) {
42
+ return null;
43
+ }
44
+ let cutoff = pruneBufferSize + tombstones.length - maxTombstones;
45
+ while (cutoff < tombstones.length && tombstones[cutoff - 1]?.clock === tombstones[cutoff]?.clock) {
46
+ cutoff++;
47
+ }
48
+ const oldestRemaining = tombstones[cutoff];
49
+ const newTombstoneHistoryStartsAtClock = oldestRemaining?.clock ?? documentClock;
50
+ const idsToDelete = tombstones.slice(0, cutoff).map((t) => t.id);
51
+ return { newTombstoneHistoryStartsAtClock, idsToDelete };
52
+ }
33
53
  const DEFAULT_INITIAL_SNAPSHOT = {
34
54
  documentClock: 0,
35
55
  tombstoneHistoryStartsAtClock: 0,
@@ -60,18 +80,9 @@ class InMemorySyncStorage {
60
80
  documentClock;
61
81
  /** @internal */
62
82
  tombstoneHistoryStartsAtClock;
63
- listeners = /* @__PURE__ */ new Set();
83
+ notifier = new import_MicrotaskNotifier.MicrotaskNotifier();
64
84
  onChange(callback) {
65
- let didDelete = false;
66
- queueMicrotask(() => {
67
- if (didDelete) return;
68
- this.listeners.add(callback);
69
- });
70
- return () => {
71
- if (didDelete) return;
72
- didDelete = true;
73
- this.listeners.delete(callback);
74
- };
85
+ return this.notifier.register(callback);
75
86
  }
76
87
  constructor({
77
88
  snapshot = DEFAULT_INITIAL_SNAPSHOT,
@@ -137,19 +148,7 @@ class InMemorySyncStorage {
137
148
  const clockAfter = this.documentClock.get();
138
149
  const didChange = clockAfter > clockBefore;
139
150
  if (didChange) {
140
- queueMicrotask(() => {
141
- const props = {
142
- id: opts?.id,
143
- documentClock: clockAfter
144
- };
145
- for (const listener of this.listeners) {
146
- try {
147
- listener(props);
148
- } catch (error) {
149
- console.error("Error in onChange callback", error);
150
- }
151
- }
152
- });
151
+ this.notifier.notify({ id: opts?.id, documentClock: clockAfter });
153
152
  }
154
153
  return { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes };
155
154
  }
@@ -160,16 +159,15 @@ class InMemorySyncStorage {
160
159
  pruneTombstones = (0, import_utils.throttle)(
161
160
  () => {
162
161
  if (this.tombstones.size > MAX_TOMBSTONES) {
163
- const tombstones = Array.from(this.tombstones);
164
- tombstones.sort((a, b) => a[1] - b[1]);
165
- let cutoff = TOMBSTONE_PRUNE_BUFFER_SIZE + this.tombstones.size - MAX_TOMBSTONES;
166
- while (cutoff < tombstones.length && tombstones[cutoff - 1][1] === tombstones[cutoff][1]) {
167
- cutoff++;
162
+ const tombstones = Array.from(this.tombstones.entries()).map(([id, clock]) => ({ id, clock })).sort((a, b) => a.clock - b.clock);
163
+ const result = computeTombstonePruning({
164
+ tombstones,
165
+ documentClock: this.documentClock.get()
166
+ });
167
+ if (result) {
168
+ this.tombstoneHistoryStartsAtClock.set(result.newTombstoneHistoryStartsAtClock);
169
+ this.tombstones.deleteMany(result.idsToDelete);
168
170
  }
169
- const oldestRemaining = tombstones[cutoff];
170
- this.tombstoneHistoryStartsAtClock.set(oldestRemaining?.[1] ?? this.documentClock.get());
171
- const toDelete = tombstones.slice(0, cutoff);
172
- this.tombstones.deleteMany(toDelete.map(([id]) => id));
173
171
  }
174
172
  },
175
173
  1e3,