@voidhash/mimic 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +202 -198
- package/dist/Document.cjs +1 -2
- package/dist/Document.d.cts +9 -3
- package/dist/Document.d.cts.map +1 -1
- package/dist/Document.d.mts +9 -3
- package/dist/Document.d.mts.map +1 -1
- package/dist/Document.mjs +1 -2
- package/dist/Document.mjs.map +1 -1
- package/dist/Primitive.d.cts +2 -2
- package/dist/Primitive.d.mts +2 -2
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.cjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.mjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.cjs +14 -0
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.mjs +13 -0
- package/dist/client/ClientDocument.cjs +17 -12
- package/dist/client/ClientDocument.d.mts.map +1 -1
- package/dist/client/ClientDocument.mjs +17 -12
- package/dist/client/ClientDocument.mjs.map +1 -1
- package/dist/client/WebSocketTransport.cjs +6 -6
- package/dist/client/WebSocketTransport.mjs +6 -6
- package/dist/client/WebSocketTransport.mjs.map +1 -1
- package/dist/primitives/Tree.cjs +55 -7
- package/dist/primitives/Tree.d.cts +97 -10
- package/dist/primitives/Tree.d.cts.map +1 -1
- package/dist/primitives/Tree.d.mts +97 -10
- package/dist/primitives/Tree.d.mts.map +1 -1
- package/dist/primitives/Tree.mjs +55 -7
- package/dist/primitives/Tree.mjs.map +1 -1
- package/dist/primitives/shared.d.cts +9 -0
- package/dist/primitives/shared.d.cts.map +1 -1
- package/dist/primitives/shared.d.mts +9 -0
- package/dist/primitives/shared.d.mts.map +1 -1
- package/dist/primitives/shared.mjs.map +1 -1
- package/dist/server/ServerDocument.cjs +1 -1
- package/dist/server/ServerDocument.d.cts +3 -3
- package/dist/server/ServerDocument.d.cts.map +1 -1
- package/dist/server/ServerDocument.d.mts +3 -3
- package/dist/server/ServerDocument.d.mts.map +1 -1
- package/dist/server/ServerDocument.mjs +1 -1
- package/dist/server/ServerDocument.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Document.ts +18 -5
- package/src/client/ClientDocument.ts +20 -21
- package/src/client/WebSocketTransport.ts +9 -9
- package/src/primitives/Tree.ts +209 -19
- package/src/primitives/shared.ts +10 -1
- package/src/server/ServerDocument.ts +4 -3
- package/tests/client/ClientDocument.test.ts +309 -2
- package/tests/client/WebSocketTransport.test.ts +228 -3
- package/tests/primitives/Tree.test.ts +291 -17
- package/tests/server/ServerDocument.test.ts +1 -1
- package/tsconfig.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientDocument.mjs","names":["_serverState: Primitive.InferState<TSchema> | undefined","_pending: PendingTransaction[]","_serverTransactionHistory: Transaction.Transaction[]","Document.make","_unsubscribe: (() => void) | null","_initState: InitState","_initTimeoutHandle: ReturnType<typeof setTimeout> | null","_initResolver: (() => void) | null","_initRejecter: ((error: Error) => void) | null","_presenceSelfId: string | undefined","_presenceSelfData: unknown","pending: PendingTransaction","Rebase.transformTransactionWithPrimitive","Rebase.rebaseAfterRejectionWithPrimitive","Transaction.isEmpty","Presence.validate"],"sources":["../../src/client/ClientDocument.ts"],"sourcesContent":["import * as Document from \"../Document\";\nimport * as Transaction from \"../Transaction\";\nimport * as Presence from \"../Presence\";\nimport type * as Primitive from \"../Primitive\";\nimport type * as Transport from \"./Transport\";\nimport * as Rebase from \"./Rebase\";\nimport {\n TransactionRejectedError,\n NotConnectedError,\n InvalidStateError,\n} from \"./errors\";\n\n// =============================================================================\n// Client Document Types\n// =============================================================================\n\n/**\n * Pending transaction with metadata for tracking.\n */\ninterface PendingTransaction {\n /** The transaction */\n readonly transaction: Transaction.Transaction;\n /** Original transaction before any rebasing */\n readonly original: Transaction.Transaction;\n /** Timestamp when the transaction was sent */\n readonly sentAt: number;\n}\n\n/**\n * Initialization state for the client document.\n * Handles the race condition during startup where transactions\n * may arrive while fetching the initial snapshot.\n */\ntype InitState =\n | { readonly type: \"uninitialized\" }\n | { readonly type: \"initializing\"; readonly bufferedMessages: Transport.ServerMessage[] }\n | { readonly type: \"ready\" };\n\n// =============================================================================\n// Presence Types\n// =============================================================================\n\n/**\n * Listener for presence changes.\n */\nexport interface PresenceListener<_TData> {\n /** Called when any presence changes (self or others) */\n readonly onPresenceChange?: () => void;\n}\n\n/**\n * Presence API exposed on the ClientDocument.\n */\nexport interface ClientPresence<TData> {\n /**\n * Returns this client's connection ID (set after receiving presence_snapshot).\n * Returns undefined before the snapshot is received.\n */\n readonly selfId: () => string | undefined;\n\n /**\n * Returns this client's current presence data.\n * Returns undefined if not set.\n */\n readonly self: () => TData | undefined;\n\n /**\n * Returns a map of other clients' presence data.\n * Keys are connection IDs.\n */\n readonly others: () => ReadonlyMap<string, Presence.PresenceEntry<TData>>;\n\n /**\n * Returns all presence entries including self.\n */\n readonly all: () => ReadonlyMap<string, Presence.PresenceEntry<TData>>;\n\n /**\n * Sets this client's presence data.\n * Validates against the presence schema before sending.\n * @throws ParseError if validation fails\n */\n readonly set: (data: TData) => void;\n\n /**\n * Clears this client's presence data.\n */\n readonly clear: () => void;\n\n /**\n * Subscribes to presence changes.\n * @returns Unsubscribe function\n */\n readonly subscribe: (listener: PresenceListener<TData>) => () => void;\n}\n\n/**\n * Options for creating a ClientDocument.\n */\nexport interface ClientDocumentOptions<\n TSchema extends Primitive.AnyPrimitive,\n TPresence extends Presence.AnyPresence | undefined = undefined\n> {\n /** The schema defining the document structure */\n readonly schema: TSchema;\n /** Transport for server communication */\n readonly transport: Transport.Transport;\n /** Initial state (optional, will sync from server if not provided) */\n readonly initialState?: Primitive.InferState<TSchema>;\n /** Initial server version (optional) */\n readonly initialVersion?: number;\n /** Called when server rejects a transaction */\n readonly onRejection?: (\n transaction: Transaction.Transaction,\n reason: string\n ) => void;\n /** Called when optimistic state changes */\n readonly onStateChange?: (state: Primitive.InferState<TSchema> | undefined) => void;\n /** Called when connection status changes */\n readonly onConnectionChange?: (connected: boolean) => void;\n /** Called when client is fully initialized and ready */\n readonly onReady?: () => void;\n /** Timeout in ms for pending transactions (default: 30000) */\n readonly transactionTimeout?: number;\n /** Timeout in ms for initialization (default: 10000) */\n readonly initTimeout?: number;\n /** Enable debug logging for all activity (default: false) */\n readonly debug?: boolean;\n /**\n * Optional presence schema for ephemeral per-user data.\n * When provided, enables the presence API on the ClientDocument.\n */\n readonly presence?: TPresence;\n /** Initial presence data, that will be set on the ClientDocument when it is created */\n readonly initialPresence?: TPresence extends Presence.AnyPresence ? Presence.Infer<TPresence> : undefined;\n}\n\n/**\n * Listener callbacks for subscribing to ClientDocument events.\n */\nexport interface ClientDocumentListener<TSchema extends Primitive.AnyPrimitive> {\n /** Called when optimistic state changes */\n readonly onStateChange?: (state: Primitive.InferState<TSchema> | undefined) => void;\n /** Called when connection status changes */\n readonly onConnectionChange?: (connected: boolean) => void;\n /** Called when client is fully initialized and ready */\n readonly onReady?: () => void;\n}\n\n/**\n * A ClientDocument provides optimistic updates with server synchronization.\n */\nexport interface ClientDocument<\n TSchema extends Primitive.AnyPrimitive,\n TPresence extends Presence.AnyPresence | undefined = undefined\n> {\n /** The schema defining this document's structure */\n readonly schema: TSchema;\n\n /** Root proxy for accessing and modifying document data (optimistic) */\n readonly root: Primitive.InferProxy<TSchema>;\n\n /** Returns the current optimistic state (server + pending) */\n get(): Primitive.InferState<TSchema> | undefined;\n\n /** Returns the confirmed server state */\n getServerState(): Primitive.InferState<TSchema> | undefined;\n\n /** Returns the current server version */\n getServerVersion(): number;\n\n /** Returns pending transactions count */\n getPendingCount(): number;\n\n /** Returns whether there are pending transactions */\n hasPendingChanges(): boolean;\n\n /**\n * Runs a function within a transaction.\n * Changes are applied optimistically and sent to the server.\n */\n transaction<R>(fn: (root: Primitive.InferProxy<TSchema>) => R): R;\n\n /**\n * Connects to the server and starts syncing.\n */\n connect(): Promise<void>;\n\n /**\n * Disconnects from the server.\n */\n disconnect(): void;\n\n /**\n * Returns whether currently connected to the server.\n */\n isConnected(): boolean;\n\n /**\n * Forces a full resync from the server.\n */\n resync(): void;\n\n /**\n * Returns whether the client is fully initialized and ready.\n */\n isReady(): boolean;\n\n /**\n * Subscribes to document events (state changes, connection changes, ready).\n * @returns Unsubscribe function\n */\n subscribe(listener: ClientDocumentListener<TSchema>): () => void;\n\n /**\n * Presence API for ephemeral per-user data.\n * Only available when presence schema is provided in options.\n */\n readonly presence: TPresence extends Presence.AnyPresence\n ? ClientPresence<Presence.Infer<TPresence>>\n : undefined;\n}\n\n// =============================================================================\n// Client Document Implementation\n// =============================================================================\n\n/**\n * Creates a new ClientDocument for the given schema.\n */\nexport const make = <\n TSchema extends Primitive.AnyPrimitive,\n TPresence extends Presence.AnyPresence | undefined = undefined\n>(\n options: ClientDocumentOptions<TSchema, TPresence>\n): ClientDocument<TSchema, TPresence> => {\n const {\n schema,\n transport,\n initialState,\n initialVersion = 0,\n onRejection,\n onStateChange,\n onConnectionChange,\n onReady,\n transactionTimeout = 30000,\n initTimeout = 10000,\n debug = false,\n presence: presenceSchema,\n initialPresence,\n } = options;\n\n // ==========================================================================\n // Internal State\n // ==========================================================================\n\n // Server-confirmed state\n let _serverState: Primitive.InferState<TSchema> | undefined = initialState;\n let _serverVersion = initialVersion;\n\n // Pending transactions queue\n let _pending: PendingTransaction[] = [];\n\n // Server transactions received (for rebase after rejection)\n let _serverTransactionHistory: Transaction.Transaction[] = [];\n const MAX_HISTORY_SIZE = 100;\n\n // The underlying document for optimistic state\n let _optimisticDoc = Document.make(schema, { initial: _serverState });\n\n // Subscription cleanup\n let _unsubscribe: (() => void) | null = null;\n\n // Timeout handles for pending transactions\n const _timeoutHandles = new Map<string, ReturnType<typeof setTimeout>>();\n\n // Initialization state - handles buffering during startup\n let _initState: InitState = initialState !== undefined\n ? { type: \"ready\" }\n : { type: \"uninitialized\" };\n\n // Init timeout handle\n let _initTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n\n // Promise resolver for connect() to wait for ready state\n let _initResolver: (() => void) | null = null;\n let _initRejecter: ((error: Error) => void) | null = null;\n\n // Subscribers for events (added after creation via subscribe())\n const _subscribers = new Set<ClientDocumentListener<TSchema>>();\n\n // ==========================================================================\n // Presence State (only used when presenceSchema is provided)\n // ==========================================================================\n\n // This client's connection ID (received from presence_snapshot)\n let _presenceSelfId: string | undefined = undefined;\n\n // This client's current presence data\n let _presenceSelfData: unknown = undefined;\n\n // Other clients' presence entries (connectionId -> entry)\n const _presenceOthers = new Map<string, Presence.PresenceEntry<unknown>>();\n\n // Presence change subscribers\n const _presenceSubscribers = new Set<PresenceListener<unknown>>();\n\n // ==========================================================================\n // Debug Logging\n // ==========================================================================\n\n /**\n * Debug logging helper that only logs when debug is enabled.\n */\n const debugLog = (...args: unknown[]): void => {\n if (debug) {\n console.log(\"[ClientDocument]\", ...args);\n }\n };\n\n // ==========================================================================\n // Notification Helpers\n // ==========================================================================\n\n /**\n * Notifies all listeners of a state change.\n */\n const notifyStateChange = (state: Primitive.InferState<TSchema> | undefined): void => {\n debugLog(\"notifyStateChange\", {\n state,\n subscriberCount: _subscribers.size,\n hasOnStateChange: !!onStateChange,\n });\n onStateChange?.(state);\n for (const listener of _subscribers) {\n listener.onStateChange?.(state);\n }\n };\n\n /**\n * Notifies all listeners of a connection change.\n */\n const notifyConnectionChange = (connected: boolean): void => {\n debugLog(\"notifyConnectionChange\", {\n connected,\n subscriberCount: _subscribers.size,\n hasOnConnectionChange: !!onConnectionChange,\n });\n onConnectionChange?.(connected);\n for (const listener of _subscribers) {\n listener.onConnectionChange?.(connected);\n }\n };\n\n /**\n * Notifies all listeners when ready.\n */\n const notifyReady = (): void => {\n debugLog(\"notifyReady\", {\n subscriberCount: _subscribers.size,\n hasOnReady: !!onReady,\n });\n onReady?.();\n for (const listener of _subscribers) {\n listener.onReady?.();\n }\n };\n\n /**\n * Notifies all presence listeners of a change.\n */\n const notifyPresenceChange = (): void => {\n debugLog(\"notifyPresenceChange\", {\n subscriberCount: _presenceSubscribers.size,\n });\n for (const listener of _presenceSubscribers) {\n try {\n listener.onPresenceChange?.();\n } catch {\n // Ignore listener errors\n }\n }\n };\n\n // ==========================================================================\n // Presence Handlers\n // ==========================================================================\n\n /**\n * Handles incoming presence snapshot from server.\n */\n const handlePresenceSnapshot = (message: Transport.PresenceSnapshotMessage): void => {\n if (!presenceSchema) return;\n\n debugLog(\"handlePresenceSnapshot\", {\n selfId: message.selfId,\n presenceCount: Object.keys(message.presences).length,\n });\n\n _presenceSelfId = message.selfId;\n _presenceOthers.clear();\n\n // Populate others from snapshot (exclude self)\n for (const [id, entry] of Object.entries(message.presences)) {\n if (id !== message.selfId) {\n _presenceOthers.set(id, entry);\n }\n }\n\n notifyPresenceChange();\n };\n\n /**\n * Handles incoming presence update from server (another user).\n */\n const handlePresenceUpdate = (message: Transport.PresenceUpdateMessage): void => {\n if (!presenceSchema) return;\n\n debugLog(\"handlePresenceUpdate\", {\n id: message.id,\n userId: message.userId,\n });\n\n _presenceOthers.set(message.id, {\n data: message.data,\n userId: message.userId,\n });\n\n notifyPresenceChange();\n };\n\n /**\n * Handles incoming presence remove from server (user disconnected).\n */\n const handlePresenceRemove = (message: Transport.PresenceRemoveMessage): void => {\n if (!presenceSchema) return;\n\n debugLog(\"handlePresenceRemove\", {\n id: message.id,\n });\n\n _presenceOthers.delete(message.id);\n notifyPresenceChange();\n };\n\n /**\n * Clears all presence state (on disconnect).\n */\n const clearPresenceState = (): void => {\n _presenceSelfId = undefined;\n _presenceSelfData = undefined;\n _presenceOthers.clear();\n notifyPresenceChange();\n };\n\n // ==========================================================================\n // Helper Functions\n // ==========================================================================\n\n /**\n * Recomputes the optimistic document from server state + pending transactions.\n */\n const recomputeOptimisticState = (): void => {\n debugLog(\"recomputeOptimisticState\", {\n serverVersion: _serverVersion,\n pendingCount: _pending.length,\n serverState: _serverState,\n });\n\n // Create fresh document from server state\n _optimisticDoc = Document.make(schema, { initial: _serverState });\n\n // Apply all pending transactions\n for (const pending of _pending) {\n _optimisticDoc.apply(pending.transaction.ops);\n }\n\n const newState = _optimisticDoc.get();\n debugLog(\"recomputeOptimisticState: new optimistic state\", newState);\n\n // Notify state change\n notifyStateChange(newState);\n };\n\n /**\n * Adds a transaction to pending queue and sends to server.\n */\n const submitTransaction = (tx: Transaction.Transaction): void => {\n if (!transport.isConnected()) {\n throw new NotConnectedError();\n }\n\n debugLog(\"submitTransaction\", {\n txId: tx.id,\n ops: tx.ops,\n pendingCount: _pending.length + 1,\n });\n\n const pending: PendingTransaction = {\n transaction: tx,\n original: tx,\n sentAt: Date.now(),\n };\n\n _pending.push(pending);\n\n // Set timeout for this transaction\n const timeoutHandle = setTimeout(() => {\n handleTransactionTimeout(tx.id);\n }, transactionTimeout);\n _timeoutHandles.set(tx.id, timeoutHandle);\n\n // Send to server\n transport.send(tx);\n debugLog(\"submitTransaction: sent to server\", { txId: tx.id });\n };\n\n /**\n * Handles a transaction timeout.\n */\n const handleTransactionTimeout = (txId: string): void => {\n debugLog(\"handleTransactionTimeout\", { txId });\n const index = _pending.findIndex((p) => p.transaction.id === txId);\n if (index === -1) {\n debugLog(\"handleTransactionTimeout: transaction not found (already confirmed/rejected)\", { txId });\n return; // Already confirmed or rejected\n }\n\n // Remove from pending\n const [removed] = _pending.splice(index, 1);\n _timeoutHandles.delete(txId);\n\n debugLog(\"handleTransactionTimeout: removed from pending\", {\n txId,\n remainingPending: _pending.length,\n });\n\n // Recompute state\n recomputeOptimisticState();\n\n // Notify as rejection\n onRejection?.(removed!.transaction, \"Transaction timed out\");\n };\n\n /**\n * Handles an incoming server transaction.\n */\n const handleServerTransaction = (\n serverTx: Transaction.Transaction,\n version: number\n ): void => {\n debugLog(\"handleServerTransaction\", {\n txId: serverTx.id,\n version,\n ops: serverTx.ops,\n currentServerVersion: _serverVersion,\n pendingCount: _pending.length,\n });\n\n // Update server version\n _serverVersion = version;\n\n // Check if this is one of our pending transactions (ACK)\n const pendingIndex = _pending.findIndex(\n (p) => p.transaction.id === serverTx.id\n );\n\n if (pendingIndex !== -1) {\n // This is our transaction - confirmed!\n debugLog(\"handleServerTransaction: transaction confirmed (ACK)\", {\n txId: serverTx.id,\n pendingIndex,\n });\n\n const confirmed = _pending[pendingIndex]!;\n\n // Clear timeout\n const timeoutHandle = _timeoutHandles.get(serverTx.id);\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n _timeoutHandles.delete(serverTx.id);\n }\n\n // Remove from pending\n _pending.splice(pendingIndex, 1);\n\n // Apply to server state\n const tempDoc = Document.make(schema, { initial: _serverState });\n tempDoc.apply(serverTx.ops);\n _serverState = tempDoc.get();\n\n debugLog(\"handleServerTransaction: updated server state\", {\n txId: serverTx.id,\n newServerState: _serverState,\n remainingPending: _pending.length,\n });\n\n // Recompute optimistic state (pending txs already applied, just need to update base)\n recomputeOptimisticState();\n } else {\n // This is someone else's transaction - need to rebase\n debugLog(\"handleServerTransaction: remote transaction, rebasing pending\", {\n txId: serverTx.id,\n pendingCount: _pending.length,\n });\n\n // Apply to server state\n const tempDoc = Document.make(schema, { initial: _serverState });\n tempDoc.apply(serverTx.ops);\n _serverState = tempDoc.get();\n\n // Add to history for potential rebase after rejection\n _serverTransactionHistory.push(serverTx);\n if (_serverTransactionHistory.length > MAX_HISTORY_SIZE) {\n _serverTransactionHistory.shift();\n }\n\n // Rebase all pending transactions using primitive-based transformation\n const rebasedPending = _pending.map((p) => ({\n ...p,\n transaction: Rebase.transformTransactionWithPrimitive(p.transaction, serverTx, schema),\n }));\n\n debugLog(\"handleServerTransaction: rebased pending transactions\", {\n txId: serverTx.id,\n rebasedCount: rebasedPending.length,\n originalPendingIds: _pending.map((p) => p.transaction.id),\n rebasedPendingIds: rebasedPending.map((p) => p.transaction.id),\n });\n\n _pending = rebasedPending;\n\n // Recompute optimistic state\n recomputeOptimisticState();\n }\n };\n\n /**\n * Handles a transaction rejection from the server.\n */\n const handleRejection = (txId: string, reason: string): void => {\n debugLog(\"handleRejection\", {\n txId,\n reason,\n pendingCount: _pending.length,\n });\n\n const index = _pending.findIndex((p) => p.transaction.id === txId);\n if (index === -1) {\n debugLog(\"handleRejection: transaction not found (already removed)\", { txId });\n return; // Already removed\n }\n\n const rejected = _pending[index]!;\n\n // Clear timeout\n const timeoutHandle = _timeoutHandles.get(txId);\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n _timeoutHandles.delete(txId);\n }\n\n // Remove rejected transaction\n _pending.splice(index, 1);\n\n debugLog(\"handleRejection: removed rejected transaction, rebasing remaining\", {\n txId,\n remainingPending: _pending.length,\n serverHistorySize: _serverTransactionHistory.length,\n });\n\n // Re-transform remaining pending transactions without the rejected one\n // We need to replay from their original state\n const remainingOriginals = _pending.map((p) => p.original);\n const retransformed = Rebase.rebaseAfterRejectionWithPrimitive(\n [...remainingOriginals, rejected.original],\n txId,\n _serverTransactionHistory,\n schema\n );\n\n // Update pending with retransformed versions\n _pending = _pending.map((p, i) => ({\n ...p,\n transaction: retransformed[i] ?? p.transaction,\n }));\n\n debugLog(\"handleRejection: rebased remaining transactions\", {\n txId,\n rebasedCount: _pending.length,\n });\n\n // Recompute optimistic state\n recomputeOptimisticState();\n\n // Notify rejection\n onRejection?.(rejected.original, reason);\n };\n\n /**\n * Handles a snapshot from the server.\n * @param isInitialSnapshot - If true, this is the initial sync snapshot\n */\n const handleSnapshot = (state: unknown, version: number, isInitialSnapshot: boolean = false): void => {\n debugLog(\"handleSnapshot\", {\n isInitialSnapshot,\n version,\n currentServerVersion: _serverVersion,\n pendingCount: _pending.length,\n state,\n });\n\n if (!isInitialSnapshot) {\n debugLog(\"handleSnapshot: non-initial snapshot, clearing pending transactions\", {\n clearedPendingCount: _pending.length,\n });\n\n // For non-initial snapshots, clear all pending (they're now invalid)\n for (const handle of _timeoutHandles.values()) {\n clearTimeout(handle);\n }\n _timeoutHandles.clear();\n\n // Notify rejections for all pending\n for (const pending of _pending) {\n onRejection?.(pending.original, \"State reset due to resync\");\n }\n\n _pending = [];\n }\n\n _serverTransactionHistory = [];\n _serverState = state as Primitive.InferState<TSchema>;\n _serverVersion = version;\n\n debugLog(\"handleSnapshot: updated server state\", {\n newVersion: _serverVersion,\n newState: _serverState,\n });\n\n // Recompute optimistic state (now equals server state)\n recomputeOptimisticState();\n };\n\n /**\n * Processes buffered messages after receiving the initial snapshot.\n * Filters out transactions already included in the snapshot (version <= snapshotVersion)\n * and applies newer transactions in order.\n */\n const processBufferedMessages = (\n bufferedMessages: Transport.ServerMessage[],\n snapshotVersion: number\n ): void => {\n debugLog(\"processBufferedMessages\", {\n bufferedCount: bufferedMessages.length,\n snapshotVersion,\n });\n\n // Sort transactions by version to ensure correct order\n const sortedMessages = [...bufferedMessages].sort((a, b) => {\n if (a.type === \"transaction\" && b.type === \"transaction\") {\n return a.version - b.version;\n }\n return 0;\n });\n\n // Process each buffered message\n for (const message of sortedMessages) {\n switch (message.type) {\n case \"transaction\":\n // Only apply transactions with version > snapshot version\n if (message.version > snapshotVersion) {\n debugLog(\"processBufferedMessages: applying buffered transaction\", {\n txId: message.transaction.id,\n version: message.version,\n snapshotVersion,\n });\n handleServerTransaction(message.transaction, message.version);\n } else {\n debugLog(\"processBufferedMessages: skipping buffered transaction (already in snapshot)\", {\n txId: message.transaction.id,\n version: message.version,\n snapshotVersion,\n });\n }\n break;\n case \"error\":\n // Errors are still relevant - pass through\n debugLog(\"processBufferedMessages: processing buffered error\", {\n txId: message.transactionId,\n reason: message.reason,\n });\n handleRejection(message.transactionId, message.reason);\n break;\n // Ignore additional snapshots in buffer - we already have one\n }\n }\n };\n\n /**\n * Completes initialization and transitions to ready state.\n */\n const completeInitialization = (): void => {\n debugLog(\"completeInitialization\");\n\n // Clear init timeout\n if (_initTimeoutHandle !== null) {\n clearTimeout(_initTimeoutHandle);\n _initTimeoutHandle = null;\n }\n\n _initState = { type: \"ready\" };\n\n // Resolve the connect promise\n if (_initResolver) {\n _initResolver();\n _initResolver = null;\n _initRejecter = null;\n }\n\n debugLog(\"completeInitialization: ready\", {\n serverVersion: _serverVersion,\n serverState: _serverState,\n });\n\n // Notify ready\n notifyReady();\n };\n\n /**\n * Handles initialization timeout.\n */\n const handleInitTimeout = (): void => {\n debugLog(\"handleInitTimeout: initialization timed out\");\n _initTimeoutHandle = null;\n\n // Reject the connect promise\n if (_initRejecter) {\n const error = new Error(\"Initialization timed out waiting for snapshot\");\n _initRejecter(error);\n _initResolver = null;\n _initRejecter = null;\n }\n\n // Reset to uninitialized state\n _initState = { type: \"uninitialized\" };\n };\n\n /**\n * Handles incoming server messages.\n * During initialization, messages are buffered until the snapshot arrives.\n * Presence messages are always processed immediately (not buffered).\n */\n const handleServerMessage = (message: Transport.ServerMessage): void => {\n debugLog(\"handleServerMessage\", {\n messageType: message.type,\n initState: _initState.type,\n });\n\n // Presence messages are always handled immediately (not buffered)\n // This allows presence to work even during document initialization\n if (message.type === \"presence_snapshot\") {\n handlePresenceSnapshot(message);\n return;\n }\n if (message.type === \"presence_update\") {\n handlePresenceUpdate(message);\n return;\n }\n if (message.type === \"presence_remove\") {\n handlePresenceRemove(message);\n return;\n }\n\n // Handle based on initialization state\n if (_initState.type === \"initializing\") {\n if (message.type === \"snapshot\") {\n debugLog(\"handleServerMessage: received snapshot during initialization\", {\n version: message.version,\n bufferedCount: _initState.bufferedMessages.length,\n });\n // Snapshot received - apply it and process buffered messages\n const buffered = _initState.bufferedMessages;\n handleSnapshot(message.state, message.version, true);\n processBufferedMessages(buffered, message.version);\n completeInitialization();\n } else {\n debugLog(\"handleServerMessage: buffering message during initialization\", {\n messageType: message.type,\n bufferedCount: _initState.bufferedMessages.length + 1,\n });\n // Buffer other messages during initialization\n _initState.bufferedMessages.push(message);\n }\n return;\n }\n\n // Normal message handling when ready (or uninitialized with initial state)\n switch (message.type) {\n case \"transaction\":\n handleServerTransaction(message.transaction, message.version);\n break;\n case \"snapshot\":\n handleSnapshot(message.state, message.version, false);\n break;\n case \"error\":\n handleRejection(message.transactionId, message.reason);\n break;\n }\n };\n\n // ==========================================================================\n // Public API\n // ==========================================================================\n\n const clientDocument = {\n schema,\n\n get root() {\n return _optimisticDoc.root;\n },\n\n get: () => _optimisticDoc.get(),\n\n getServerState: () => _serverState,\n\n getServerVersion: () => _serverVersion,\n\n getPendingCount: () => _pending.length,\n\n hasPendingChanges: () => _pending.length > 0,\n\n transaction: <R,>(fn: (root: Primitive.InferProxy<TSchema>) => R): R => {\n debugLog(\"transaction: starting\", {\n isConnected: transport.isConnected(),\n isReady: _initState.type === \"ready\",\n pendingCount: _pending.length,\n });\n\n if (!transport.isConnected()) {\n throw new NotConnectedError();\n }\n\n if (_initState.type !== \"ready\") {\n throw new InvalidStateError(\"Client is not ready. Wait for initialization to complete.\");\n }\n\n // Run the transaction on the optimistic document\n const result = _optimisticDoc.transaction(fn);\n\n // Flush and get the transaction\n const tx = _optimisticDoc.flush();\n\n // If there are operations, submit to server\n if (!Transaction.isEmpty(tx)) {\n debugLog(\"transaction: flushed, submitting\", {\n txId: tx.id,\n opsCount: tx.ops.length,\n });\n submitTransaction(tx);\n } else {\n debugLog(\"transaction: flushed, empty transaction (no ops)\");\n }\n\n // Notify state change\n notifyStateChange(_optimisticDoc.get());\n\n return result;\n },\n\n connect: async (): Promise<void> => {\n debugLog(\"connect: starting\");\n // Subscribe to server messages\n _unsubscribe = transport.subscribe(handleServerMessage);\n\n // Connect transport\n await transport.connect();\n debugLog(\"connect: transport connected\");\n\n notifyConnectionChange(true);\n\n // Set initial presence if provided\n if (presenceSchema && initialPresence !== undefined) {\n debugLog(\"connect: setting initial presence\", { initialPresence });\n const validated = Presence.validate(presenceSchema, initialPresence);\n _presenceSelfData = validated;\n transport.sendPresenceSet(validated);\n notifyPresenceChange();\n }\n\n // If we already have initial state, we're ready immediately\n if (_initState.type === \"ready\") {\n debugLog(\"connect: already ready (has initial state)\");\n notifyReady();\n return;\n }\n\n // Enter initializing state - buffer messages until snapshot arrives\n _initState = { type: \"initializing\", bufferedMessages: [] };\n debugLog(\"connect: entering initializing state\", {\n initTimeout,\n });\n\n // Set up initialization timeout\n _initTimeoutHandle = setTimeout(handleInitTimeout, initTimeout);\n\n // Create a promise that resolves when we're ready\n const readyPromise = new Promise<void>((resolve, reject) => {\n _initResolver = resolve;\n _initRejecter = reject;\n });\n\n // Request initial snapshot\n debugLog(\"connect: requesting initial snapshot\");\n \n transport.requestSnapshot();\n\n\n // Wait for initialization to complete\n await readyPromise;\n debugLog(\"connect: completed\");\n },\n\n disconnect: (): void => {\n debugLog(\"disconnect: starting\", {\n pendingCount: _pending.length,\n initState: _initState.type,\n });\n\n // Clear all timeouts\n for (const handle of _timeoutHandles.values()) {\n clearTimeout(handle);\n }\n _timeoutHandles.clear();\n\n // Clear init timeout\n if (_initTimeoutHandle !== null) {\n clearTimeout(_initTimeoutHandle);\n _initTimeoutHandle = null;\n }\n\n // Reject any pending init promise\n if (_initRejecter) {\n _initRejecter(new Error(\"Disconnected during initialization\"));\n _initResolver = null;\n _initRejecter = null;\n }\n\n // Reset init state\n if (_initState.type === \"initializing\") {\n _initState = { type: \"uninitialized\" };\n }\n\n // Clear presence state\n clearPresenceState();\n\n // Unsubscribe\n if (_unsubscribe) {\n _unsubscribe();\n _unsubscribe = null;\n }\n\n // Disconnect transport\n transport.disconnect();\n\n notifyConnectionChange(false);\n debugLog(\"disconnect: completed\");\n },\n\n isConnected: () => transport.isConnected(),\n\n isReady: () => _initState.type === \"ready\",\n\n resync: (): void => {\n debugLog(\"resync: requesting snapshot\", {\n currentVersion: _serverVersion,\n pendingCount: _pending.length,\n });\n if (!transport.isConnected()) {\n throw new NotConnectedError();\n }\n transport.requestSnapshot();\n },\n\n subscribe: (listener: ClientDocumentListener<TSchema>): (() => void) => {\n _subscribers.add(listener);\n return () => {\n _subscribers.delete(listener);\n };\n },\n\n // =========================================================================\n // Presence API\n // =========================================================================\n\n presence: (presenceSchema\n ? {\n selfId: () => _presenceSelfId,\n\n self: () => _presenceSelfData as Presence.Infer<NonNullable<TPresence>> | undefined,\n\n others: () => _presenceOthers as ReadonlyMap<string, Presence.PresenceEntry<Presence.Infer<NonNullable<TPresence>>>>,\n\n all: () => {\n const all = new Map<string, Presence.PresenceEntry<unknown>>();\n // Add others\n for (const [id, entry] of _presenceOthers) {\n all.set(id, entry);\n }\n // Add self if we have data\n if (_presenceSelfId !== undefined && _presenceSelfData !== undefined) {\n all.set(_presenceSelfId, { data: _presenceSelfData });\n }\n return all as ReadonlyMap<string, Presence.PresenceEntry<Presence.Infer<NonNullable<TPresence>>>>;\n },\n\n set: (data: Presence.Infer<NonNullable<TPresence>>) => {\n if (!presenceSchema) return;\n\n // Validate against schema (throws if invalid)\n const validated = Presence.validate(presenceSchema, data);\n\n debugLog(\"presence.set\", { data: validated });\n\n // Update local state\n _presenceSelfData = validated;\n\n // Send to server\n transport.sendPresenceSet(validated);\n\n // Notify listeners\n notifyPresenceChange();\n },\n\n clear: () => {\n if (!presenceSchema) return;\n\n debugLog(\"presence.clear\");\n\n // Clear local state\n _presenceSelfData = undefined;\n\n // Send to server\n transport.sendPresenceClear();\n\n // Notify listeners\n notifyPresenceChange();\n },\n\n subscribe: (listener: PresenceListener<Presence.Infer<NonNullable<TPresence>>>) => {\n _presenceSubscribers.add(listener as PresenceListener<unknown>);\n return () => {\n _presenceSubscribers.delete(listener as PresenceListener<unknown>);\n };\n },\n }\n : undefined) as TPresence extends Presence.AnyPresence\n ? ClientPresence<Presence.Infer<TPresence>>\n : undefined,\n } as ClientDocument<TSchema, TPresence>;\n\n return clientDocument;\n};\n"],"mappings":";;;;;;;;;;;;;AAsOA,MAAa,QAIX,YACuC;CACvC,MAAM,EACJ,QACA,WACA,cACA,iBAAiB,GACjB,aACA,eACA,oBACA,SACA,qBAAqB,KACrB,cAAc,KACd,QAAQ,OACR,UAAU,gBACV,oBACE;CAOJ,IAAIA,eAA0D;CAC9D,IAAI,iBAAiB;CAGrB,IAAIC,WAAiC,EAAE;CAGvC,IAAIC,4BAAuD,EAAE;CAC7D,MAAM,mBAAmB;CAGzB,IAAI,iBAAiBC,OAAc,QAAQ,EAAE,SAAS,cAAc,CAAC;CAGrE,IAAIC,eAAoC;CAGxC,MAAM,kCAAkB,IAAI,KAA4C;CAGxE,IAAIC,aAAwB,iBAAiB,SACzC,EAAE,MAAM,SAAS,GACjB,EAAE,MAAM,iBAAiB;CAG7B,IAAIC,qBAA2D;CAG/D,IAAIC,gBAAqC;CACzC,IAAIC,gBAAiD;CAGrD,MAAM,+BAAe,IAAI,KAAsC;CAO/D,IAAIC,kBAAsC;CAG1C,IAAIC,oBAA6B;CAGjC,MAAM,kCAAkB,IAAI,KAA8C;CAG1E,MAAM,uCAAuB,IAAI,KAAgC;;;;CASjE,MAAM,YAAY,GAAG,SAA0B;AAC7C,MAAI,MACF,SAAQ,IAAI,oBAAoB,GAAG,KAAK;;;;;CAW5C,MAAM,qBAAqB,UAA2D;AACpF,WAAS,qBAAqB;GAC5B;GACA,iBAAiB,aAAa;GAC9B,kBAAkB,CAAC,CAAC;GACrB,CAAC;AACF,sEAAgB,MAAM;AACtB,OAAK,MAAM,YAAY,cAAc;;AACnC,qCAAS,oGAAgB,MAAM;;;;;;CAOnC,MAAM,0BAA0B,cAA6B;AAC3D,WAAS,0BAA0B;GACjC;GACA,iBAAiB,aAAa;GAC9B,uBAAuB,CAAC,CAAC;GAC1B,CAAC;AACF,qFAAqB,UAAU;AAC/B,OAAK,MAAM,YAAY,cAAc;;AACnC,qCAAS,yGAAqB,UAAU;;;;;;CAO5C,MAAM,oBAA0B;AAC9B,WAAS,eAAe;GACtB,iBAAiB,aAAa;GAC9B,YAAY,CAAC,CAAC;GACf,CAAC;AACF,qDAAW;AACX,OAAK,MAAM,YAAY,cAAc;;AACnC,iCAAS,qFAAW;;;;;;CAOxB,MAAM,6BAAmC;AACvC,WAAS,wBAAwB,EAC/B,iBAAiB,qBAAqB,MACvC,CAAC;AACF,OAAK,MAAM,YAAY,qBACrB,KAAI;;AACF,qCAAS,sGAAoB;oBACvB;;;;;CAaZ,MAAM,0BAA0B,YAAqD;AACnF,MAAI,CAAC,eAAgB;AAErB,WAAS,0BAA0B;GACjC,QAAQ,QAAQ;GAChB,eAAe,OAAO,KAAK,QAAQ,UAAU,CAAC;GAC/C,CAAC;AAEF,oBAAkB,QAAQ;AAC1B,kBAAgB,OAAO;AAGvB,OAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,QAAQ,UAAU,CACzD,KAAI,OAAO,QAAQ,OACjB,iBAAgB,IAAI,IAAI,MAAM;AAIlC,wBAAsB;;;;;CAMxB,MAAM,wBAAwB,YAAmD;AAC/E,MAAI,CAAC,eAAgB;AAErB,WAAS,wBAAwB;GAC/B,IAAI,QAAQ;GACZ,QAAQ,QAAQ;GACjB,CAAC;AAEF,kBAAgB,IAAI,QAAQ,IAAI;GAC9B,MAAM,QAAQ;GACd,QAAQ,QAAQ;GACjB,CAAC;AAEF,wBAAsB;;;;;CAMxB,MAAM,wBAAwB,YAAmD;AAC/E,MAAI,CAAC,eAAgB;AAErB,WAAS,wBAAwB,EAC/B,IAAI,QAAQ,IACb,CAAC;AAEF,kBAAgB,OAAO,QAAQ,GAAG;AAClC,wBAAsB;;;;;CAMxB,MAAM,2BAAiC;AACrC,oBAAkB;AAClB,sBAAoB;AACpB,kBAAgB,OAAO;AACvB,wBAAsB;;;;;CAUxB,MAAM,iCAAuC;AAC3C,WAAS,4BAA4B;GACnC,eAAe;GACf,cAAc,SAAS;GACvB,aAAa;GACd,CAAC;AAGF,mBAAiBP,OAAc,QAAQ,EAAE,SAAS,cAAc,CAAC;AAGjE,OAAK,MAAM,WAAW,SACpB,gBAAe,MAAM,QAAQ,YAAY,IAAI;EAG/C,MAAM,WAAW,eAAe,KAAK;AACrC,WAAS,kDAAkD,SAAS;AAGpE,oBAAkB,SAAS;;;;;CAM7B,MAAM,qBAAqB,OAAsC;AAC/D,MAAI,CAAC,UAAU,aAAa,CAC1B,OAAM,IAAI,mBAAmB;AAG/B,WAAS,qBAAqB;GAC5B,MAAM,GAAG;GACT,KAAK,GAAG;GACR,cAAc,SAAS,SAAS;GACjC,CAAC;EAEF,MAAMQ,UAA8B;GAClC,aAAa;GACb,UAAU;GACV,QAAQ,KAAK,KAAK;GACnB;AAED,WAAS,KAAK,QAAQ;EAGtB,MAAM,gBAAgB,iBAAiB;AACrC,4BAAyB,GAAG,GAAG;KAC9B,mBAAmB;AACtB,kBAAgB,IAAI,GAAG,IAAI,cAAc;AAGzC,YAAU,KAAK,GAAG;AAClB,WAAS,qCAAqC,EAAE,MAAM,GAAG,IAAI,CAAC;;;;;CAMhE,MAAM,4BAA4B,SAAuB;AACvD,WAAS,4BAA4B,EAAE,MAAM,CAAC;EAC9C,MAAM,QAAQ,SAAS,WAAW,MAAM,EAAE,YAAY,OAAO,KAAK;AAClE,MAAI,UAAU,IAAI;AAChB,YAAS,gFAAgF,EAAE,MAAM,CAAC;AAClG;;EAIF,MAAM,CAAC,WAAW,SAAS,OAAO,OAAO,EAAE;AAC3C,kBAAgB,OAAO,KAAK;AAE5B,WAAS,kDAAkD;GACzD;GACA,kBAAkB,SAAS;GAC5B,CAAC;AAGF,4BAA0B;AAG1B,gEAAc,QAAS,aAAa,wBAAwB;;;;;CAM9D,MAAM,2BACJ,UACA,YACS;AACT,WAAS,2BAA2B;GAClC,MAAM,SAAS;GACf;GACA,KAAK,SAAS;GACd,sBAAsB;GACtB,cAAc,SAAS;GACxB,CAAC;AAGF,mBAAiB;EAGjB,MAAM,eAAe,SAAS,WAC3B,MAAM,EAAE,YAAY,OAAO,SAAS,GACtC;AAED,MAAI,iBAAiB,IAAI;AAEvB,YAAS,wDAAwD;IAC/D,MAAM,SAAS;IACf;IACD,CAAC;AAEgB,YAAS;GAG3B,MAAM,gBAAgB,gBAAgB,IAAI,SAAS,GAAG;AACtD,OAAI,eAAe;AACjB,iBAAa,cAAc;AAC3B,oBAAgB,OAAO,SAAS,GAAG;;AAIrC,YAAS,OAAO,cAAc,EAAE;GAGhC,MAAM,UAAUR,OAAc,QAAQ,EAAE,SAAS,cAAc,CAAC;AAChE,WAAQ,MAAM,SAAS,IAAI;AAC3B,kBAAe,QAAQ,KAAK;AAE5B,YAAS,iDAAiD;IACxD,MAAM,SAAS;IACf,gBAAgB;IAChB,kBAAkB,SAAS;IAC5B,CAAC;AAGF,6BAA0B;SACrB;AAEL,YAAS,iEAAiE;IACxE,MAAM,SAAS;IACf,cAAc,SAAS;IACxB,CAAC;GAGF,MAAM,UAAUA,OAAc,QAAQ,EAAE,SAAS,cAAc,CAAC;AAChE,WAAQ,MAAM,SAAS,IAAI;AAC3B,kBAAe,QAAQ,KAAK;AAG5B,6BAA0B,KAAK,SAAS;AACxC,OAAI,0BAA0B,SAAS,iBACrC,2BAA0B,OAAO;GAInC,MAAM,iBAAiB,SAAS,KAAK,wCAChC,UACH,aAAaS,kCAAyC,EAAE,aAAa,UAAU,OAAO,IACrF;AAEH,YAAS,yDAAyD;IAChE,MAAM,SAAS;IACf,cAAc,eAAe;IAC7B,oBAAoB,SAAS,KAAK,MAAM,EAAE,YAAY,GAAG;IACzD,mBAAmB,eAAe,KAAK,MAAM,EAAE,YAAY,GAAG;IAC/D,CAAC;AAEF,cAAW;AAGX,6BAA0B;;;;;;CAO9B,MAAM,mBAAmB,MAAc,WAAyB;AAC9D,WAAS,mBAAmB;GAC1B;GACA;GACA,cAAc,SAAS;GACxB,CAAC;EAEF,MAAM,QAAQ,SAAS,WAAW,MAAM,EAAE,YAAY,OAAO,KAAK;AAClE,MAAI,UAAU,IAAI;AAChB,YAAS,4DAA4D,EAAE,MAAM,CAAC;AAC9E;;EAGF,MAAM,WAAW,SAAS;EAG1B,MAAM,gBAAgB,gBAAgB,IAAI,KAAK;AAC/C,MAAI,eAAe;AACjB,gBAAa,cAAc;AAC3B,mBAAgB,OAAO,KAAK;;AAI9B,WAAS,OAAO,OAAO,EAAE;AAEzB,WAAS,qEAAqE;GAC5E;GACA,kBAAkB,SAAS;GAC3B,mBAAmB,0BAA0B;GAC9C,CAAC;EAIF,MAAM,qBAAqB,SAAS,KAAK,MAAM,EAAE,SAAS;EAC1D,MAAM,gBAAgBC,kCACpB,CAAC,GAAG,oBAAoB,SAAS,SAAS,EAC1C,MACA,2BACA,OACD;AAGD,aAAW,SAAS,KAAK,GAAG,MAAM;;4CAC7B,UACH,iCAAa,cAAc,iEAAM,EAAE;IAClC;AAEH,WAAS,mDAAmD;GAC1D;GACA,cAAc,SAAS;GACxB,CAAC;AAGF,4BAA0B;AAG1B,gEAAc,SAAS,UAAU,OAAO;;;;;;CAO1C,MAAM,kBAAkB,OAAgB,SAAiB,oBAA6B,UAAgB;AACpG,WAAS,kBAAkB;GACzB;GACA;GACA,sBAAsB;GACtB,cAAc,SAAS;GACvB;GACD,CAAC;AAEF,MAAI,CAAC,mBAAmB;AACtB,YAAS,uEAAuE,EAC9E,qBAAqB,SAAS,QAC/B,CAAC;AAGF,QAAK,MAAM,UAAU,gBAAgB,QAAQ,CAC3C,cAAa,OAAO;AAEtB,mBAAgB,OAAO;AAGvB,QAAK,MAAM,WAAW,SACpB,+DAAc,QAAQ,UAAU,4BAA4B;AAG9D,cAAW,EAAE;;AAGf,8BAA4B,EAAE;AAC9B,iBAAe;AACf,mBAAiB;AAEjB,WAAS,wCAAwC;GAC/C,YAAY;GACZ,UAAU;GACX,CAAC;AAGF,4BAA0B;;;;;;;CAQ5B,MAAM,2BACJ,kBACA,oBACS;AACT,WAAS,2BAA2B;GAClC,eAAe,iBAAiB;GAChC;GACD,CAAC;EAGF,MAAM,iBAAiB,CAAC,GAAG,iBAAiB,CAAC,MAAM,GAAG,MAAM;AAC1D,OAAI,EAAE,SAAS,iBAAiB,EAAE,SAAS,cACzC,QAAO,EAAE,UAAU,EAAE;AAEvB,UAAO;IACP;AAGF,OAAK,MAAM,WAAW,eACpB,SAAQ,QAAQ,MAAhB;GACE,KAAK;AAEH,QAAI,QAAQ,UAAU,iBAAiB;AACrC,cAAS,0DAA0D;MACjE,MAAM,QAAQ,YAAY;MAC1B,SAAS,QAAQ;MACjB;MACD,CAAC;AACF,6BAAwB,QAAQ,aAAa,QAAQ,QAAQ;UAE7D,UAAS,gFAAgF;KACvF,MAAM,QAAQ,YAAY;KAC1B,SAAS,QAAQ;KACjB;KACD,CAAC;AAEJ;GACF,KAAK;AAEH,aAAS,sDAAsD;KAC7D,MAAM,QAAQ;KACd,QAAQ,QAAQ;KACjB,CAAC;AACF,oBAAgB,QAAQ,eAAe,QAAQ,OAAO;AACtD;;;;;;CASR,MAAM,+BAAqC;AACzC,WAAS,yBAAyB;AAGlC,MAAI,uBAAuB,MAAM;AAC/B,gBAAa,mBAAmB;AAChC,wBAAqB;;AAGvB,eAAa,EAAE,MAAM,SAAS;AAG9B,MAAI,eAAe;AACjB,kBAAe;AACf,mBAAgB;AAChB,mBAAgB;;AAGlB,WAAS,iCAAiC;GACxC,eAAe;GACf,aAAa;GACd,CAAC;AAGF,eAAa;;;;;CAMf,MAAM,0BAAgC;AACpC,WAAS,8CAA8C;AACvD,uBAAqB;AAGrB,MAAI,eAAe;AAEjB,iCADc,IAAI,MAAM,gDAAgD,CACpD;AACpB,mBAAgB;AAChB,mBAAgB;;AAIlB,eAAa,EAAE,MAAM,iBAAiB;;;;;;;CAQxC,MAAM,uBAAuB,YAA2C;AACtE,WAAS,uBAAuB;GAC9B,aAAa,QAAQ;GACrB,WAAW,WAAW;GACvB,CAAC;AAIF,MAAI,QAAQ,SAAS,qBAAqB;AACxC,0BAAuB,QAAQ;AAC/B;;AAEF,MAAI,QAAQ,SAAS,mBAAmB;AACtC,wBAAqB,QAAQ;AAC7B;;AAEF,MAAI,QAAQ,SAAS,mBAAmB;AACtC,wBAAqB,QAAQ;AAC7B;;AAIF,MAAI,WAAW,SAAS,gBAAgB;AACtC,OAAI,QAAQ,SAAS,YAAY;AAC/B,aAAS,gEAAgE;KACvE,SAAS,QAAQ;KACjB,eAAe,WAAW,iBAAiB;KAC5C,CAAC;IAEF,MAAM,WAAW,WAAW;AAC5B,mBAAe,QAAQ,OAAO,QAAQ,SAAS,KAAK;AACpD,4BAAwB,UAAU,QAAQ,QAAQ;AAClD,4BAAwB;UACnB;AACL,aAAS,gEAAgE;KACvE,aAAa,QAAQ;KACrB,eAAe,WAAW,iBAAiB,SAAS;KACrD,CAAC;AAEF,eAAW,iBAAiB,KAAK,QAAQ;;AAE3C;;AAIF,UAAQ,QAAQ,MAAhB;GACE,KAAK;AACH,4BAAwB,QAAQ,aAAa,QAAQ,QAAQ;AAC7D;GACF,KAAK;AACH,mBAAe,QAAQ,OAAO,QAAQ,SAAS,MAAM;AACrD;GACF,KAAK;AACH,oBAAgB,QAAQ,eAAe,QAAQ,OAAO;AACtD;;;AA8PN,QAtPuB;EACrB;EAEA,IAAI,OAAO;AACT,UAAO,eAAe;;EAGxB,WAAW,eAAe,KAAK;EAE/B,sBAAsB;EAEtB,wBAAwB;EAExB,uBAAuB,SAAS;EAEhC,yBAAyB,SAAS,SAAS;EAE3C,cAAkB,OAAsD;AACtE,YAAS,yBAAyB;IAChC,aAAa,UAAU,aAAa;IACpC,SAAS,WAAW,SAAS;IAC7B,cAAc,SAAS;IACxB,CAAC;AAEF,OAAI,CAAC,UAAU,aAAa,CAC1B,OAAM,IAAI,mBAAmB;AAG/B,OAAI,WAAW,SAAS,QACtB,OAAM,IAAI,kBAAkB,4DAA4D;GAI1F,MAAM,SAAS,eAAe,YAAY,GAAG;GAG7C,MAAM,KAAK,eAAe,OAAO;AAGjC,OAAI,CAACC,QAAoB,GAAG,EAAE;AAC5B,aAAS,oCAAoC;KAC3C,MAAM,GAAG;KACT,UAAU,GAAG,IAAI;KAClB,CAAC;AACF,sBAAkB,GAAG;SAErB,UAAS,mDAAmD;AAI9D,qBAAkB,eAAe,KAAK,CAAC;AAEvC,UAAO;;EAGT,SAAS,YAA2B;AAClC,YAAS,oBAAoB;AAE7B,kBAAe,UAAU,UAAU,oBAAoB;AAGvD,SAAM,UAAU,SAAS;AACzB,YAAS,+BAA+B;AAExC,0BAAuB,KAAK;AAG5B,OAAI,kBAAkB,oBAAoB,QAAW;AACnD,aAAS,qCAAqC,EAAE,iBAAiB,CAAC;IAClE,MAAM,YAAYC,SAAkB,gBAAgB,gBAAgB;AACpE,wBAAoB;AACpB,cAAU,gBAAgB,UAAU;AACpC,0BAAsB;;AAIxB,OAAI,WAAW,SAAS,SAAS;AAC/B,aAAS,6CAA6C;AACtD,iBAAa;AACb;;AAIF,gBAAa;IAAE,MAAM;IAAgB,kBAAkB,EAAE;IAAE;AAC3D,YAAS,wCAAwC,EAC/C,aACD,CAAC;AAGF,wBAAqB,WAAW,mBAAmB,YAAY;GAG/D,MAAM,eAAe,IAAI,SAAe,SAAS,WAAW;AAC1D,oBAAgB;AAChB,oBAAgB;KAChB;AAGF,YAAS,uCAAuC;AAEhD,aAAU,iBAAiB;AAI3B,SAAM;AACN,YAAS,qBAAqB;;EAGhC,kBAAwB;AACtB,YAAS,wBAAwB;IAC/B,cAAc,SAAS;IACvB,WAAW,WAAW;IACvB,CAAC;AAGF,QAAK,MAAM,UAAU,gBAAgB,QAAQ,CAC3C,cAAa,OAAO;AAEtB,mBAAgB,OAAO;AAGvB,OAAI,uBAAuB,MAAM;AAC/B,iBAAa,mBAAmB;AAChC,yBAAqB;;AAIvB,OAAI,eAAe;AACjB,kCAAc,IAAI,MAAM,qCAAqC,CAAC;AAC9D,oBAAgB;AAChB,oBAAgB;;AAIlB,OAAI,WAAW,SAAS,eACtB,cAAa,EAAE,MAAM,iBAAiB;AAIxC,uBAAoB;AAGpB,OAAI,cAAc;AAChB,kBAAc;AACd,mBAAe;;AAIjB,aAAU,YAAY;AAEtB,0BAAuB,MAAM;AAC7B,YAAS,wBAAwB;;EAGnC,mBAAmB,UAAU,aAAa;EAE1C,eAAe,WAAW,SAAS;EAEnC,cAAoB;AAClB,YAAS,+BAA+B;IACtC,gBAAgB;IAChB,cAAc,SAAS;IACxB,CAAC;AACF,OAAI,CAAC,UAAU,aAAa,CAC1B,OAAM,IAAI,mBAAmB;AAE/B,aAAU,iBAAiB;;EAG7B,YAAY,aAA4D;AACtE,gBAAa,IAAI,SAAS;AAC1B,gBAAa;AACX,iBAAa,OAAO,SAAS;;;EAQjC,UAAW,iBACP;GACE,cAAc;GAEd,YAAY;GAEZ,cAAc;GAEd,WAAW;IACT,MAAM,sBAAM,IAAI,KAA8C;AAE9D,SAAK,MAAM,CAAC,IAAI,UAAU,gBACxB,KAAI,IAAI,IAAI,MAAM;AAGpB,QAAI,oBAAoB,UAAa,sBAAsB,OACzD,KAAI,IAAI,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,WAAO;;GAGT,MAAM,SAAiD;AACrD,QAAI,CAAC,eAAgB;IAGrB,MAAM,YAAYA,SAAkB,gBAAgB,KAAK;AAEzD,aAAS,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG7C,wBAAoB;AAGpB,cAAU,gBAAgB,UAAU;AAGpC,0BAAsB;;GAGxB,aAAa;AACX,QAAI,CAAC,eAAgB;AAErB,aAAS,iBAAiB;AAG1B,wBAAoB;AAGpB,cAAU,mBAAmB;AAG7B,0BAAsB;;GAGxB,YAAY,aAAuE;AACjF,yBAAqB,IAAI,SAAsC;AAC/D,iBAAa;AACX,0BAAqB,OAAO,SAAsC;;;GAGvE,GACD;EAGL"}
|
|
1
|
+
{"version":3,"file":"ClientDocument.mjs","names":["_serverState: Primitive.InferState<TSchema> | undefined","_pending: PendingTransaction[]","_serverTransactionHistory: Transaction.Transaction[]","Document.make","_unsubscribe: (() => void) | null","_initState: InitState","_initTimeoutHandle: ReturnType<typeof setTimeout> | null","_initResolver: (() => void) | null","_initRejecter: ((error: Error) => void) | null","_presenceSelfId: string | undefined","_presenceSelfData: unknown","pending: PendingTransaction","Rebase.transformTransactionWithPrimitive","Rebase.rebaseAfterRejectionWithPrimitive","Transaction.isEmpty","Presence.validate"],"sources":["../../src/client/ClientDocument.ts"],"sourcesContent":["import * as Document from \"../Document\";\nimport * as Transaction from \"../Transaction\";\nimport * as Presence from \"../Presence\";\nimport type * as Primitive from \"../Primitive\";\nimport type * as Transport from \"./Transport\";\nimport * as Rebase from \"./Rebase\";\nimport {\n TransactionRejectedError,\n NotConnectedError,\n InvalidStateError,\n} from \"./errors\";\n\n// =============================================================================\n// Client Document Types\n// =============================================================================\n\n/**\n * Pending transaction with metadata for tracking.\n */\ninterface PendingTransaction {\n /** The transaction */\n readonly transaction: Transaction.Transaction;\n /** Original transaction before any rebasing */\n readonly original: Transaction.Transaction;\n /** Timestamp when the transaction was sent */\n readonly sentAt: number;\n}\n\n/**\n * Initialization state for the client document.\n * Handles the race condition during startup where transactions\n * may arrive while fetching the initial snapshot.\n */\ntype InitState =\n | { readonly type: \"uninitialized\" }\n | { readonly type: \"initializing\"; readonly bufferedMessages: Transport.ServerMessage[] }\n | { readonly type: \"ready\" };\n\n// =============================================================================\n// Presence Types\n// =============================================================================\n\n/**\n * Listener for presence changes.\n */\nexport interface PresenceListener<_TData> {\n /** Called when any presence changes (self or others) */\n readonly onPresenceChange?: () => void;\n}\n\n/**\n * Presence API exposed on the ClientDocument.\n */\nexport interface ClientPresence<TData> {\n /**\n * Returns this client's connection ID (set after receiving presence_snapshot).\n * Returns undefined before the snapshot is received.\n */\n readonly selfId: () => string | undefined;\n\n /**\n * Returns this client's current presence data.\n * Returns undefined if not set.\n */\n readonly self: () => TData | undefined;\n\n /**\n * Returns a map of other clients' presence data.\n * Keys are connection IDs.\n */\n readonly others: () => ReadonlyMap<string, Presence.PresenceEntry<TData>>;\n\n /**\n * Returns all presence entries including self.\n */\n readonly all: () => ReadonlyMap<string, Presence.PresenceEntry<TData>>;\n\n /**\n * Sets this client's presence data.\n * Validates against the presence schema before sending.\n * @throws ParseError if validation fails\n */\n readonly set: (data: TData) => void;\n\n /**\n * Clears this client's presence data.\n */\n readonly clear: () => void;\n\n /**\n * Subscribes to presence changes.\n * @returns Unsubscribe function\n */\n readonly subscribe: (listener: PresenceListener<TData>) => () => void;\n}\n\n/**\n * Options for creating a ClientDocument.\n */\nexport interface ClientDocumentOptions<\n TSchema extends Primitive.AnyPrimitive,\n TPresence extends Presence.AnyPresence | undefined = undefined\n> {\n /** The schema defining the document structure */\n readonly schema: TSchema;\n /** Transport for server communication */\n readonly transport: Transport.Transport;\n /** Initial state (optional, will sync from server if not provided) */\n readonly initialState?: Primitive.InferState<TSchema>;\n /** Initial server version (optional) */\n readonly initialVersion?: number;\n /** Called when server rejects a transaction */\n readonly onRejection?: (\n transaction: Transaction.Transaction,\n reason: string\n ) => void;\n /** Called when optimistic state changes */\n readonly onStateChange?: (state: Primitive.InferState<TSchema> | undefined) => void;\n /** Called when connection status changes */\n readonly onConnectionChange?: (connected: boolean) => void;\n /** Called when client is fully initialized and ready */\n readonly onReady?: () => void;\n /** Timeout in ms for pending transactions (default: 30000) */\n readonly transactionTimeout?: number;\n /** Timeout in ms for initialization (default: 10000) */\n readonly initTimeout?: number;\n /** Enable debug logging for all activity (default: false) */\n readonly debug?: boolean;\n /**\n * Optional presence schema for ephemeral per-user data.\n * When provided, enables the presence API on the ClientDocument.\n */\n readonly presence?: TPresence;\n /** Initial presence data, that will be set on the ClientDocument when it is created */\n readonly initialPresence?: TPresence extends Presence.AnyPresence ? Presence.Infer<TPresence> : undefined;\n}\n\n/**\n * Listener callbacks for subscribing to ClientDocument events.\n */\nexport interface ClientDocumentListener<TSchema extends Primitive.AnyPrimitive> {\n /** Called when optimistic state changes */\n readonly onStateChange?: (state: Primitive.InferState<TSchema> | undefined) => void;\n /** Called when connection status changes */\n readonly onConnectionChange?: (connected: boolean) => void;\n /** Called when client is fully initialized and ready */\n readonly onReady?: () => void;\n}\n\n/**\n * A ClientDocument provides optimistic updates with server synchronization.\n */\nexport interface ClientDocument<\n TSchema extends Primitive.AnyPrimitive,\n TPresence extends Presence.AnyPresence | undefined = undefined\n> {\n /** The schema defining this document's structure */\n readonly schema: TSchema;\n\n /** Root proxy for accessing and modifying document data (optimistic) */\n readonly root: Primitive.InferProxy<TSchema>;\n\n /** Returns the current optimistic state (server + pending) */\n get(): Primitive.InferState<TSchema> | undefined;\n\n /** Returns the confirmed server state */\n getServerState(): Primitive.InferState<TSchema> | undefined;\n\n /** Returns the current server version */\n getServerVersion(): number;\n\n /** Returns pending transactions count */\n getPendingCount(): number;\n\n /** Returns whether there are pending transactions */\n hasPendingChanges(): boolean;\n\n /**\n * Runs a function within a transaction.\n * Changes are applied optimistically and sent to the server.\n */\n transaction<R>(fn: (root: Primitive.InferProxy<TSchema>) => R): R;\n\n /**\n * Connects to the server and starts syncing.\n */\n connect(): Promise<void>;\n\n /**\n * Disconnects from the server.\n */\n disconnect(): void;\n\n /**\n * Returns whether currently connected to the server.\n */\n isConnected(): boolean;\n\n /**\n * Forces a full resync from the server.\n */\n resync(): void;\n\n /**\n * Returns whether the client is fully initialized and ready.\n */\n isReady(): boolean;\n\n /**\n * Subscribes to document events (state changes, connection changes, ready).\n * @returns Unsubscribe function\n */\n subscribe(listener: ClientDocumentListener<TSchema>): () => void;\n\n /**\n * Presence API for ephemeral per-user data.\n * Only available when presence schema is provided in options.\n */\n readonly presence: TPresence extends Presence.AnyPresence\n ? ClientPresence<Presence.Infer<TPresence>>\n : undefined;\n}\n\n// =============================================================================\n// Client Document Implementation\n// =============================================================================\n\n/**\n * Creates a new ClientDocument for the given schema.\n */\nexport const make = <\n TSchema extends Primitive.AnyPrimitive,\n TPresence extends Presence.AnyPresence | undefined = undefined\n>(\n options: ClientDocumentOptions<TSchema, TPresence>\n): ClientDocument<TSchema, TPresence> => {\n const {\n schema,\n transport,\n initialState,\n initialVersion = 0,\n onRejection,\n onStateChange,\n onConnectionChange,\n onReady,\n transactionTimeout = 30000,\n initTimeout = 10000,\n debug = false,\n presence: presenceSchema,\n initialPresence,\n } = options;\n\n // ==========================================================================\n // Internal State\n // ==========================================================================\n\n // Server-confirmed state\n let _serverState: Primitive.InferState<TSchema> | undefined = initialState;\n let _serverVersion = initialVersion;\n\n // Pending transactions queue\n let _pending: PendingTransaction[] = [];\n\n // Server transactions received (for rebase after rejection)\n let _serverTransactionHistory: Transaction.Transaction[] = [];\n const MAX_HISTORY_SIZE = 100;\n\n // The underlying document for optimistic state (use initialState for raw state format)\n let _optimisticDoc = Document.make(schema, { initialState: _serverState });\n\n // Subscription cleanup\n let _unsubscribe: (() => void) | null = null;\n\n // Timeout handles for pending transactions\n const _timeoutHandles = new Map<string, ReturnType<typeof setTimeout>>();\n\n // Initialization state - handles buffering during startup\n let _initState: InitState = initialState !== undefined\n ? { type: \"ready\" }\n : { type: \"uninitialized\" };\n\n // Init timeout handle\n let _initTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n\n // Promise resolver for connect() to wait for ready state\n let _initResolver: (() => void) | null = null;\n let _initRejecter: ((error: Error) => void) | null = null;\n\n // Subscribers for events (added after creation via subscribe())\n const _subscribers = new Set<ClientDocumentListener<TSchema>>();\n\n // ==========================================================================\n // Presence State (only used when presenceSchema is provided)\n // ==========================================================================\n\n // This client's connection ID (received from presence_snapshot)\n let _presenceSelfId: string | undefined = undefined;\n\n // This client's current presence data\n let _presenceSelfData: unknown = undefined;\n\n // Other clients' presence entries (connectionId -> entry)\n const _presenceOthers = new Map<string, Presence.PresenceEntry<unknown>>();\n\n // Presence change subscribers\n const _presenceSubscribers = new Set<PresenceListener<unknown>>();\n\n // ==========================================================================\n // Debug Logging\n // ==========================================================================\n\n /**\n * Debug logging helper that only logs when debug is enabled.\n */\n const debugLog = (...args: unknown[]): void => {\n if (debug) {\n console.log(\"[ClientDocument]\", ...args);\n }\n };\n\n // ==========================================================================\n // Notification Helpers\n // ==========================================================================\n\n /**\n * Notifies all listeners of a state change.\n */\n const notifyStateChange = (state: Primitive.InferState<TSchema> | undefined): void => {\n debugLog(\"notifyStateChange\", {\n state,\n subscriberCount: _subscribers.size,\n hasOnStateChange: !!onStateChange,\n });\n onStateChange?.(state);\n for (const listener of _subscribers) {\n listener.onStateChange?.(state);\n }\n };\n\n /**\n * Notifies all listeners of a connection change.\n */\n const notifyConnectionChange = (connected: boolean): void => {\n debugLog(\"notifyConnectionChange\", {\n connected,\n subscriberCount: _subscribers.size,\n hasOnConnectionChange: !!onConnectionChange,\n });\n onConnectionChange?.(connected);\n for (const listener of _subscribers) {\n listener.onConnectionChange?.(connected);\n }\n };\n\n /**\n * Notifies all listeners when ready.\n */\n const notifyReady = (): void => {\n debugLog(\"notifyReady\", {\n subscriberCount: _subscribers.size,\n hasOnReady: !!onReady,\n });\n onReady?.();\n for (const listener of _subscribers) {\n listener.onReady?.();\n }\n };\n\n /**\n * Notifies all presence listeners of a change.\n */\n const notifyPresenceChange = (): void => {\n debugLog(\"notifyPresenceChange\", {\n subscriberCount: _presenceSubscribers.size,\n });\n for (const listener of _presenceSubscribers) {\n try {\n listener.onPresenceChange?.();\n } catch {\n // Ignore listener errors\n }\n }\n };\n\n // ==========================================================================\n // Presence Handlers\n // ==========================================================================\n\n /**\n * Handles incoming presence snapshot from server.\n */\n const handlePresenceSnapshot = (message: Transport.PresenceSnapshotMessage): void => {\n if (!presenceSchema) return;\n\n debugLog(\"handlePresenceSnapshot\", {\n selfId: message.selfId,\n presenceCount: Object.keys(message.presences).length,\n });\n\n _presenceSelfId = message.selfId;\n _presenceOthers.clear();\n\n // Populate others from snapshot (exclude self)\n for (const [id, entry] of Object.entries(message.presences)) {\n if (id !== message.selfId) {\n _presenceOthers.set(id, entry);\n }\n }\n\n notifyPresenceChange();\n };\n\n /**\n * Handles incoming presence update from server (another user).\n */\n const handlePresenceUpdate = (message: Transport.PresenceUpdateMessage): void => {\n if (!presenceSchema) return;\n\n debugLog(\"handlePresenceUpdate\", {\n id: message.id,\n userId: message.userId,\n });\n\n _presenceOthers.set(message.id, {\n data: message.data,\n userId: message.userId,\n });\n\n notifyPresenceChange();\n };\n\n /**\n * Handles incoming presence remove from server (user disconnected).\n */\n const handlePresenceRemove = (message: Transport.PresenceRemoveMessage): void => {\n if (!presenceSchema) return;\n\n debugLog(\"handlePresenceRemove\", {\n id: message.id,\n });\n\n _presenceOthers.delete(message.id);\n notifyPresenceChange();\n };\n\n /**\n * Clears all presence state (on disconnect).\n */\n const clearPresenceState = (): void => {\n _presenceSelfId = undefined;\n _presenceSelfData = undefined;\n _presenceOthers.clear();\n notifyPresenceChange();\n };\n\n // ==========================================================================\n // Helper Functions\n // ==========================================================================\n\n /**\n * Recomputes the optimistic document from server state + pending transactions.\n */\n const recomputeOptimisticState = (): void => {\n debugLog(\"recomputeOptimisticState\", {\n serverVersion: _serverVersion,\n pendingCount: _pending.length,\n serverState: _serverState,\n });\n\n // Create fresh document from server state (use initialState for raw state format)\n _optimisticDoc = Document.make(schema, { initialState: _serverState });\n\n // Apply all pending transactions\n for (const pending of _pending) {\n _optimisticDoc.apply(pending.transaction.ops);\n }\n\n const newState = _optimisticDoc.get();\n debugLog(\"recomputeOptimisticState: new optimistic state\", newState);\n\n // Notify state change\n notifyStateChange(newState);\n };\n\n /**\n * Adds a transaction to pending queue and sends to server.\n * If disconnected, the transport will queue it for later submission.\n */\n const submitTransaction = (tx: Transaction.Transaction): void => {\n debugLog(\"submitTransaction\", {\n txId: tx.id,\n ops: tx.ops,\n pendingCount: _pending.length + 1,\n isConnected: transport.isConnected(),\n });\n\n const pending: PendingTransaction = {\n transaction: tx,\n original: tx,\n sentAt: Date.now(),\n };\n\n _pending.push(pending);\n\n // Only set timeout if connected - otherwise the transport queues it\n // and the timeout will start when the message is actually sent on reconnect\n if (transport.isConnected()) {\n const timeoutHandle = setTimeout(() => {\n handleTransactionTimeout(tx.id);\n }, transactionTimeout);\n _timeoutHandles.set(tx.id, timeoutHandle);\n }\n\n // Send to server (transport queues if disconnected)\n transport.send(tx);\n debugLog(\"submitTransaction: sent to transport\", { txId: tx.id, queued: !transport.isConnected() });\n };\n\n /**\n * Handles a transaction timeout.\n */\n const handleTransactionTimeout = (txId: string): void => {\n debugLog(\"handleTransactionTimeout\", { txId });\n const index = _pending.findIndex((p) => p.transaction.id === txId);\n if (index === -1) {\n debugLog(\"handleTransactionTimeout: transaction not found (already confirmed/rejected)\", { txId });\n return; // Already confirmed or rejected\n }\n\n // Remove from pending\n const [removed] = _pending.splice(index, 1);\n _timeoutHandles.delete(txId);\n\n debugLog(\"handleTransactionTimeout: removed from pending\", {\n txId,\n remainingPending: _pending.length,\n });\n\n // Recompute state\n recomputeOptimisticState();\n\n // Notify as rejection\n onRejection?.(removed!.transaction, \"Transaction timed out\");\n };\n\n /**\n * Handles an incoming server transaction.\n */\n const handleServerTransaction = (\n serverTx: Transaction.Transaction,\n version: number\n ): void => {\n debugLog(\"handleServerTransaction\", {\n txId: serverTx.id,\n version,\n ops: serverTx.ops,\n currentServerVersion: _serverVersion,\n pendingCount: _pending.length,\n });\n\n // Update server version\n _serverVersion = version;\n\n // Check if this is one of our pending transactions (ACK)\n const pendingIndex = _pending.findIndex(\n (p) => p.transaction.id === serverTx.id\n );\n\n if (pendingIndex !== -1) {\n // This is our transaction - confirmed!\n debugLog(\"handleServerTransaction: transaction confirmed (ACK)\", {\n txId: serverTx.id,\n pendingIndex,\n });\n\n const confirmed = _pending[pendingIndex]!;\n\n // Clear timeout\n const timeoutHandle = _timeoutHandles.get(serverTx.id);\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n _timeoutHandles.delete(serverTx.id);\n }\n\n // Remove from pending\n _pending.splice(pendingIndex, 1);\n\n // Apply to server state\n const tempDoc = Document.make(schema, { initialState: _serverState });\n tempDoc.apply(serverTx.ops);\n _serverState = tempDoc.get();\n\n debugLog(\"handleServerTransaction: updated server state\", {\n txId: serverTx.id,\n newServerState: _serverState,\n remainingPending: _pending.length,\n });\n\n // Recompute optimistic state (pending txs already applied, just need to update base)\n recomputeOptimisticState();\n } else {\n // This is someone else's transaction - need to rebase\n debugLog(\"handleServerTransaction: remote transaction, rebasing pending\", {\n txId: serverTx.id,\n pendingCount: _pending.length,\n });\n\n // Apply to server state\n const tempDoc = Document.make(schema, { initialState: _serverState });\n tempDoc.apply(serverTx.ops);\n _serverState = tempDoc.get();\n\n // Add to history for potential rebase after rejection\n _serverTransactionHistory.push(serverTx);\n if (_serverTransactionHistory.length > MAX_HISTORY_SIZE) {\n _serverTransactionHistory.shift();\n }\n\n // Rebase all pending transactions using primitive-based transformation\n const rebasedPending = _pending.map((p) => ({\n ...p,\n transaction: Rebase.transformTransactionWithPrimitive(p.transaction, serverTx, schema),\n }));\n\n debugLog(\"handleServerTransaction: rebased pending transactions\", {\n txId: serverTx.id,\n rebasedCount: rebasedPending.length,\n originalPendingIds: _pending.map((p) => p.transaction.id),\n rebasedPendingIds: rebasedPending.map((p) => p.transaction.id),\n });\n\n _pending = rebasedPending;\n\n // Recompute optimistic state\n recomputeOptimisticState();\n }\n };\n\n /**\n * Handles a transaction rejection from the server.\n */\n const handleRejection = (txId: string, reason: string): void => {\n debugLog(\"handleRejection\", {\n txId,\n reason,\n pendingCount: _pending.length,\n });\n\n const index = _pending.findIndex((p) => p.transaction.id === txId);\n if (index === -1) {\n debugLog(\"handleRejection: transaction not found (already removed)\", { txId });\n return; // Already removed\n }\n\n const rejected = _pending[index]!;\n\n // Clear timeout\n const timeoutHandle = _timeoutHandles.get(txId);\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n _timeoutHandles.delete(txId);\n }\n\n // Remove rejected transaction\n _pending.splice(index, 1);\n\n debugLog(\"handleRejection: removed rejected transaction, rebasing remaining\", {\n txId,\n remainingPending: _pending.length,\n serverHistorySize: _serverTransactionHistory.length,\n });\n\n // Re-transform remaining pending transactions without the rejected one\n // We need to replay from their original state\n const remainingOriginals = _pending.map((p) => p.original);\n const retransformed = Rebase.rebaseAfterRejectionWithPrimitive(\n [...remainingOriginals, rejected.original],\n txId,\n _serverTransactionHistory,\n schema\n );\n\n // Update pending with retransformed versions\n _pending = _pending.map((p, i) => ({\n ...p,\n transaction: retransformed[i] ?? p.transaction,\n }));\n\n debugLog(\"handleRejection: rebased remaining transactions\", {\n txId,\n rebasedCount: _pending.length,\n });\n\n // Recompute optimistic state\n recomputeOptimisticState();\n\n // Notify rejection\n onRejection?.(rejected.original, reason);\n };\n\n /**\n * Handles a snapshot from the server.\n * @param isInitialSnapshot - If true, this is the initial sync snapshot\n */\n const handleSnapshot = (state: unknown, version: number, isInitialSnapshot: boolean = false): void => {\n debugLog(\"handleSnapshot\", {\n isInitialSnapshot,\n version,\n currentServerVersion: _serverVersion,\n pendingCount: _pending.length,\n state,\n });\n\n if (!isInitialSnapshot) {\n debugLog(\"handleSnapshot: non-initial snapshot, clearing pending transactions\", {\n clearedPendingCount: _pending.length,\n });\n\n // For non-initial snapshots, clear all pending (they're now invalid)\n for (const handle of _timeoutHandles.values()) {\n clearTimeout(handle);\n }\n _timeoutHandles.clear();\n\n // Notify rejections for all pending\n for (const pending of _pending) {\n onRejection?.(pending.original, \"State reset due to resync\");\n }\n\n _pending = [];\n }\n\n _serverTransactionHistory = [];\n _serverState = state as Primitive.InferState<TSchema>;\n _serverVersion = version;\n\n debugLog(\"handleSnapshot: updated server state\", {\n newVersion: _serverVersion,\n newState: _serverState,\n });\n\n // Recompute optimistic state (now equals server state)\n recomputeOptimisticState();\n };\n\n /**\n * Processes buffered messages after receiving the initial snapshot.\n * Filters out transactions already included in the snapshot (version <= snapshotVersion)\n * and applies newer transactions in order.\n */\n const processBufferedMessages = (\n bufferedMessages: Transport.ServerMessage[],\n snapshotVersion: number\n ): void => {\n debugLog(\"processBufferedMessages\", {\n bufferedCount: bufferedMessages.length,\n snapshotVersion,\n });\n\n // Sort transactions by version to ensure correct order\n const sortedMessages = [...bufferedMessages].sort((a, b) => {\n if (a.type === \"transaction\" && b.type === \"transaction\") {\n return a.version - b.version;\n }\n return 0;\n });\n\n // Process each buffered message\n for (const message of sortedMessages) {\n switch (message.type) {\n case \"transaction\":\n // Only apply transactions with version > snapshot version\n if (message.version > snapshotVersion) {\n debugLog(\"processBufferedMessages: applying buffered transaction\", {\n txId: message.transaction.id,\n version: message.version,\n snapshotVersion,\n });\n handleServerTransaction(message.transaction, message.version);\n } else {\n debugLog(\"processBufferedMessages: skipping buffered transaction (already in snapshot)\", {\n txId: message.transaction.id,\n version: message.version,\n snapshotVersion,\n });\n }\n break;\n case \"error\":\n // Errors are still relevant - pass through\n debugLog(\"processBufferedMessages: processing buffered error\", {\n txId: message.transactionId,\n reason: message.reason,\n });\n handleRejection(message.transactionId, message.reason);\n break;\n // Ignore additional snapshots in buffer - we already have one\n }\n }\n };\n\n /**\n * Completes initialization and transitions to ready state.\n */\n const completeInitialization = (): void => {\n debugLog(\"completeInitialization\");\n\n // Clear init timeout\n if (_initTimeoutHandle !== null) {\n clearTimeout(_initTimeoutHandle);\n _initTimeoutHandle = null;\n }\n\n _initState = { type: \"ready\" };\n\n // Resolve the connect promise\n if (_initResolver) {\n _initResolver();\n _initResolver = null;\n _initRejecter = null;\n }\n\n debugLog(\"completeInitialization: ready\", {\n serverVersion: _serverVersion,\n serverState: _serverState,\n });\n\n // Notify ready\n notifyReady();\n };\n\n /**\n * Handles initialization timeout.\n */\n const handleInitTimeout = (): void => {\n debugLog(\"handleInitTimeout: initialization timed out\");\n _initTimeoutHandle = null;\n\n // Reject the connect promise\n if (_initRejecter) {\n const error = new Error(\"Initialization timed out waiting for snapshot\");\n _initRejecter(error);\n _initResolver = null;\n _initRejecter = null;\n }\n\n // Reset to uninitialized state\n _initState = { type: \"uninitialized\" };\n };\n\n /**\n * Handles incoming server messages.\n * During initialization, messages are buffered until the snapshot arrives.\n * Presence messages are always processed immediately (not buffered).\n */\n const handleServerMessage = (message: Transport.ServerMessage): void => {\n debugLog(\"handleServerMessage\", {\n messageType: message.type,\n initState: _initState.type,\n });\n\n // Presence messages are always handled immediately (not buffered)\n // This allows presence to work even during document initialization\n if (message.type === \"presence_snapshot\") {\n handlePresenceSnapshot(message);\n return;\n }\n if (message.type === \"presence_update\") {\n handlePresenceUpdate(message);\n return;\n }\n if (message.type === \"presence_remove\") {\n handlePresenceRemove(message);\n return;\n }\n\n // Handle based on initialization state\n if (_initState.type === \"initializing\") {\n if (message.type === \"snapshot\") {\n debugLog(\"handleServerMessage: received snapshot during initialization\", {\n version: message.version,\n bufferedCount: _initState.bufferedMessages.length,\n });\n // Snapshot received - apply it and process buffered messages\n const buffered = _initState.bufferedMessages;\n handleSnapshot(message.state, message.version, true);\n processBufferedMessages(buffered, message.version);\n completeInitialization();\n } else {\n debugLog(\"handleServerMessage: buffering message during initialization\", {\n messageType: message.type,\n bufferedCount: _initState.bufferedMessages.length + 1,\n });\n // Buffer other messages during initialization\n _initState.bufferedMessages.push(message);\n }\n return;\n }\n\n // Normal message handling when ready (or uninitialized with initial state)\n switch (message.type) {\n case \"transaction\":\n handleServerTransaction(message.transaction, message.version);\n break;\n case \"snapshot\":\n handleSnapshot(message.state, message.version, false);\n break;\n case \"error\":\n handleRejection(message.transactionId, message.reason);\n break;\n }\n };\n\n // ==========================================================================\n // Public API\n // ==========================================================================\n\n const clientDocument = {\n schema,\n\n get root() {\n return _optimisticDoc.root;\n },\n\n get: () => _optimisticDoc.get(),\n\n getServerState: () => _serverState,\n\n getServerVersion: () => _serverVersion,\n\n getPendingCount: () => _pending.length,\n\n hasPendingChanges: () => _pending.length > 0,\n\n transaction: <R,>(fn: (root: Primitive.InferProxy<TSchema>) => R): R => {\n debugLog(\"transaction: starting\", {\n isConnected: transport.isConnected(),\n isReady: _initState.type === \"ready\",\n pendingCount: _pending.length,\n });\n\n // Allow transactions even when disconnected - they will be queued\n // Only require that we have been initialized at least once (have server state)\n if (_initState.type !== \"ready\") {\n throw new InvalidStateError(\"Client is not ready. Wait for initialization to complete.\");\n }\n\n // Run the transaction on the optimistic document\n const result = _optimisticDoc.transaction(fn);\n\n // Flush and get the transaction\n const tx = _optimisticDoc.flush();\n\n // If there are operations, submit to server\n if (!Transaction.isEmpty(tx)) {\n debugLog(\"transaction: flushed, submitting\", {\n txId: tx.id,\n opsCount: tx.ops.length,\n });\n submitTransaction(tx);\n } else {\n debugLog(\"transaction: flushed, empty transaction (no ops)\");\n }\n\n // Notify state change\n notifyStateChange(_optimisticDoc.get());\n\n return result;\n },\n\n connect: async (): Promise<void> => {\n debugLog(\"connect: starting\");\n // Subscribe to server messages\n _unsubscribe = transport.subscribe(handleServerMessage);\n\n // Connect transport\n await transport.connect();\n debugLog(\"connect: transport connected\");\n\n notifyConnectionChange(true);\n\n // Set initial presence if provided\n if (presenceSchema && initialPresence !== undefined) {\n debugLog(\"connect: setting initial presence\", { initialPresence });\n const validated = Presence.validate(presenceSchema, initialPresence);\n _presenceSelfData = validated;\n transport.sendPresenceSet(validated);\n notifyPresenceChange();\n }\n\n // If we already have initial state, we're ready immediately\n if (_initState.type === \"ready\") {\n debugLog(\"connect: already ready (has initial state)\");\n notifyReady();\n return;\n }\n\n // Enter initializing state - buffer messages until snapshot arrives\n _initState = { type: \"initializing\", bufferedMessages: [] };\n debugLog(\"connect: entering initializing state\", {\n initTimeout,\n });\n\n // Set up initialization timeout\n _initTimeoutHandle = setTimeout(handleInitTimeout, initTimeout);\n\n // Create a promise that resolves when we're ready\n const readyPromise = new Promise<void>((resolve, reject) => {\n _initResolver = resolve;\n _initRejecter = reject;\n });\n\n // Request initial snapshot\n debugLog(\"connect: requesting initial snapshot\");\n \n transport.requestSnapshot();\n\n\n // Wait for initialization to complete\n await readyPromise;\n debugLog(\"connect: completed\");\n },\n\n disconnect: (): void => {\n debugLog(\"disconnect: starting\", {\n pendingCount: _pending.length,\n initState: _initState.type,\n });\n\n // Clear all timeouts\n for (const handle of _timeoutHandles.values()) {\n clearTimeout(handle);\n }\n _timeoutHandles.clear();\n\n // Clear init timeout\n if (_initTimeoutHandle !== null) {\n clearTimeout(_initTimeoutHandle);\n _initTimeoutHandle = null;\n }\n\n // Reject any pending init promise\n if (_initRejecter) {\n _initRejecter(new Error(\"Disconnected during initialization\"));\n _initResolver = null;\n _initRejecter = null;\n }\n\n // Reset init state\n if (_initState.type === \"initializing\") {\n _initState = { type: \"uninitialized\" };\n }\n\n // Clear presence state\n clearPresenceState();\n\n // Unsubscribe\n if (_unsubscribe) {\n _unsubscribe();\n _unsubscribe = null;\n }\n\n // Disconnect transport\n transport.disconnect();\n\n notifyConnectionChange(false);\n debugLog(\"disconnect: completed\");\n },\n\n isConnected: () => transport.isConnected(),\n\n isReady: () => _initState.type === \"ready\",\n\n resync: (): void => {\n debugLog(\"resync: requesting snapshot\", {\n currentVersion: _serverVersion,\n pendingCount: _pending.length,\n });\n if (!transport.isConnected()) {\n throw new NotConnectedError();\n }\n transport.requestSnapshot();\n },\n\n subscribe: (listener: ClientDocumentListener<TSchema>): (() => void) => {\n _subscribers.add(listener);\n return () => {\n _subscribers.delete(listener);\n };\n },\n\n // =========================================================================\n // Presence API\n // =========================================================================\n\n presence: (presenceSchema\n ? {\n selfId: () => _presenceSelfId,\n\n self: () => _presenceSelfData as Presence.Infer<NonNullable<TPresence>> | undefined,\n\n others: () => _presenceOthers as ReadonlyMap<string, Presence.PresenceEntry<Presence.Infer<NonNullable<TPresence>>>>,\n\n all: () => {\n const all = new Map<string, Presence.PresenceEntry<unknown>>();\n // Add others\n for (const [id, entry] of _presenceOthers) {\n all.set(id, entry);\n }\n // Add self if we have data\n if (_presenceSelfId !== undefined && _presenceSelfData !== undefined) {\n all.set(_presenceSelfId, { data: _presenceSelfData });\n }\n return all as ReadonlyMap<string, Presence.PresenceEntry<Presence.Infer<NonNullable<TPresence>>>>;\n },\n\n set: (data: Presence.Infer<NonNullable<TPresence>>) => {\n if (!presenceSchema) return;\n\n // Validate against schema (throws if invalid)\n const validated = Presence.validate(presenceSchema, data);\n\n debugLog(\"presence.set\", { data: validated });\n\n // Update local state\n _presenceSelfData = validated;\n\n // Send to server\n transport.sendPresenceSet(validated);\n\n // Notify listeners\n notifyPresenceChange();\n },\n\n clear: () => {\n if (!presenceSchema) return;\n\n debugLog(\"presence.clear\");\n\n // Clear local state\n _presenceSelfData = undefined;\n\n // Send to server\n transport.sendPresenceClear();\n\n // Notify listeners\n notifyPresenceChange();\n },\n\n subscribe: (listener: PresenceListener<Presence.Infer<NonNullable<TPresence>>>) => {\n _presenceSubscribers.add(listener as PresenceListener<unknown>);\n return () => {\n _presenceSubscribers.delete(listener as PresenceListener<unknown>);\n };\n },\n }\n : undefined) as TPresence extends Presence.AnyPresence\n ? ClientPresence<Presence.Infer<TPresence>>\n : undefined,\n } as ClientDocument<TSchema, TPresence>;\n\n return clientDocument;\n};\n"],"mappings":"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"}
|
|
@@ -310,12 +310,12 @@ const make = (options) => {
|
|
|
310
310
|
transaction
|
|
311
311
|
};
|
|
312
312
|
if (_state.type === "connected") sendRaw(message);
|
|
313
|
-
else
|
|
313
|
+
else _messageQueue.push(message);
|
|
314
314
|
},
|
|
315
315
|
requestSnapshot: () => {
|
|
316
316
|
const message = { type: "request_snapshot" };
|
|
317
317
|
if (_state.type === "connected") sendRaw(message);
|
|
318
|
-
else
|
|
318
|
+
else _messageQueue.push(message);
|
|
319
319
|
},
|
|
320
320
|
subscribe: (handler) => {
|
|
321
321
|
_messageHandlers.add(handler);
|
|
@@ -371,16 +371,16 @@ const make = (options) => {
|
|
|
371
371
|
data
|
|
372
372
|
};
|
|
373
373
|
if (_state.type === "connected") sendRaw(message);
|
|
374
|
-
else
|
|
375
|
-
_messageQueue = _messageQueue.filter((
|
|
374
|
+
else {
|
|
375
|
+
_messageQueue = _messageQueue.filter((m) => m.type !== "presence_set");
|
|
376
376
|
_messageQueue.push(message);
|
|
377
377
|
}
|
|
378
378
|
},
|
|
379
379
|
sendPresenceClear: () => {
|
|
380
380
|
const message = { type: "presence_clear" };
|
|
381
381
|
if (_state.type === "connected") sendRaw(message);
|
|
382
|
-
else
|
|
383
|
-
_messageQueue = _messageQueue.filter((
|
|
382
|
+
else {
|
|
383
|
+
_messageQueue = _messageQueue.filter((m) => m.type !== "presence_clear");
|
|
384
384
|
_messageQueue.push(message);
|
|
385
385
|
}
|
|
386
386
|
}
|
|
@@ -310,12 +310,12 @@ const make = (options) => {
|
|
|
310
310
|
transaction
|
|
311
311
|
};
|
|
312
312
|
if (_state.type === "connected") sendRaw(message);
|
|
313
|
-
else
|
|
313
|
+
else _messageQueue.push(message);
|
|
314
314
|
},
|
|
315
315
|
requestSnapshot: () => {
|
|
316
316
|
const message = { type: "request_snapshot" };
|
|
317
317
|
if (_state.type === "connected") sendRaw(message);
|
|
318
|
-
else
|
|
318
|
+
else _messageQueue.push(message);
|
|
319
319
|
},
|
|
320
320
|
subscribe: (handler) => {
|
|
321
321
|
_messageHandlers.add(handler);
|
|
@@ -371,16 +371,16 @@ const make = (options) => {
|
|
|
371
371
|
data
|
|
372
372
|
};
|
|
373
373
|
if (_state.type === "connected") sendRaw(message);
|
|
374
|
-
else
|
|
375
|
-
_messageQueue = _messageQueue.filter((
|
|
374
|
+
else {
|
|
375
|
+
_messageQueue = _messageQueue.filter((m) => m.type !== "presence_set");
|
|
376
376
|
_messageQueue.push(message);
|
|
377
377
|
}
|
|
378
378
|
},
|
|
379
379
|
sendPresenceClear: () => {
|
|
380
380
|
const message = { type: "presence_clear" };
|
|
381
381
|
if (_state.type === "connected") sendRaw(message);
|
|
382
|
-
else
|
|
383
|
-
_messageQueue = _messageQueue.filter((
|
|
382
|
+
else {
|
|
383
|
+
_messageQueue = _messageQueue.filter((m) => m.type !== "presence_clear");
|
|
384
384
|
_messageQueue.push(message);
|
|
385
385
|
}
|
|
386
386
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebSocketTransport.mjs","names":["_state: ConnectionState","_ws: WebSocket | null","_messageHandlers: Set<(message: Transport.ServerMessage) => void>","_connectionTimeoutHandle: ReturnType<typeof setTimeout> | null","_heartbeatIntervalHandle: ReturnType<typeof setInterval> | null","_heartbeatTimeoutHandle: ReturnType<typeof setTimeout> | null","_reconnectTimeoutHandle: ReturnType<typeof setTimeout> | null","_messageQueue: Transport.ClientMessage[]","_connectResolver: (() => void) | null","_connectRejecter: ((error: Error) => void) | null","Transaction.encode","Transaction.decode","message: Transport.ClientMessage","message"],"sources":["../../src/client/WebSocketTransport.ts"],"sourcesContent":["import * as Transaction from \"../Transaction\";\n\nimport * as Transport from \"./Transport\";\nimport { WebSocketError, AuthenticationError } from \"./errors\";\n\n// =============================================================================\n// WebSocket Transport Options\n// =============================================================================\n\n/**\n * Options for creating a WebSocket transport.\n */\nexport interface WebSocketTransportOptions extends Transport.TransportOptions {\n /** WebSocket URL (ws:// or wss://) - base URL without document path */\n readonly url: string;\n /** Document ID to connect to. Will be appended to URL as /doc/{documentId} */\n readonly documentId?: string;\n /** WebSocket subprotocols */\n readonly protocols?: string[];\n /** Authentication token or function that returns a token */\n readonly authToken?: string | (() => string | Promise<string>);\n /** Interval between heartbeat pings (ms). Default: 30000 */\n readonly heartbeatInterval?: number;\n /** Timeout to wait for pong response (ms). Default: 10000 */\n readonly heartbeatTimeout?: number;\n /** Maximum delay between reconnection attempts (ms). Default: 30000 */\n readonly maxReconnectDelay?: number;\n}\n\n// =============================================================================\n// Connection State\n// =============================================================================\n\ntype ConnectionState =\n | { type: \"disconnected\" }\n | { type: \"connecting\" }\n | { type: \"authenticating\" }\n | { type: \"connected\" }\n | { type: \"reconnecting\"; attempt: number };\n\n// =============================================================================\n// WebSocket Transport Implementation\n// =============================================================================\n\n/**\n * Creates a WebSocket-based transport for real-time server communication.\n */\n/**\n * Build the WebSocket URL with optional document ID path.\n */\nconst buildWebSocketUrl = (baseUrl: string, documentId?: string): string => {\n if (!documentId) {\n return baseUrl;\n }\n // Remove trailing slash from base URL\n const normalizedBase = baseUrl.replace(/\\/+$/, \"\");\n // Encode the document ID for URL safety\n const encodedDocId = encodeURIComponent(documentId);\n return `${normalizedBase}/doc/${encodedDocId}`;\n};\n\nexport const make = (options: WebSocketTransportOptions): Transport.Transport => {\n const {\n url: baseUrl,\n documentId,\n protocols,\n authToken,\n onEvent,\n connectionTimeout = 10000,\n autoReconnect = true,\n maxReconnectAttempts = 10,\n reconnectDelay = 1000,\n maxReconnectDelay = 30000,\n heartbeatInterval = 30000,\n heartbeatTimeout = 10000,\n } = options;\n\n // Build the full URL with document ID if provided\n const url = buildWebSocketUrl(baseUrl, documentId);\n\n // ==========================================================================\n // Internal State\n // ==========================================================================\n\n let _state: ConnectionState = { type: \"disconnected\" };\n let _ws: WebSocket | null = null;\n let _messageHandlers: Set<(message: Transport.ServerMessage) => void> = new Set();\n\n // Timers\n let _connectionTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n let _heartbeatIntervalHandle: ReturnType<typeof setInterval> | null = null;\n let _heartbeatTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n let _reconnectTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n\n // Message queue for messages sent while reconnecting\n let _messageQueue: Transport.ClientMessage[] = [];\n\n // Promise resolvers for connect()\n let _connectResolver: (() => void) | null = null;\n let _connectRejecter: ((error: Error) => void) | null = null;\n\n // Track reconnection attempt count (persists through connecting state)\n let _reconnectAttempt = 0;\n\n // ==========================================================================\n // Helper Functions\n // ==========================================================================\n\n const emit = (handler: Transport.TransportEventHandler | undefined, event: Parameters<Transport.TransportEventHandler>[0]) => {\n handler?.(event);\n };\n\n /**\n * Encodes a client message for network transport.\n */\n const encodeClientMessage = (message: Transport.ClientMessage): Transport.EncodedClientMessage => {\n if (message.type === \"submit\") {\n return {\n type: \"submit\",\n transaction: Transaction.encode(message.transaction),\n };\n }\n return message;\n };\n\n /**\n * Decodes a server message from network transport.\n */\n const decodeServerMessage = (encoded: Transport.EncodedServerMessage): Transport.ServerMessage => {\n if (encoded.type === \"transaction\") {\n return {\n type: \"transaction\",\n transaction: Transaction.decode(encoded.transaction),\n version: encoded.version,\n };\n }\n return encoded;\n };\n\n /**\n * Sends a raw message over the WebSocket.\n */\n const sendRaw = (message: Transport.ClientMessage): void => {\n if (_ws && _ws.readyState === WebSocket.OPEN) {\n _ws.send(JSON.stringify(encodeClientMessage(message)));\n }\n };\n\n /**\n * Clears all active timers.\n */\n const clearTimers = (): void => {\n if (_connectionTimeoutHandle) {\n clearTimeout(_connectionTimeoutHandle);\n _connectionTimeoutHandle = null;\n }\n if (_heartbeatIntervalHandle) {\n clearInterval(_heartbeatIntervalHandle);\n _heartbeatIntervalHandle = null;\n }\n if (_heartbeatTimeoutHandle) {\n clearTimeout(_heartbeatTimeoutHandle);\n _heartbeatTimeoutHandle = null;\n }\n if (_reconnectTimeoutHandle) {\n clearTimeout(_reconnectTimeoutHandle);\n _reconnectTimeoutHandle = null;\n }\n };\n\n /**\n * Starts the heartbeat mechanism.\n */\n const startHeartbeat = (): void => {\n stopHeartbeat();\n\n _heartbeatIntervalHandle = setInterval(() => {\n if (_state.type !== \"connected\") return;\n\n // Send ping\n sendRaw({ type: \"ping\" });\n\n // Set timeout for pong response\n _heartbeatTimeoutHandle = setTimeout(() => {\n // No pong received - connection is dead\n handleConnectionLost(\"Heartbeat timeout\");\n }, heartbeatTimeout);\n }, heartbeatInterval);\n };\n\n /**\n * Stops the heartbeat mechanism.\n */\n const stopHeartbeat = (): void => {\n if (_heartbeatIntervalHandle) {\n clearInterval(_heartbeatIntervalHandle);\n _heartbeatIntervalHandle = null;\n }\n if (_heartbeatTimeoutHandle) {\n clearTimeout(_heartbeatTimeoutHandle);\n _heartbeatTimeoutHandle = null;\n }\n };\n\n /**\n * Handles pong response - clears the heartbeat timeout.\n */\n const handlePong = (): void => {\n if (_heartbeatTimeoutHandle) {\n clearTimeout(_heartbeatTimeoutHandle);\n _heartbeatTimeoutHandle = null;\n }\n };\n\n /**\n * Flushes the message queue after reconnection.\n */\n const flushMessageQueue = (): void => {\n const queue = _messageQueue;\n _messageQueue = [];\n for (const message of queue) {\n sendRaw(message);\n }\n };\n\n /**\n * Calculates reconnection delay with exponential backoff.\n */\n const getReconnectDelay = (attempt: number): number => {\n const delay = reconnectDelay * Math.pow(2, attempt);\n return Math.min(delay, maxReconnectDelay);\n };\n\n /**\n * Resolves the auth token (handles both string and function).\n * Returns empty string if no token is configured.\n */\n const resolveAuthToken = async (): Promise<string> => {\n if (!authToken) return \"\";\n if (typeof authToken === \"string\") return authToken;\n return authToken();\n };\n\n /**\n * Performs authentication after connection.\n * Always sends an auth message (even with empty token) to trigger server auth flow.\n */\n const authenticate = async (): Promise<void> => {\n const token = await resolveAuthToken();\n _state = { type: \"authenticating\" };\n sendRaw({ type: \"auth\", token });\n };\n\n /**\n * Handles authentication result from server.\n */\n const handleAuthResult = (success: boolean, error?: string): void => {\n if (!success) {\n const authError = new AuthenticationError(error || \"Authentication failed\");\n cleanup();\n _connectRejecter?.(authError);\n _connectResolver = null;\n _connectRejecter = null;\n emit(onEvent, { type: \"error\", error: authError });\n return;\n }\n\n // Auth successful - complete connection\n completeConnection();\n };\n\n /**\n * Completes the connection process.\n */\n const completeConnection = (): void => {\n _state = { type: \"connected\" };\n\n // Reset reconnection attempt counter on successful connection\n _reconnectAttempt = 0;\n\n // Clear connection timeout\n if (_connectionTimeoutHandle) {\n clearTimeout(_connectionTimeoutHandle);\n _connectionTimeoutHandle = null;\n }\n\n // Start heartbeat\n startHeartbeat();\n\n // Flush any queued messages\n flushMessageQueue();\n\n // Resolve connect promise\n _connectResolver?.();\n _connectResolver = null;\n _connectRejecter = null;\n\n emit(onEvent, { type: \"connected\" });\n };\n\n /**\n * Cleans up WebSocket and related state.\n */\n const cleanup = (): void => {\n clearTimers();\n\n if (_ws) {\n // Remove listeners to prevent callbacks\n _ws.onopen = null;\n _ws.onclose = null;\n _ws.onerror = null;\n _ws.onmessage = null;\n \n if (_ws.readyState === WebSocket.OPEN || _ws.readyState === WebSocket.CONNECTING) {\n _ws.close();\n }\n _ws = null;\n }\n };\n\n /**\n * Handles connection lost - initiates reconnection if enabled.\n */\n const handleConnectionLost = (reason?: string): void => {\n cleanup();\n\n if (_state.type === \"disconnected\") return;\n\n const wasInitialConnect = _connectRejecter !== null;\n\n if (wasInitialConnect) {\n // Failed during initial connection\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n _connectRejecter!(new WebSocketError(\"Connection failed\", undefined, reason));\n _connectResolver = null;\n _connectRejecter = null;\n emit(onEvent, { type: \"disconnected\", reason });\n return;\n }\n\n if (!autoReconnect) {\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n emit(onEvent, { type: \"disconnected\", reason });\n return;\n }\n\n _reconnectAttempt++;\n\n if (_reconnectAttempt > maxReconnectAttempts) {\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n emit(onEvent, { type: \"disconnected\", reason: \"Max reconnection attempts reached\" });\n return;\n }\n\n // Enter reconnecting state\n _state = { type: \"reconnecting\", attempt: _reconnectAttempt };\n emit(onEvent, { type: \"reconnecting\", attempt: _reconnectAttempt });\n\n // Schedule reconnection\n const delay = getReconnectDelay(_reconnectAttempt - 1);\n _reconnectTimeoutHandle = setTimeout(() => {\n _reconnectTimeoutHandle = null;\n attemptConnection();\n }, delay);\n };\n\n /**\n * Attempts to establish WebSocket connection.\n */\n const attemptConnection = (): void => {\n if (_state.type === \"connected\") return;\n\n _state = { type: \"connecting\" };\n\n try {\n _ws = new WebSocket(url, protocols);\n } catch (error) {\n handleConnectionLost((error as Error).message);\n return;\n }\n\n // Set connection timeout\n _connectionTimeoutHandle = setTimeout(() => {\n _connectionTimeoutHandle = null;\n handleConnectionLost(\"Connection timeout\");\n }, connectionTimeout);\n\n _ws.onopen = async () => {\n // Clear connection timeout\n if (_connectionTimeoutHandle) {\n clearTimeout(_connectionTimeoutHandle);\n _connectionTimeoutHandle = null;\n }\n\n try {\n // Always authenticate (even with empty token) to trigger server auth flow\n await authenticate();\n // Connection completes after auth_result is received\n } catch (error) {\n handleConnectionLost((error as Error).message);\n }\n };\n\n _ws.onclose = (event) => {\n handleConnectionLost(event.reason || `Connection closed (code: ${event.code})`);\n };\n\n _ws.onerror = () => {\n // Error details come through onclose\n };\n\n _ws.onmessage = (event) => {\n try {\n const encoded = JSON.parse(event.data as string) as Transport.EncodedServerMessage;\n const message = decodeServerMessage(encoded);\n handleMessage(message);\n } catch {\n // Invalid message - ignore\n }\n };\n };\n\n /**\n * Handles incoming server messages.\n */\n const handleMessage = (message: Transport.ServerMessage): void => {\n // Handle internal messages\n if (message.type === \"pong\") {\n handlePong();\n return;\n }\n\n if (message.type === \"auth_result\") {\n handleAuthResult(message.success, message.error);\n return;\n }\n\n // Forward to subscribers\n for (const handler of _messageHandlers) {\n try {\n handler(message);\n } catch {\n // Ignore handler errors\n }\n }\n };\n\n // ==========================================================================\n // Public API\n // ==========================================================================\n\n const transport: Transport.Transport = {\n send: (transaction: Transaction.Transaction): void => {\n const message: Transport.ClientMessage = { type: \"submit\", transaction };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else if (_state.type === \"reconnecting\") {\n // Queue message for when we reconnect\n _messageQueue.push(message);\n }\n // If disconnected, silently drop (caller should check isConnected)\n },\n\n requestSnapshot: (): void => {\n const message: Transport.ClientMessage = { type: \"request_snapshot\" };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else if (_state.type === \"reconnecting\") {\n _messageQueue.push(message);\n }\n },\n\n subscribe: (handler: (message: Transport.ServerMessage) => void): (() => void) => {\n _messageHandlers.add(handler);\n return () => {\n _messageHandlers.delete(handler);\n };\n },\n\n connect: async (): Promise<void> => {\n if (_state.type === \"connected\") {\n return;\n }\n\n if (_state.type === \"connecting\" || _state.type === \"authenticating\") {\n // Already connecting - wait for existing promise\n return new Promise((resolve, reject) => {\n const existingResolver = _connectResolver;\n const existingRejecter = _connectRejecter;\n _connectResolver = () => {\n existingResolver?.();\n resolve();\n };\n _connectRejecter = (error) => {\n existingRejecter?.(error);\n reject(error);\n };\n });\n }\n\n return new Promise((resolve, reject) => {\n _connectResolver = resolve;\n _connectRejecter = reject;\n attemptConnection();\n });\n },\n\n disconnect: (): void => {\n // Cancel any pending reconnection\n if (_reconnectTimeoutHandle) {\n clearTimeout(_reconnectTimeoutHandle);\n _reconnectTimeoutHandle = null;\n }\n\n // Reject any pending connect promise\n if (_connectRejecter) {\n _connectRejecter(new WebSocketError(\"Disconnected by user\"));\n _connectResolver = null;\n _connectRejecter = null;\n }\n\n // Clean up\n cleanup();\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n _messageQueue = [];\n\n emit(onEvent, { type: \"disconnected\", reason: \"User disconnected\" });\n },\n\n isConnected: (): boolean => {\n return _state.type === \"connected\";\n },\n\n // =========================================================================\n // Presence Methods\n // =========================================================================\n\n sendPresenceSet: (data: unknown): void => {\n const message: Transport.ClientMessage = { type: \"presence_set\", data };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else if (_state.type === \"reconnecting\") {\n // Remove all set messages from the message queue\n _messageQueue = _messageQueue.filter((message) => message.type !== \"presence_set\");\n // Add the new presence set message to the queue\n _messageQueue.push(message);\n }\n },\n\n sendPresenceClear: (): void => {\n const message: Transport.ClientMessage = { type: \"presence_clear\" };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else if (_state.type === \"reconnecting\") {\n // Remove all clear messages from the message queue\n _messageQueue = _messageQueue.filter((message) => message.type !== \"presence_clear\");\n // Add the new presence clear message to the queue\n _messageQueue.push(message);\n }\n },\n };\n\n return transport;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WebSocketTransport.mjs","names":["_state: ConnectionState","_ws: WebSocket | null","_messageHandlers: Set<(message: Transport.ServerMessage) => void>","_connectionTimeoutHandle: ReturnType<typeof setTimeout> | null","_heartbeatIntervalHandle: ReturnType<typeof setInterval> | null","_heartbeatTimeoutHandle: ReturnType<typeof setTimeout> | null","_reconnectTimeoutHandle: ReturnType<typeof setTimeout> | null","_messageQueue: Transport.ClientMessage[]","_connectResolver: (() => void) | null","_connectRejecter: ((error: Error) => void) | null","Transaction.encode","Transaction.decode","message: Transport.ClientMessage"],"sources":["../../src/client/WebSocketTransport.ts"],"sourcesContent":["import * as Transaction from \"../Transaction\";\n\nimport * as Transport from \"./Transport\";\nimport { WebSocketError, AuthenticationError } from \"./errors\";\n\n// =============================================================================\n// WebSocket Transport Options\n// =============================================================================\n\n/**\n * Options for creating a WebSocket transport.\n */\nexport interface WebSocketTransportOptions extends Transport.TransportOptions {\n /** WebSocket URL (ws:// or wss://) - base URL without document path */\n readonly url: string;\n /** Document ID to connect to. Will be appended to URL as /doc/{documentId} */\n readonly documentId?: string;\n /** WebSocket subprotocols */\n readonly protocols?: string[];\n /** Authentication token or function that returns a token */\n readonly authToken?: string | (() => string | Promise<string>);\n /** Interval between heartbeat pings (ms). Default: 30000 */\n readonly heartbeatInterval?: number;\n /** Timeout to wait for pong response (ms). Default: 10000 */\n readonly heartbeatTimeout?: number;\n /** Maximum delay between reconnection attempts (ms). Default: 30000 */\n readonly maxReconnectDelay?: number;\n}\n\n// =============================================================================\n// Connection State\n// =============================================================================\n\ntype ConnectionState =\n | { type: \"disconnected\" }\n | { type: \"connecting\" }\n | { type: \"authenticating\" }\n | { type: \"connected\" }\n | { type: \"reconnecting\"; attempt: number };\n\n// =============================================================================\n// WebSocket Transport Implementation\n// =============================================================================\n\n/**\n * Creates a WebSocket-based transport for real-time server communication.\n */\n/**\n * Build the WebSocket URL with optional document ID path.\n */\nconst buildWebSocketUrl = (baseUrl: string, documentId?: string): string => {\n if (!documentId) {\n return baseUrl;\n }\n // Remove trailing slash from base URL\n const normalizedBase = baseUrl.replace(/\\/+$/, \"\");\n // Encode the document ID for URL safety\n const encodedDocId = encodeURIComponent(documentId);\n return `${normalizedBase}/doc/${encodedDocId}`;\n};\n\nexport const make = (options: WebSocketTransportOptions): Transport.Transport => {\n const {\n url: baseUrl,\n documentId,\n protocols,\n authToken,\n onEvent,\n connectionTimeout = 10000,\n autoReconnect = true,\n maxReconnectAttempts = 10,\n reconnectDelay = 1000,\n maxReconnectDelay = 30000,\n heartbeatInterval = 30000,\n heartbeatTimeout = 10000,\n } = options;\n\n // Build the full URL with document ID if provided\n const url = buildWebSocketUrl(baseUrl, documentId);\n\n // ==========================================================================\n // Internal State\n // ==========================================================================\n\n let _state: ConnectionState = { type: \"disconnected\" };\n let _ws: WebSocket | null = null;\n let _messageHandlers: Set<(message: Transport.ServerMessage) => void> = new Set();\n\n // Timers\n let _connectionTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n let _heartbeatIntervalHandle: ReturnType<typeof setInterval> | null = null;\n let _heartbeatTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n let _reconnectTimeoutHandle: ReturnType<typeof setTimeout> | null = null;\n\n // Message queue for messages sent while reconnecting\n let _messageQueue: Transport.ClientMessage[] = [];\n\n // Promise resolvers for connect()\n let _connectResolver: (() => void) | null = null;\n let _connectRejecter: ((error: Error) => void) | null = null;\n\n // Track reconnection attempt count (persists through connecting state)\n let _reconnectAttempt = 0;\n\n // ==========================================================================\n // Helper Functions\n // ==========================================================================\n\n const emit = (handler: Transport.TransportEventHandler | undefined, event: Parameters<Transport.TransportEventHandler>[0]) => {\n handler?.(event);\n };\n\n /**\n * Encodes a client message for network transport.\n */\n const encodeClientMessage = (message: Transport.ClientMessage): Transport.EncodedClientMessage => {\n if (message.type === \"submit\") {\n return {\n type: \"submit\",\n transaction: Transaction.encode(message.transaction),\n };\n }\n return message;\n };\n\n /**\n * Decodes a server message from network transport.\n */\n const decodeServerMessage = (encoded: Transport.EncodedServerMessage): Transport.ServerMessage => {\n if (encoded.type === \"transaction\") {\n return {\n type: \"transaction\",\n transaction: Transaction.decode(encoded.transaction),\n version: encoded.version,\n };\n }\n return encoded;\n };\n\n /**\n * Sends a raw message over the WebSocket.\n */\n const sendRaw = (message: Transport.ClientMessage): void => {\n if (_ws && _ws.readyState === WebSocket.OPEN) {\n _ws.send(JSON.stringify(encodeClientMessage(message)));\n }\n };\n\n /**\n * Clears all active timers.\n */\n const clearTimers = (): void => {\n if (_connectionTimeoutHandle) {\n clearTimeout(_connectionTimeoutHandle);\n _connectionTimeoutHandle = null;\n }\n if (_heartbeatIntervalHandle) {\n clearInterval(_heartbeatIntervalHandle);\n _heartbeatIntervalHandle = null;\n }\n if (_heartbeatTimeoutHandle) {\n clearTimeout(_heartbeatTimeoutHandle);\n _heartbeatTimeoutHandle = null;\n }\n if (_reconnectTimeoutHandle) {\n clearTimeout(_reconnectTimeoutHandle);\n _reconnectTimeoutHandle = null;\n }\n };\n\n /**\n * Starts the heartbeat mechanism.\n */\n const startHeartbeat = (): void => {\n stopHeartbeat();\n\n _heartbeatIntervalHandle = setInterval(() => {\n if (_state.type !== \"connected\") return;\n\n // Send ping\n sendRaw({ type: \"ping\" });\n\n // Set timeout for pong response\n _heartbeatTimeoutHandle = setTimeout(() => {\n // No pong received - connection is dead\n handleConnectionLost(\"Heartbeat timeout\");\n }, heartbeatTimeout);\n }, heartbeatInterval);\n };\n\n /**\n * Stops the heartbeat mechanism.\n */\n const stopHeartbeat = (): void => {\n if (_heartbeatIntervalHandle) {\n clearInterval(_heartbeatIntervalHandle);\n _heartbeatIntervalHandle = null;\n }\n if (_heartbeatTimeoutHandle) {\n clearTimeout(_heartbeatTimeoutHandle);\n _heartbeatTimeoutHandle = null;\n }\n };\n\n /**\n * Handles pong response - clears the heartbeat timeout.\n */\n const handlePong = (): void => {\n if (_heartbeatTimeoutHandle) {\n clearTimeout(_heartbeatTimeoutHandle);\n _heartbeatTimeoutHandle = null;\n }\n };\n\n /**\n * Flushes the message queue after reconnection.\n */\n const flushMessageQueue = (): void => {\n const queue = _messageQueue;\n _messageQueue = [];\n for (const message of queue) {\n sendRaw(message);\n }\n };\n\n /**\n * Calculates reconnection delay with exponential backoff.\n */\n const getReconnectDelay = (attempt: number): number => {\n const delay = reconnectDelay * Math.pow(2, attempt);\n return Math.min(delay, maxReconnectDelay);\n };\n\n /**\n * Resolves the auth token (handles both string and function).\n * Returns empty string if no token is configured.\n */\n const resolveAuthToken = async (): Promise<string> => {\n if (!authToken) return \"\";\n if (typeof authToken === \"string\") return authToken;\n return authToken();\n };\n\n /**\n * Performs authentication after connection.\n * Always sends an auth message (even with empty token) to trigger server auth flow.\n */\n const authenticate = async (): Promise<void> => {\n const token = await resolveAuthToken();\n _state = { type: \"authenticating\" };\n sendRaw({ type: \"auth\", token });\n };\n\n /**\n * Handles authentication result from server.\n */\n const handleAuthResult = (success: boolean, error?: string): void => {\n if (!success) {\n const authError = new AuthenticationError(error || \"Authentication failed\");\n cleanup();\n _connectRejecter?.(authError);\n _connectResolver = null;\n _connectRejecter = null;\n emit(onEvent, { type: \"error\", error: authError });\n return;\n }\n\n // Auth successful - complete connection\n completeConnection();\n };\n\n /**\n * Completes the connection process.\n */\n const completeConnection = (): void => {\n _state = { type: \"connected\" };\n\n // Reset reconnection attempt counter on successful connection\n _reconnectAttempt = 0;\n\n // Clear connection timeout\n if (_connectionTimeoutHandle) {\n clearTimeout(_connectionTimeoutHandle);\n _connectionTimeoutHandle = null;\n }\n\n // Start heartbeat\n startHeartbeat();\n\n // Flush any queued messages\n flushMessageQueue();\n\n // Resolve connect promise\n _connectResolver?.();\n _connectResolver = null;\n _connectRejecter = null;\n\n emit(onEvent, { type: \"connected\" });\n };\n\n /**\n * Cleans up WebSocket and related state.\n */\n const cleanup = (): void => {\n clearTimers();\n\n if (_ws) {\n // Remove listeners to prevent callbacks\n _ws.onopen = null;\n _ws.onclose = null;\n _ws.onerror = null;\n _ws.onmessage = null;\n \n if (_ws.readyState === WebSocket.OPEN || _ws.readyState === WebSocket.CONNECTING) {\n _ws.close();\n }\n _ws = null;\n }\n };\n\n /**\n * Handles connection lost - initiates reconnection if enabled.\n */\n const handleConnectionLost = (reason?: string): void => {\n cleanup();\n\n if (_state.type === \"disconnected\") return;\n\n const wasInitialConnect = _connectRejecter !== null;\n\n if (wasInitialConnect) {\n // Failed during initial connection\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n _connectRejecter!(new WebSocketError(\"Connection failed\", undefined, reason));\n _connectResolver = null;\n _connectRejecter = null;\n emit(onEvent, { type: \"disconnected\", reason });\n return;\n }\n\n if (!autoReconnect) {\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n emit(onEvent, { type: \"disconnected\", reason });\n return;\n }\n\n _reconnectAttempt++;\n\n if (_reconnectAttempt > maxReconnectAttempts) {\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n emit(onEvent, { type: \"disconnected\", reason: \"Max reconnection attempts reached\" });\n return;\n }\n\n // Enter reconnecting state\n _state = { type: \"reconnecting\", attempt: _reconnectAttempt };\n emit(onEvent, { type: \"reconnecting\", attempt: _reconnectAttempt });\n\n // Schedule reconnection\n const delay = getReconnectDelay(_reconnectAttempt - 1);\n _reconnectTimeoutHandle = setTimeout(() => {\n _reconnectTimeoutHandle = null;\n attemptConnection();\n }, delay);\n };\n\n /**\n * Attempts to establish WebSocket connection.\n */\n const attemptConnection = (): void => {\n if (_state.type === \"connected\") return;\n\n _state = { type: \"connecting\" };\n\n try {\n _ws = new WebSocket(url, protocols);\n } catch (error) {\n handleConnectionLost((error as Error).message);\n return;\n }\n\n // Set connection timeout\n _connectionTimeoutHandle = setTimeout(() => {\n _connectionTimeoutHandle = null;\n handleConnectionLost(\"Connection timeout\");\n }, connectionTimeout);\n\n _ws.onopen = async () => {\n // Clear connection timeout\n if (_connectionTimeoutHandle) {\n clearTimeout(_connectionTimeoutHandle);\n _connectionTimeoutHandle = null;\n }\n\n try {\n // Always authenticate (even with empty token) to trigger server auth flow\n await authenticate();\n // Connection completes after auth_result is received\n } catch (error) {\n handleConnectionLost((error as Error).message);\n }\n };\n\n _ws.onclose = (event) => {\n handleConnectionLost(event.reason || `Connection closed (code: ${event.code})`);\n };\n\n _ws.onerror = () => {\n // Error details come through onclose\n };\n\n _ws.onmessage = (event) => {\n try {\n const encoded = JSON.parse(event.data as string) as Transport.EncodedServerMessage;\n const message = decodeServerMessage(encoded);\n handleMessage(message);\n } catch {\n // Invalid message - ignore\n }\n };\n };\n\n /**\n * Handles incoming server messages.\n */\n const handleMessage = (message: Transport.ServerMessage): void => {\n // Handle internal messages\n if (message.type === \"pong\") {\n handlePong();\n return;\n }\n\n if (message.type === \"auth_result\") {\n handleAuthResult(message.success, message.error);\n return;\n }\n\n // Forward to subscribers\n for (const handler of _messageHandlers) {\n try {\n handler(message);\n } catch {\n // Ignore handler errors\n }\n }\n };\n\n // ==========================================================================\n // Public API\n // ==========================================================================\n\n const transport: Transport.Transport = {\n send: (transaction: Transaction.Transaction): void => {\n const message: Transport.ClientMessage = { type: \"submit\", transaction };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else {\n // Queue message for when we reconnect (works for both reconnecting and disconnected states)\n _messageQueue.push(message);\n }\n },\n\n requestSnapshot: (): void => {\n const message: Transport.ClientMessage = { type: \"request_snapshot\" };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else {\n // Queue for later\n _messageQueue.push(message);\n }\n },\n\n subscribe: (handler: (message: Transport.ServerMessage) => void): (() => void) => {\n _messageHandlers.add(handler);\n return () => {\n _messageHandlers.delete(handler);\n };\n },\n\n connect: async (): Promise<void> => {\n if (_state.type === \"connected\") {\n return;\n }\n\n if (_state.type === \"connecting\" || _state.type === \"authenticating\") {\n // Already connecting - wait for existing promise\n return new Promise((resolve, reject) => {\n const existingResolver = _connectResolver;\n const existingRejecter = _connectRejecter;\n _connectResolver = () => {\n existingResolver?.();\n resolve();\n };\n _connectRejecter = (error) => {\n existingRejecter?.(error);\n reject(error);\n };\n });\n }\n\n return new Promise((resolve, reject) => {\n _connectResolver = resolve;\n _connectRejecter = reject;\n attemptConnection();\n });\n },\n\n disconnect: (): void => {\n // Cancel any pending reconnection\n if (_reconnectTimeoutHandle) {\n clearTimeout(_reconnectTimeoutHandle);\n _reconnectTimeoutHandle = null;\n }\n\n // Reject any pending connect promise\n if (_connectRejecter) {\n _connectRejecter(new WebSocketError(\"Disconnected by user\"));\n _connectResolver = null;\n _connectRejecter = null;\n }\n\n // Clean up\n cleanup();\n _state = { type: \"disconnected\" };\n _reconnectAttempt = 0;\n _messageQueue = [];\n\n emit(onEvent, { type: \"disconnected\", reason: \"User disconnected\" });\n },\n\n isConnected: (): boolean => {\n return _state.type === \"connected\";\n },\n\n // =========================================================================\n // Presence Methods\n // =========================================================================\n\n sendPresenceSet: (data: unknown): void => {\n const message: Transport.ClientMessage = { type: \"presence_set\", data };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else {\n // Remove all set messages from the message queue (only keep latest)\n _messageQueue = _messageQueue.filter((m) => m.type !== \"presence_set\");\n // Add the new presence set message to the queue\n _messageQueue.push(message);\n }\n },\n\n sendPresenceClear: (): void => {\n const message: Transport.ClientMessage = { type: \"presence_clear\" };\n\n if (_state.type === \"connected\") {\n sendRaw(message);\n } else {\n // Remove all clear messages from the message queue\n _messageQueue = _messageQueue.filter((m) => m.type !== \"presence_clear\");\n // Add the new presence clear message to the queue\n _messageQueue.push(message);\n }\n },\n };\n\n return transport;\n};\n"],"mappings":"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"}
|
package/dist/primitives/Tree.cjs
CHANGED
|
@@ -6,9 +6,15 @@ const require_OperationDefinition = require('../OperationDefinition.cjs');
|
|
|
6
6
|
const require_OperationPath = require('../OperationPath.cjs');
|
|
7
7
|
const require_Operation = require('../Operation.cjs');
|
|
8
8
|
const require_FractionalIndex = require('../FractionalIndex.cjs');
|
|
9
|
+
const require_objectWithoutProperties = require('../_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.cjs');
|
|
9
10
|
let effect = require("effect");
|
|
10
11
|
|
|
11
12
|
//#region src/primitives/Tree.ts
|
|
13
|
+
const _excluded = [
|
|
14
|
+
"type",
|
|
15
|
+
"id",
|
|
16
|
+
"children"
|
|
17
|
+
];
|
|
12
18
|
/**
|
|
13
19
|
* Helper to get children sorted by position
|
|
14
20
|
*/
|
|
@@ -133,8 +139,9 @@ var TreePrimitive = class TreePrimitive {
|
|
|
133
139
|
get: () => {
|
|
134
140
|
return getCurrentState();
|
|
135
141
|
},
|
|
136
|
-
set: (
|
|
137
|
-
|
|
142
|
+
set: (input) => {
|
|
143
|
+
const flatState = this._convertNestedToFlat(input, env.generateId);
|
|
144
|
+
env.addOperation(require_Operation.fromDefinition(operationPath, this._opDefinitions.set, flatState));
|
|
138
145
|
},
|
|
139
146
|
root: () => {
|
|
140
147
|
return getCurrentState().find((n) => n.parentId === null);
|
|
@@ -429,7 +436,7 @@ var TreePrimitive = class TreePrimitive {
|
|
|
429
436
|
},
|
|
430
437
|
getInitialState: () => {
|
|
431
438
|
var _rootNodeType$data$_i;
|
|
432
|
-
if (this._schema.
|
|
439
|
+
if (this._schema.defaultInput !== void 0) return this._convertNestedToFlat(this._schema.defaultInput);
|
|
433
440
|
const rootNodeType = this._schema.root;
|
|
434
441
|
const rootData = (_rootNodeType$data$_i = rootNodeType.data._internal.getInitialState()) !== null && _rootNodeType$data$_i !== void 0 ? _rootNodeType$data$_i : {};
|
|
435
442
|
const rootId = crypto.randomUUID();
|
|
@@ -442,6 +449,9 @@ var TreePrimitive = class TreePrimitive {
|
|
|
442
449
|
data: rootData
|
|
443
450
|
}];
|
|
444
451
|
},
|
|
452
|
+
convertSetInputToState: (input) => {
|
|
453
|
+
return this._convertNestedToFlat(input);
|
|
454
|
+
},
|
|
445
455
|
transformOperation: (clientOp, serverOp) => {
|
|
446
456
|
const clientPath = clientOp.path;
|
|
447
457
|
const serverPath = serverOp.path;
|
|
@@ -510,9 +520,9 @@ var TreePrimitive = class TreePrimitive {
|
|
|
510
520
|
required() {
|
|
511
521
|
return new TreePrimitive(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, this._schema), {}, { required: true }));
|
|
512
522
|
}
|
|
513
|
-
/** Set a default value for this tree */
|
|
514
|
-
default(
|
|
515
|
-
return new TreePrimitive(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, this._schema), {}, {
|
|
523
|
+
/** Set a default value for this tree (nested format) */
|
|
524
|
+
default(defaultInput) {
|
|
525
|
+
return new TreePrimitive(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, this._schema), {}, { defaultInput }));
|
|
516
526
|
}
|
|
517
527
|
/** Get the root node type */
|
|
518
528
|
get root() {
|
|
@@ -561,11 +571,49 @@ var TreePrimitive = class TreePrimitive {
|
|
|
561
571
|
const parentNodePrimitive = this._getNodeTypePrimitive(parentType);
|
|
562
572
|
if (!parentNodePrimitive.isChildAllowed(childType)) throw new require_shared.ValidationError(`Node type "${childType}" is not allowed as a child of "${parentType}". Allowed types: ${parentNodePrimitive.children.map((c) => c.type).join(", ") || "none"}`);
|
|
563
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Convert a nested TreeNodeSetInput to flat TreeState format.
|
|
576
|
+
* Recursively processes nodes, generating IDs and positions as needed.
|
|
577
|
+
*
|
|
578
|
+
* @param input - The root nested input to convert
|
|
579
|
+
* @param generateId - Optional ID generator (defaults to crypto.randomUUID)
|
|
580
|
+
* @returns Flat TreeState array
|
|
581
|
+
*/
|
|
582
|
+
_convertNestedToFlat(input, generateId = () => crypto.randomUUID()) {
|
|
583
|
+
const result = [];
|
|
584
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
585
|
+
const processNode = (nodeInput, parentId, parentType, leftPos, rightPos) => {
|
|
586
|
+
var _nodeInput$id;
|
|
587
|
+
this._validateChildType(parentType, nodeInput.type);
|
|
588
|
+
const nodePrimitive = this._getNodeTypePrimitive(nodeInput.type);
|
|
589
|
+
const id = (_nodeInput$id = nodeInput.id) !== null && _nodeInput$id !== void 0 ? _nodeInput$id : generateId();
|
|
590
|
+
if (seenIds.has(id)) throw new require_shared.ValidationError(`Duplicate node ID: ${id}`);
|
|
591
|
+
seenIds.add(id);
|
|
592
|
+
const pos = generateTreePosBetween(leftPos, rightPos);
|
|
593
|
+
const { type: _type, id: _id, children } = nodeInput, dataFields = require_objectWithoutProperties._objectWithoutProperties(nodeInput, _excluded);
|
|
594
|
+
const mergedData = require_shared.applyDefaults(nodePrimitive.data, dataFields);
|
|
595
|
+
result.push({
|
|
596
|
+
id,
|
|
597
|
+
type: nodeInput.type,
|
|
598
|
+
parentId,
|
|
599
|
+
pos,
|
|
600
|
+
data: mergedData
|
|
601
|
+
});
|
|
602
|
+
let prevChildPos = null;
|
|
603
|
+
for (let i = 0; i < children.length; i++) {
|
|
604
|
+
const childInput = children[i];
|
|
605
|
+
processNode(childInput, id, nodeInput.type, prevChildPos, null);
|
|
606
|
+
prevChildPos = result[result.length - 1].pos;
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
processNode(input, null, null, null, null);
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
564
612
|
};
|
|
565
613
|
/** Creates a new TreePrimitive with the given root node type */
|
|
566
614
|
const Tree = (options) => new TreePrimitive({
|
|
567
615
|
required: false,
|
|
568
|
-
|
|
616
|
+
defaultInput: void 0,
|
|
569
617
|
root: options.root,
|
|
570
618
|
validators: []
|
|
571
619
|
});
|