@tldraw/sync-core 3.15.0 → 3.15.1

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.
@@ -130,6 +130,7 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
130
130
  private room;
131
131
  private readonly sessions;
132
132
  readonly log?: TLSyncLog;
133
+ private readonly syncCallbacks;
133
134
  constructor(opts: {
134
135
  /* Excluded from this release type: onPresenceChange */
135
136
  clientTimeout?: number;
package/dist-cjs/index.js CHANGED
@@ -50,7 +50,7 @@ var import_TLSyncClient = require("./lib/TLSyncClient");
50
50
  var import_TLSyncRoom = require("./lib/TLSyncRoom");
51
51
  (0, import_utils.registerTldrawLibraryVersion)(
52
52
  "@tldraw/sync-core",
53
- "3.15.0",
53
+ "3.15.1",
54
54
  "cjs"
55
55
  );
56
56
  //# sourceMappingURL=index.js.map
@@ -32,11 +32,14 @@ class TLSocketRoom {
32
32
  constructor(opts) {
33
33
  this.opts = opts;
34
34
  const initialSnapshot = opts.initialSnapshot && "store" in opts.initialSnapshot ? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot) : opts.initialSnapshot;
35
+ this.syncCallbacks = {
36
+ onDataChange: opts.onDataChange,
37
+ onPresenceChange: opts.onPresenceChange
38
+ };
35
39
  this.room = new import_TLSyncRoom.TLSyncRoom({
40
+ ...this.syncCallbacks,
36
41
  schema: opts.schema ?? (0, import_tlschema.createTLSchema)(),
37
42
  snapshot: initialSnapshot,
38
- onDataChange: opts.onDataChange,
39
- onPresenceChange: opts.onPresenceChange,
40
43
  log: opts.log
41
44
  });
42
45
  this.room.events.on("session_removed", (args) => {
@@ -54,6 +57,7 @@ class TLSocketRoom {
54
57
  room;
55
58
  sessions = /* @__PURE__ */ new Map();
56
59
  log;
60
+ syncCallbacks;
57
61
  /**
58
62
  * Returns the number of active sessions.
59
63
  * Note that this is not the same as the number of connected sockets!
@@ -244,6 +248,7 @@ class TLSocketRoom {
244
248
  delete tombstones[id];
245
249
  });
246
250
  const newRoom = new import_TLSyncRoom.TLSyncRoom({
251
+ ...this.syncCallbacks,
247
252
  schema: oldRoom.schema,
248
253
  snapshot: {
249
254
  clock: oldRoom.clock + 1,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/TLSocketRoom.ts"],
4
- "sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot, createTLSchema } from '@tldraw/tlschema'\nimport { objectMapValues, structuredClone } from '@tldraw/utils'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n// TODO: structured logging support\n/** @public */\nexport interface TLSyncLog {\n\twarn?(...args: any[]): void\n\terror?(...args: any[]): void\n}\n\n/** @public */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\n\tconstructor(\n\t\tpublic readonly opts: {\n\t\t\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t\t\tschema?: StoreSchema<R, any>\n\t\t\t// how long to wait for a client to communicate before disconnecting them\n\t\t\tclientTimeout?: number\n\t\t\tlog?: TLSyncLog\n\t\t\t// a callback that is called when a client is disconnected\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonSessionRemoved?: (\n\t\t\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\t\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t\t\t) => void\n\t\t\t// a callback that is called whenever a message is sent\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonBeforeSendMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonAfterReceiveMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\tonDataChange?(): void\n\t\t\t/** @internal */\n\t\t\tonPresenceChange?(): void\n\t\t}\n\t) {\n\t\tconst initialSnapshot =\n\t\t\topts.initialSnapshot && 'store' in opts.initialSnapshot\n\t\t\t\t? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)\n\t\t\t\t: opts.initialSnapshot\n\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tsnapshot: initialSnapshot,\n\t\t\tonDataChange: opts.onDataChange,\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t\tlog: opts.log,\n\t\t})\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Call this when a client establishes a new socket connection.\n\t *\n\t * - `sessionId` is a unique ID for a browser tab. This is passed as a query param by the useSync hook.\n\t * - `socket` is a WebSocket-like object that the server uses to communicate with the client.\n\t * - `isReadonly` is an optional boolean that can be set to true if the client should not be able to make changes to the document. They will still be able to send presence updates.\n\t * - `meta` is an optional object that can be used to store additional information about the session.\n\t *\n\t * @param opts - The options object\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners\n\t * (e.g. Bun.serve, Cloudflare Worker with WebSocket hibernation), you should call this\n\t * method when messages are received. See our self-hosting example for Bun.serve for an example.\n\t *\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t * @param message - The message received from the client.\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket error occurs.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket is closed.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current 'clock' of the document.\n\t * The clock is an integer that increments every time the document changes.\n\t * The clock is stored as part of the snapshot of the document for consistency purposes.\n\t *\n\t * @returns The clock\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.room.documentClock\n\t}\n\n\t/**\n\t * Returns a deeply cloned record from the store, if available.\n\t * @param id - The id of the record\n\t * @returns the cloned record\n\t */\n\tgetRecord(id: string) {\n\t\treturn structuredClone(this.room.state.get().documents[id]?.state)\n\t}\n\n\t/**\n\t * Returns a list of the sessions in the room.\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Return a snapshot of the document state, including clock-related bookkeeping.\n\t * You can store this and load it later on when initializing a TLSocketRoom.\n\t * You can also pass a snapshot to {@link TLSocketRoom#loadSnapshot} if you need to revert to a previous state.\n\t * @returns The snapshot\n\t */\n\tgetCurrentSnapshot() {\n\t\treturn this.room.getSnapshot()\n\t}\n\n\t/**\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const document of Object.values(this.room.state.get().documents)) {\n\t\t\tif (document.state.typeName === this.room.presenceType?.typeName) {\n\t\t\t\tresult[document.state.id] = document.state\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Return a serialized snapshot of the document state, including clock-related bookkeeping.\n\t * @returns The serialized snapshot\n\t * @internal\n\t */\n\tgetCurrentSerializedSnapshot() {\n\t\treturn JSON.stringify(this.room.getSnapshot())\n\t}\n\n\t/**\n\t * Load a snapshot of the document state, overwriting the current state.\n\t * @param snapshot - The snapshot to load\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tif ('store' in snapshot) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\t\t}\n\t\tconst oldRoom = this.room\n\t\tconst oldIds = oldRoom.getSnapshot().documents.map((d) => d.state.id)\n\t\tconst newIds = new Set(snapshot.documents.map((d) => d.state.id))\n\t\tconst removedIds = oldIds.filter((id) => !newIds.has(id))\n\n\t\tconst tombstones = { ...snapshot.tombstones }\n\t\tremovedIds.forEach((id) => {\n\t\t\ttombstones[id] = oldRoom.clock + 1\n\t\t})\n\t\tnewIds.forEach((id) => {\n\t\t\tdelete tombstones[id]\n\t\t})\n\n\t\tconst newRoom = new TLSyncRoom<R, SessionMeta>({\n\t\t\tschema: oldRoom.schema,\n\t\t\tsnapshot: {\n\t\t\t\tclock: oldRoom.clock + 1,\n\t\t\t\tdocuments: snapshot.documents.map((d) => ({\n\t\t\t\t\tlastChangedClock: oldRoom.clock + 1,\n\t\t\t\t\tstate: d.state,\n\t\t\t\t})),\n\t\t\t\tschema: snapshot.schema,\n\t\t\t\ttombstones,\n\t\t\t},\n\t\t\tlog: this.log,\n\t\t})\n\t\t// replace room with new one and kick out all the clients\n\t\tthis.room = newRoom\n\t\toldRoom.close()\n\t}\n\n\t/**\n\t * Allow applying changes to the store inside of a transaction.\n\t *\n\t * You can get values from the store by id with `store.get(id)`.\n\t * These values are safe to mutate, but to commit the changes you must call `store.put(...)` with the updated value.\n\t * You can get all values in the store with `store.getAll()`.\n\t * You can also delete values with `store.delete(id)`.\n\t *\n\t * @example\n\t * ```ts\n\t * room.updateStore(store => {\n\t * const shape = store.get('shape:abc123')\n\t * shape.meta.approved = true\n\t * store.put(shape)\n\t * })\n\t * ```\n\t *\n\t * Changes to the store inside the callback are isolated from changes made by other clients until the transaction commits.\n\t *\n\t * @param updater - A function that will be called with a store object that can be used to make changes.\n\t * @returns A promise that resolves when the transaction is complete.\n\t */\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\treturn this.room.updateStore(updater)\n\t}\n\n\t/**\n\t * Immediately remove a session from the room, and close its socket if not already closed.\n\t *\n\t * The client will attempt to reconnect unless you provide a `fatalReason` parameter.\n\t *\n\t * The `fatalReason` parameter will be available in the return value of the `useSync` hook as `useSync().error.reason`.\n\t *\n\t * @param sessionId - The id of the session to remove\n\t * @param fatalReason - The reason message to use when calling .close on the underlying websocket\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Close the room and disconnect all clients. Call this before discarding the room instance or shutting down the server.\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t}\n\n\t/**\n\t * @returns true if the room is closed\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/** @public */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\nfunction convertStoreSnapshotToRoomSnapshot(snapshot: TLStoreSnapshot): RoomSnapshot {\n\treturn {\n\t\tclock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot, createTLSchema } from '@tldraw/tlschema'\nimport { objectMapValues, structuredClone } from '@tldraw/utils'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n// TODO: structured logging support\n/** @public */\nexport interface TLSyncLog {\n\twarn?(...args: any[]): void\n\terror?(...args: any[]): void\n}\n\n/** @public */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\tprivate readonly syncCallbacks: {\n\t\tonDataChange?(): void\n\t\tonPresenceChange?(): void\n\t}\n\n\tconstructor(\n\t\tpublic readonly opts: {\n\t\t\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t\t\tschema?: StoreSchema<R, any>\n\t\t\t// how long to wait for a client to communicate before disconnecting them\n\t\t\tclientTimeout?: number\n\t\t\tlog?: TLSyncLog\n\t\t\t// a callback that is called when a client is disconnected\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonSessionRemoved?: (\n\t\t\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\t\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t\t\t) => void\n\t\t\t// a callback that is called whenever a message is sent\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonBeforeSendMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonAfterReceiveMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\tonDataChange?(): void\n\t\t\t/** @internal */\n\t\t\tonPresenceChange?(): void\n\t\t}\n\t) {\n\t\tconst initialSnapshot =\n\t\t\topts.initialSnapshot && 'store' in opts.initialSnapshot\n\t\t\t\t? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)\n\t\t\t\t: opts.initialSnapshot\n\n\t\tthis.syncCallbacks = {\n\t\t\tonDataChange: opts.onDataChange,\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tsnapshot: initialSnapshot,\n\t\t\tlog: opts.log,\n\t\t})\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Call this when a client establishes a new socket connection.\n\t *\n\t * - `sessionId` is a unique ID for a browser tab. This is passed as a query param by the useSync hook.\n\t * - `socket` is a WebSocket-like object that the server uses to communicate with the client.\n\t * - `isReadonly` is an optional boolean that can be set to true if the client should not be able to make changes to the document. They will still be able to send presence updates.\n\t * - `meta` is an optional object that can be used to store additional information about the session.\n\t *\n\t * @param opts - The options object\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners\n\t * (e.g. Bun.serve, Cloudflare Worker with WebSocket hibernation), you should call this\n\t * method when messages are received. See our self-hosting example for Bun.serve for an example.\n\t *\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t * @param message - The message received from the client.\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket error occurs.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket is closed.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current 'clock' of the document.\n\t * The clock is an integer that increments every time the document changes.\n\t * The clock is stored as part of the snapshot of the document for consistency purposes.\n\t *\n\t * @returns The clock\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.room.documentClock\n\t}\n\n\t/**\n\t * Returns a deeply cloned record from the store, if available.\n\t * @param id - The id of the record\n\t * @returns the cloned record\n\t */\n\tgetRecord(id: string) {\n\t\treturn structuredClone(this.room.state.get().documents[id]?.state)\n\t}\n\n\t/**\n\t * Returns a list of the sessions in the room.\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Return a snapshot of the document state, including clock-related bookkeeping.\n\t * You can store this and load it later on when initializing a TLSocketRoom.\n\t * You can also pass a snapshot to {@link TLSocketRoom#loadSnapshot} if you need to revert to a previous state.\n\t * @returns The snapshot\n\t */\n\tgetCurrentSnapshot() {\n\t\treturn this.room.getSnapshot()\n\t}\n\n\t/**\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const document of Object.values(this.room.state.get().documents)) {\n\t\t\tif (document.state.typeName === this.room.presenceType?.typeName) {\n\t\t\t\tresult[document.state.id] = document.state\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Return a serialized snapshot of the document state, including clock-related bookkeeping.\n\t * @returns The serialized snapshot\n\t * @internal\n\t */\n\tgetCurrentSerializedSnapshot() {\n\t\treturn JSON.stringify(this.room.getSnapshot())\n\t}\n\n\t/**\n\t * Load a snapshot of the document state, overwriting the current state.\n\t * @param snapshot - The snapshot to load\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tif ('store' in snapshot) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\t\t}\n\t\tconst oldRoom = this.room\n\t\tconst oldIds = oldRoom.getSnapshot().documents.map((d) => d.state.id)\n\t\tconst newIds = new Set(snapshot.documents.map((d) => d.state.id))\n\t\tconst removedIds = oldIds.filter((id) => !newIds.has(id))\n\n\t\tconst tombstones = { ...snapshot.tombstones }\n\t\tremovedIds.forEach((id) => {\n\t\t\ttombstones[id] = oldRoom.clock + 1\n\t\t})\n\t\tnewIds.forEach((id) => {\n\t\t\tdelete tombstones[id]\n\t\t})\n\n\t\tconst newRoom = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: oldRoom.schema,\n\t\t\tsnapshot: {\n\t\t\t\tclock: oldRoom.clock + 1,\n\t\t\t\tdocuments: snapshot.documents.map((d) => ({\n\t\t\t\t\tlastChangedClock: oldRoom.clock + 1,\n\t\t\t\t\tstate: d.state,\n\t\t\t\t})),\n\t\t\t\tschema: snapshot.schema,\n\t\t\t\ttombstones,\n\t\t\t},\n\t\t\tlog: this.log,\n\t\t})\n\t\t// replace room with new one and kick out all the clients\n\t\tthis.room = newRoom\n\t\toldRoom.close()\n\t}\n\n\t/**\n\t * Allow applying changes to the store inside of a transaction.\n\t *\n\t * You can get values from the store by id with `store.get(id)`.\n\t * These values are safe to mutate, but to commit the changes you must call `store.put(...)` with the updated value.\n\t * You can get all values in the store with `store.getAll()`.\n\t * You can also delete values with `store.delete(id)`.\n\t *\n\t * @example\n\t * ```ts\n\t * room.updateStore(store => {\n\t * const shape = store.get('shape:abc123')\n\t * shape.meta.approved = true\n\t * store.put(shape)\n\t * })\n\t * ```\n\t *\n\t * Changes to the store inside the callback are isolated from changes made by other clients until the transaction commits.\n\t *\n\t * @param updater - A function that will be called with a store object that can be used to make changes.\n\t * @returns A promise that resolves when the transaction is complete.\n\t */\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\treturn this.room.updateStore(updater)\n\t}\n\n\t/**\n\t * Immediately remove a session from the room, and close its socket if not already closed.\n\t *\n\t * The client will attempt to reconnect unless you provide a `fatalReason` parameter.\n\t *\n\t * The `fatalReason` parameter will be available in the return value of the `useSync` hook as `useSync().error.reason`.\n\t *\n\t * @param sessionId - The id of the session to remove\n\t * @param fatalReason - The reason message to use when calling .close on the underlying websocket\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Close the room and disconnect all clients. Call this before discarding the room instance or shutting down the server.\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t}\n\n\t/**\n\t * @returns true if the room is closed\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/** @public */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\nfunction convertStoreSnapshotToRoomSnapshot(snapshot: TLStoreSnapshot): RoomSnapshot {\n\treturn {\n\t\tclock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,sBAAgD;AAChD,mBAAiD;AACjD,yBAAiC;AACjC,iCAAsD;AACtD,0BAA4C;AAC5C,wBAA2D;AAC3D,mBAAmC;AAW5B,MAAM,aAA0E;AAAA,EAatF,YACiB,MAiCf;AAjCe;AAkChB,UAAM,kBACL,KAAK,mBAAmB,WAAW,KAAK,kBACrC,mCAAmC,KAAK,eAAgB,IACxD,KAAK;AAET,SAAK,gBAAgB;AAAA,MACpB,cAAc,KAAK;AAAA,MACnB,kBAAkB,KAAK;AAAA,IACxB;AACA,SAAK,OAAO,IAAI,6BAA2B;AAAA,MAC1C,GAAG,KAAK;AAAA,MACR,QAAQ,KAAK,cAAW,gCAAe;AAAA,MACvC,UAAU;AAAA,MACV,KAAK,KAAK;AAAA,IACX,CAAC;AACD,SAAK,KAAK,OAAO,GAAG,mBAAmB,CAAC,SAAS;AAChD,WAAK,SAAS,OAAO,KAAK,SAAS;AACnC,UAAI,KAAK,KAAK,kBAAkB;AAC/B,aAAK,KAAK,iBAAiB,MAAM;AAAA,UAChC,WAAW,KAAK;AAAA,UAChB,sBAAsB,KAAK,KAAK,SAAS;AAAA,UACzC,MAAM,KAAK;AAAA,QACZ,CAAC;AAAA,MACF;AAAA,IACD,CAAC;AACD,SAAK,MAAM,SAAS,OAAO,KAAK,MAAM,EAAE,OAAO,QAAQ,MAAM;AAAA,EAC9D;AAAA,EAzEQ;AAAA,EACS,WAAW,oBAAI,IAI9B;AAAA,EACO;AAAA,EACQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2EjB,uBAAuB;AACtB,WAAO,KAAK,KAAK,SAAS;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,oBACC,MAKC;AACD,UAAM,EAAE,WAAW,QAAQ,aAAa,MAAM,IAAI;AAClD,UAAM,sBAAsB,CAAC,UAC5B,KAAK,oBAAoB,WAAW,MAAM,IAAI;AAC/C,UAAM,oBAAoB,KAAK,kBAAkB,KAAK,MAAM,SAAS;AACrE,UAAM,oBAAoB,KAAK,kBAAkB,KAAK,MAAM,SAAS;AAErE,SAAK,SAAS,IAAI,WAAW;AAAA,MAC5B,WAAW,IAAI,gCAAmB;AAAA,MAClC;AAAA,MACA,UAAU,MAAM;AACf,eAAO,sBAAsB,WAAW,mBAAmB;AAC3D,eAAO,sBAAsB,SAAS,iBAAiB;AACvD,eAAO,sBAAsB,SAAS,iBAAiB;AAAA,MACxD;AAAA,IACD,CAAC;AAED,SAAK,KAAK,iBAAiB;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,QAAQ,IAAI,+CAAoB;AAAA,QAC/B,IAAI;AAAA,QACJ,qBAAqB,KAAK,KAAK,sBAC5B,CAAC,SAAS,gBACV,KAAK,KAAK,oBAAqB;AAAA,UAC9B;AAAA,UACA;AAAA,UACA;AAAA,UACA,MAAM,KAAK,KAAK,SAAS,IAAI,SAAS,GAAG;AAAA,QAC1C,CAAC,IACD;AAAA,MACJ,CAAC;AAAA,MACD,MAAM,UAAU,OAAQ,KAAK,OAAe;AAAA,IAC7C,CAAC;AAED,WAAO,mBAAmB,WAAW,mBAAmB;AACxD,WAAO,mBAAmB,SAAS,iBAAiB;AACpD,WAAO,mBAAmB,SAAS,iBAAiB;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,oBAAoB,WAAmB,SAA2C;AACjF,UAAM,YAAY,KAAK,SAAS,IAAI,SAAS,GAAG;AAChD,QAAI,CAAC,WAAW;AACf,WAAK,KAAK,OAAO,yCAAyC,SAAS;AACnE;AAAA,IACD;AAEA,QAAI;AACH,YAAM,gBACL,OAAO,YAAY,WAAW,UAAU,IAAI,YAAY,EAAE,OAAO,OAAO;AACzE,YAAM,MAAM,UAAU,cAAc,aAAa;AACjD,UAAI,CAAC,KAAK;AAET;AAAA,MACD;AACA,UAAI,UAAU,KAAK;AAElB,YAAI,KAAK,KAAK,uBAAuB;AACpC,gBAAM,UAAU,KAAK,KAAK,SAAS,IAAI,SAAS;AAChD,cAAI,SAAS;AACZ,iBAAK,KAAK,sBAAsB;AAAA,cAC/B;AAAA,cACA,SAAS,IAAI;AAAA,cACb,aAAa,IAAI;AAAA,cACjB,MAAM,QAAQ;AAAA,YACf,CAAC;AAAA,UACF;AAAA,QACD;AAEA,aAAK,KAAK,cAAc,WAAW,IAAI,IAAW;AAAA,MACnD,OAAO;AACN,aAAK,KAAK,QAAQ,4BAA4B,IAAI,KAAK;AAEvD,aAAK,kBAAkB,SAAS;AAAA,MACjC;AAAA,IACD,SAAS,GAAG;AACX,WAAK,KAAK,QAAQ,CAAC;AAGnB,WAAK,KAAK,cAAc,WAAW,gDAA4B,aAAa;AAAA,IAC7E;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,kBAAkB,WAAmB;AACpC,SAAK,KAAK,YAAY,SAAS;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,kBAAkB,WAAmB;AACpC,SAAK,KAAK,YAAY,SAAS;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,0BAA0B;AACzB,WAAO,KAAK,KAAK;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAU,IAAY;AACrB,eAAO,8BAAgB,KAAK,KAAK,MAAM,IAAI,EAAE,UAAU,EAAE,GAAG,KAAK;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA,EAKA,cAKG;AACF,WAAO,CAAC,GAAG,KAAK,KAAK,SAAS,OAAO,CAAC,EAAE,IAAI,CAAC,YAAY;AACxD,aAAO;AAAA,QACN,WAAW,QAAQ;AAAA,QACnB,aAAa,QAAQ,UAAU,oCAAiB;AAAA,QAChD,YAAY,QAAQ;AAAA,QACpB,MAAM,QAAQ;AAAA,MACf;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,qBAAqB;AACpB,WAAO,KAAK,KAAK,YAAY;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,qBAAqB;AACpB,UAAM,SAAS,CAAC;AAChB,eAAW,YAAY,OAAO,OAAO,KAAK,KAAK,MAAM,IAAI,EAAE,SAAS,GAAG;AACtE,UAAI,SAAS,MAAM,aAAa,KAAK,KAAK,cAAc,UAAU;AACjE,eAAO,SAAS,MAAM,EAAE,IAAI,SAAS;AAAA,MACtC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,+BAA+B;AAC9B,WAAO,KAAK,UAAU,KAAK,KAAK,YAAY,CAAC;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,UAA0C;AACtD,QAAI,WAAW,UAAU;AACxB,iBAAW,mCAAmC,QAAQ;AAAA,IACvD;AACA,UAAM,UAAU,KAAK;AACrB,UAAM,SAAS,QAAQ,YAAY,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE;AACpE,UAAM,SAAS,IAAI,IAAI,SAAS,UAAU,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;AAChE,UAAM,aAAa,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;AAExD,UAAM,aAAa,EAAE,GAAG,SAAS,WAAW;AAC5C,eAAW,QAAQ,CAAC,OAAO;AAC1B,iBAAW,EAAE,IAAI,QAAQ,QAAQ;AAAA,IAClC,CAAC;AACD,WAAO,QAAQ,CAAC,OAAO;AACtB,aAAO,WAAW,EAAE;AAAA,IACrB,CAAC;AAED,UAAM,UAAU,IAAI,6BAA2B;AAAA,MAC9C,GAAG,KAAK;AAAA,MACR,QAAQ,QAAQ;AAAA,MAChB,UAAU;AAAA,QACT,OAAO,QAAQ,QAAQ;AAAA,QACvB,WAAW,SAAS,UAAU,IAAI,CAAC,OAAO;AAAA,UACzC,kBAAkB,QAAQ,QAAQ;AAAA,UAClC,OAAO,EAAE;AAAA,QACV,EAAE;AAAA,QACF,QAAQ,SAAS;AAAA,QACjB;AAAA,MACD;AAAA,MACA,KAAK,KAAK;AAAA,IACX,CAAC;AAED,SAAK,OAAO;AACZ,YAAQ,MAAM;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwBA,MAAM,YAAY,SAA+D;AAChF,WAAO,KAAK,KAAK,YAAY,OAAO;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,aAAa,WAAmB,aAAoD;AACnF,SAAK,KAAK,cAAc,WAAW,WAAW;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ;AACP,SAAK,KAAK,MAAM;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW;AACV,WAAO,KAAK,KAAK,SAAS;AAAA,EAC3B;AACD;AAOA,SAAS,mCAAmC,UAAyC;AACpF,SAAO;AAAA,IACN,OAAO;AAAA,IACP,eAAW,8BAAgB,SAAS,KAAK,EAAE,IAAI,CAAC,WAAW;AAAA,MAC1D;AAAA,MACA,kBAAkB;AAAA,IACnB,EAAE;AAAA,IACF,QAAQ,SAAS;AAAA,IACjB,YAAY,CAAC;AAAA,EACd;AACD;",
6
6
  "names": []
7
7
  }
@@ -130,6 +130,7 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
130
130
  private room;
131
131
  private readonly sessions;
132
132
  readonly log?: TLSyncLog;
133
+ private readonly syncCallbacks;
133
134
  constructor(opts: {
134
135
  /* Excluded from this release type: onPresenceChange */
135
136
  clientTimeout?: number;
@@ -26,7 +26,7 @@ import {
26
26
  } from "./lib/TLSyncRoom.mjs";
27
27
  registerTldrawLibraryVersion(
28
28
  "@tldraw/sync-core",
29
- "3.15.0",
29
+ "3.15.1",
30
30
  "esm"
31
31
  );
32
32
  export {
@@ -9,11 +9,14 @@ class TLSocketRoom {
9
9
  constructor(opts) {
10
10
  this.opts = opts;
11
11
  const initialSnapshot = opts.initialSnapshot && "store" in opts.initialSnapshot ? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot) : opts.initialSnapshot;
12
+ this.syncCallbacks = {
13
+ onDataChange: opts.onDataChange,
14
+ onPresenceChange: opts.onPresenceChange
15
+ };
12
16
  this.room = new TLSyncRoom({
17
+ ...this.syncCallbacks,
13
18
  schema: opts.schema ?? createTLSchema(),
14
19
  snapshot: initialSnapshot,
15
- onDataChange: opts.onDataChange,
16
- onPresenceChange: opts.onPresenceChange,
17
20
  log: opts.log
18
21
  });
19
22
  this.room.events.on("session_removed", (args) => {
@@ -31,6 +34,7 @@ class TLSocketRoom {
31
34
  room;
32
35
  sessions = /* @__PURE__ */ new Map();
33
36
  log;
37
+ syncCallbacks;
34
38
  /**
35
39
  * Returns the number of active sessions.
36
40
  * Note that this is not the same as the number of connected sockets!
@@ -221,6 +225,7 @@ class TLSocketRoom {
221
225
  delete tombstones[id];
222
226
  });
223
227
  const newRoom = new TLSyncRoom({
228
+ ...this.syncCallbacks,
224
229
  schema: oldRoom.schema,
225
230
  snapshot: {
226
231
  clock: oldRoom.clock + 1,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/TLSocketRoom.ts"],
4
- "sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot, createTLSchema } from '@tldraw/tlschema'\nimport { objectMapValues, structuredClone } from '@tldraw/utils'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n// TODO: structured logging support\n/** @public */\nexport interface TLSyncLog {\n\twarn?(...args: any[]): void\n\terror?(...args: any[]): void\n}\n\n/** @public */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\n\tconstructor(\n\t\tpublic readonly opts: {\n\t\t\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t\t\tschema?: StoreSchema<R, any>\n\t\t\t// how long to wait for a client to communicate before disconnecting them\n\t\t\tclientTimeout?: number\n\t\t\tlog?: TLSyncLog\n\t\t\t// a callback that is called when a client is disconnected\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonSessionRemoved?: (\n\t\t\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\t\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t\t\t) => void\n\t\t\t// a callback that is called whenever a message is sent\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonBeforeSendMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonAfterReceiveMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\tonDataChange?(): void\n\t\t\t/** @internal */\n\t\t\tonPresenceChange?(): void\n\t\t}\n\t) {\n\t\tconst initialSnapshot =\n\t\t\topts.initialSnapshot && 'store' in opts.initialSnapshot\n\t\t\t\t? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)\n\t\t\t\t: opts.initialSnapshot\n\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tsnapshot: initialSnapshot,\n\t\t\tonDataChange: opts.onDataChange,\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t\tlog: opts.log,\n\t\t})\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Call this when a client establishes a new socket connection.\n\t *\n\t * - `sessionId` is a unique ID for a browser tab. This is passed as a query param by the useSync hook.\n\t * - `socket` is a WebSocket-like object that the server uses to communicate with the client.\n\t * - `isReadonly` is an optional boolean that can be set to true if the client should not be able to make changes to the document. They will still be able to send presence updates.\n\t * - `meta` is an optional object that can be used to store additional information about the session.\n\t *\n\t * @param opts - The options object\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners\n\t * (e.g. Bun.serve, Cloudflare Worker with WebSocket hibernation), you should call this\n\t * method when messages are received. See our self-hosting example for Bun.serve for an example.\n\t *\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t * @param message - The message received from the client.\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket error occurs.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket is closed.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current 'clock' of the document.\n\t * The clock is an integer that increments every time the document changes.\n\t * The clock is stored as part of the snapshot of the document for consistency purposes.\n\t *\n\t * @returns The clock\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.room.documentClock\n\t}\n\n\t/**\n\t * Returns a deeply cloned record from the store, if available.\n\t * @param id - The id of the record\n\t * @returns the cloned record\n\t */\n\tgetRecord(id: string) {\n\t\treturn structuredClone(this.room.state.get().documents[id]?.state)\n\t}\n\n\t/**\n\t * Returns a list of the sessions in the room.\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Return a snapshot of the document state, including clock-related bookkeeping.\n\t * You can store this and load it later on when initializing a TLSocketRoom.\n\t * You can also pass a snapshot to {@link TLSocketRoom#loadSnapshot} if you need to revert to a previous state.\n\t * @returns The snapshot\n\t */\n\tgetCurrentSnapshot() {\n\t\treturn this.room.getSnapshot()\n\t}\n\n\t/**\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const document of Object.values(this.room.state.get().documents)) {\n\t\t\tif (document.state.typeName === this.room.presenceType?.typeName) {\n\t\t\t\tresult[document.state.id] = document.state\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Return a serialized snapshot of the document state, including clock-related bookkeeping.\n\t * @returns The serialized snapshot\n\t * @internal\n\t */\n\tgetCurrentSerializedSnapshot() {\n\t\treturn JSON.stringify(this.room.getSnapshot())\n\t}\n\n\t/**\n\t * Load a snapshot of the document state, overwriting the current state.\n\t * @param snapshot - The snapshot to load\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tif ('store' in snapshot) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\t\t}\n\t\tconst oldRoom = this.room\n\t\tconst oldIds = oldRoom.getSnapshot().documents.map((d) => d.state.id)\n\t\tconst newIds = new Set(snapshot.documents.map((d) => d.state.id))\n\t\tconst removedIds = oldIds.filter((id) => !newIds.has(id))\n\n\t\tconst tombstones = { ...snapshot.tombstones }\n\t\tremovedIds.forEach((id) => {\n\t\t\ttombstones[id] = oldRoom.clock + 1\n\t\t})\n\t\tnewIds.forEach((id) => {\n\t\t\tdelete tombstones[id]\n\t\t})\n\n\t\tconst newRoom = new TLSyncRoom<R, SessionMeta>({\n\t\t\tschema: oldRoom.schema,\n\t\t\tsnapshot: {\n\t\t\t\tclock: oldRoom.clock + 1,\n\t\t\t\tdocuments: snapshot.documents.map((d) => ({\n\t\t\t\t\tlastChangedClock: oldRoom.clock + 1,\n\t\t\t\t\tstate: d.state,\n\t\t\t\t})),\n\t\t\t\tschema: snapshot.schema,\n\t\t\t\ttombstones,\n\t\t\t},\n\t\t\tlog: this.log,\n\t\t})\n\t\t// replace room with new one and kick out all the clients\n\t\tthis.room = newRoom\n\t\toldRoom.close()\n\t}\n\n\t/**\n\t * Allow applying changes to the store inside of a transaction.\n\t *\n\t * You can get values from the store by id with `store.get(id)`.\n\t * These values are safe to mutate, but to commit the changes you must call `store.put(...)` with the updated value.\n\t * You can get all values in the store with `store.getAll()`.\n\t * You can also delete values with `store.delete(id)`.\n\t *\n\t * @example\n\t * ```ts\n\t * room.updateStore(store => {\n\t * const shape = store.get('shape:abc123')\n\t * shape.meta.approved = true\n\t * store.put(shape)\n\t * })\n\t * ```\n\t *\n\t * Changes to the store inside the callback are isolated from changes made by other clients until the transaction commits.\n\t *\n\t * @param updater - A function that will be called with a store object that can be used to make changes.\n\t * @returns A promise that resolves when the transaction is complete.\n\t */\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\treturn this.room.updateStore(updater)\n\t}\n\n\t/**\n\t * Immediately remove a session from the room, and close its socket if not already closed.\n\t *\n\t * The client will attempt to reconnect unless you provide a `fatalReason` parameter.\n\t *\n\t * The `fatalReason` parameter will be available in the return value of the `useSync` hook as `useSync().error.reason`.\n\t *\n\t * @param sessionId - The id of the session to remove\n\t * @param fatalReason - The reason message to use when calling .close on the underlying websocket\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Close the room and disconnect all clients. Call this before discarding the room instance or shutting down the server.\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t}\n\n\t/**\n\t * @returns true if the room is closed\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/** @public */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\nfunction convertStoreSnapshotToRoomSnapshot(snapshot: TLStoreSnapshot): RoomSnapshot {\n\treturn {\n\t\tclock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot, createTLSchema } from '@tldraw/tlschema'\nimport { objectMapValues, structuredClone } from '@tldraw/utils'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n// TODO: structured logging support\n/** @public */\nexport interface TLSyncLog {\n\twarn?(...args: any[]): void\n\terror?(...args: any[]): void\n}\n\n/** @public */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\tprivate readonly syncCallbacks: {\n\t\tonDataChange?(): void\n\t\tonPresenceChange?(): void\n\t}\n\n\tconstructor(\n\t\tpublic readonly opts: {\n\t\t\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t\t\tschema?: StoreSchema<R, any>\n\t\t\t// how long to wait for a client to communicate before disconnecting them\n\t\t\tclientTimeout?: number\n\t\t\tlog?: TLSyncLog\n\t\t\t// a callback that is called when a client is disconnected\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonSessionRemoved?: (\n\t\t\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\t\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t\t\t) => void\n\t\t\t// a callback that is called whenever a message is sent\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonBeforeSendMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonAfterReceiveMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\tonDataChange?(): void\n\t\t\t/** @internal */\n\t\t\tonPresenceChange?(): void\n\t\t}\n\t) {\n\t\tconst initialSnapshot =\n\t\t\topts.initialSnapshot && 'store' in opts.initialSnapshot\n\t\t\t\t? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)\n\t\t\t\t: opts.initialSnapshot\n\n\t\tthis.syncCallbacks = {\n\t\t\tonDataChange: opts.onDataChange,\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tsnapshot: initialSnapshot,\n\t\t\tlog: opts.log,\n\t\t})\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Call this when a client establishes a new socket connection.\n\t *\n\t * - `sessionId` is a unique ID for a browser tab. This is passed as a query param by the useSync hook.\n\t * - `socket` is a WebSocket-like object that the server uses to communicate with the client.\n\t * - `isReadonly` is an optional boolean that can be set to true if the client should not be able to make changes to the document. They will still be able to send presence updates.\n\t * - `meta` is an optional object that can be used to store additional information about the session.\n\t *\n\t * @param opts - The options object\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners\n\t * (e.g. Bun.serve, Cloudflare Worker with WebSocket hibernation), you should call this\n\t * method when messages are received. See our self-hosting example for Bun.serve for an example.\n\t *\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t * @param message - The message received from the client.\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket error occurs.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket is closed.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current 'clock' of the document.\n\t * The clock is an integer that increments every time the document changes.\n\t * The clock is stored as part of the snapshot of the document for consistency purposes.\n\t *\n\t * @returns The clock\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.room.documentClock\n\t}\n\n\t/**\n\t * Returns a deeply cloned record from the store, if available.\n\t * @param id - The id of the record\n\t * @returns the cloned record\n\t */\n\tgetRecord(id: string) {\n\t\treturn structuredClone(this.room.state.get().documents[id]?.state)\n\t}\n\n\t/**\n\t * Returns a list of the sessions in the room.\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Return a snapshot of the document state, including clock-related bookkeeping.\n\t * You can store this and load it later on when initializing a TLSocketRoom.\n\t * You can also pass a snapshot to {@link TLSocketRoom#loadSnapshot} if you need to revert to a previous state.\n\t * @returns The snapshot\n\t */\n\tgetCurrentSnapshot() {\n\t\treturn this.room.getSnapshot()\n\t}\n\n\t/**\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const document of Object.values(this.room.state.get().documents)) {\n\t\t\tif (document.state.typeName === this.room.presenceType?.typeName) {\n\t\t\t\tresult[document.state.id] = document.state\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Return a serialized snapshot of the document state, including clock-related bookkeeping.\n\t * @returns The serialized snapshot\n\t * @internal\n\t */\n\tgetCurrentSerializedSnapshot() {\n\t\treturn JSON.stringify(this.room.getSnapshot())\n\t}\n\n\t/**\n\t * Load a snapshot of the document state, overwriting the current state.\n\t * @param snapshot - The snapshot to load\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tif ('store' in snapshot) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\t\t}\n\t\tconst oldRoom = this.room\n\t\tconst oldIds = oldRoom.getSnapshot().documents.map((d) => d.state.id)\n\t\tconst newIds = new Set(snapshot.documents.map((d) => d.state.id))\n\t\tconst removedIds = oldIds.filter((id) => !newIds.has(id))\n\n\t\tconst tombstones = { ...snapshot.tombstones }\n\t\tremovedIds.forEach((id) => {\n\t\t\ttombstones[id] = oldRoom.clock + 1\n\t\t})\n\t\tnewIds.forEach((id) => {\n\t\t\tdelete tombstones[id]\n\t\t})\n\n\t\tconst newRoom = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: oldRoom.schema,\n\t\t\tsnapshot: {\n\t\t\t\tclock: oldRoom.clock + 1,\n\t\t\t\tdocuments: snapshot.documents.map((d) => ({\n\t\t\t\t\tlastChangedClock: oldRoom.clock + 1,\n\t\t\t\t\tstate: d.state,\n\t\t\t\t})),\n\t\t\t\tschema: snapshot.schema,\n\t\t\t\ttombstones,\n\t\t\t},\n\t\t\tlog: this.log,\n\t\t})\n\t\t// replace room with new one and kick out all the clients\n\t\tthis.room = newRoom\n\t\toldRoom.close()\n\t}\n\n\t/**\n\t * Allow applying changes to the store inside of a transaction.\n\t *\n\t * You can get values from the store by id with `store.get(id)`.\n\t * These values are safe to mutate, but to commit the changes you must call `store.put(...)` with the updated value.\n\t * You can get all values in the store with `store.getAll()`.\n\t * You can also delete values with `store.delete(id)`.\n\t *\n\t * @example\n\t * ```ts\n\t * room.updateStore(store => {\n\t * const shape = store.get('shape:abc123')\n\t * shape.meta.approved = true\n\t * store.put(shape)\n\t * })\n\t * ```\n\t *\n\t * Changes to the store inside the callback are isolated from changes made by other clients until the transaction commits.\n\t *\n\t * @param updater - A function that will be called with a store object that can be used to make changes.\n\t * @returns A promise that resolves when the transaction is complete.\n\t */\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\treturn this.room.updateStore(updater)\n\t}\n\n\t/**\n\t * Immediately remove a session from the room, and close its socket if not already closed.\n\t *\n\t * The client will attempt to reconnect unless you provide a `fatalReason` parameter.\n\t *\n\t * The `fatalReason` parameter will be available in the return value of the `useSync` hook as `useSync().error.reason`.\n\t *\n\t * @param sessionId - The id of the session to remove\n\t * @param fatalReason - The reason message to use when calling .close on the underlying websocket\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Close the room and disconnect all clients. Call this before discarding the room instance or shutting down the server.\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t}\n\n\t/**\n\t * @returns true if the room is closed\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/** @public */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\nfunction convertStoreSnapshotToRoomSnapshot(snapshot: TLStoreSnapshot): RoomSnapshot {\n\treturn {\n\t\tclock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync-core",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync).",
4
- "version": "3.15.0",
4
+ "version": "3.15.1",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -47,7 +47,7 @@
47
47
  "@types/uuid-readable": "^0.0.3",
48
48
  "react": "^18.3.1",
49
49
  "react-dom": "^18.3.1",
50
- "tldraw": "3.15.0",
50
+ "tldraw": "3.15.1",
51
51
  "typescript": "^5.8.3",
52
52
  "uuid-by-string": "^4.0.0",
53
53
  "uuid-readable": "^0.0.2"
@@ -67,10 +67,10 @@
67
67
  ]
68
68
  },
69
69
  "dependencies": {
70
- "@tldraw/state": "3.15.0",
71
- "@tldraw/store": "3.15.0",
72
- "@tldraw/tlschema": "3.15.0",
73
- "@tldraw/utils": "3.15.0",
70
+ "@tldraw/state": "3.15.1",
71
+ "@tldraw/store": "3.15.1",
72
+ "@tldraw/tlschema": "3.15.1",
73
+ "@tldraw/utils": "3.15.1",
74
74
  "nanoevents": "^7.0.1",
75
75
  "ws": "^8.18.0"
76
76
  },
@@ -24,6 +24,10 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
24
24
  { assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }
25
25
  >()
26
26
  readonly log?: TLSyncLog
27
+ private readonly syncCallbacks: {
28
+ onDataChange?(): void
29
+ onPresenceChange?(): void
30
+ }
27
31
 
28
32
  constructor(
29
33
  public readonly opts: {
@@ -65,11 +69,14 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
65
69
  ? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)
66
70
  : opts.initialSnapshot
67
71
 
72
+ this.syncCallbacks = {
73
+ onDataChange: opts.onDataChange,
74
+ onPresenceChange: opts.onPresenceChange,
75
+ }
68
76
  this.room = new TLSyncRoom<R, SessionMeta>({
77
+ ...this.syncCallbacks,
69
78
  schema: opts.schema ?? (createTLSchema() as any),
70
79
  snapshot: initialSnapshot,
71
- onDataChange: opts.onDataChange,
72
- onPresenceChange: opts.onPresenceChange,
73
80
  log: opts.log,
74
81
  })
75
82
  this.room.events.on('session_removed', (args) => {
@@ -314,6 +321,7 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
314
321
  })
315
322
 
316
323
  const newRoom = new TLSyncRoom<R, SessionMeta>({
324
+ ...this.syncCallbacks,
317
325
  schema: oldRoom.schema,
318
326
  snapshot: {
319
327
  clock: oldRoom.clock + 1,
@@ -1,4 +1,4 @@
1
- import { createTLSchema, createTLStore } from 'tldraw'
1
+ import { createTLSchema, createTLStore, PageRecordType, ZERO_INDEX_KEY } from 'tldraw'
2
2
  import { TLSocketRoom } from '../lib/TLSocketRoom'
3
3
 
4
4
  function getStore() {
@@ -97,4 +97,28 @@ describe(TLSocketRoom, () => {
97
97
  ]
98
98
  `)
99
99
  })
100
+
101
+ it('passes onDataChange handler through', async () => {
102
+ const addPage = (room: TLSocketRoom) =>
103
+ room.updateStore((store) => {
104
+ store.put(
105
+ PageRecordType.create({ id: PageRecordType.createId(), name: '', index: ZERO_INDEX_KEY })
106
+ )
107
+ })
108
+ const store = getStore()
109
+ store.ensureStoreIsUsable()
110
+ let called = 0
111
+
112
+ const room = new TLSocketRoom({ onDataChange: () => ++called })
113
+ expect(called).toEqual(0)
114
+
115
+ await addPage(room)
116
+ expect(called).toEqual(1)
117
+
118
+ room.loadSnapshot(room.getCurrentSnapshot())
119
+ expect(called).toEqual(1)
120
+
121
+ await addPage(room)
122
+ expect(called).toEqual(2)
123
+ })
100
124
  })