@athenaintel/react 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +61 -0
  2. package/dist/agent-runtime/src/athena-asset-drag.cjs +44 -0
  3. package/dist/agent-runtime/src/athena-asset-drag.cjs.map +1 -0
  4. package/dist/agent-runtime/src/athena-asset-drag.js +44 -0
  5. package/dist/agent-runtime/src/athena-asset-drag.js.map +1 -0
  6. package/dist/agent-runtime/src/config-mutations.cjs +59 -0
  7. package/dist/agent-runtime/src/config-mutations.cjs.map +1 -0
  8. package/dist/agent-runtime/src/config-mutations.js +59 -0
  9. package/dist/agent-runtime/src/config-mutations.js.map +1 -0
  10. package/dist/agent-runtime/src/generated/runconfig-contract.cjs +6 -0
  11. package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  12. package/dist/agent-runtime/src/generated/runconfig-contract.js +6 -0
  13. package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  14. package/dist/agent-runtime/src/quota-safe-local-storage-session.cjs +48 -0
  15. package/dist/agent-runtime/src/quota-safe-local-storage-session.cjs.map +1 -1
  16. package/dist/agent-runtime/src/quota-safe-local-storage-session.js +49 -1
  17. package/dist/agent-runtime/src/quota-safe-local-storage-session.js.map +1 -1
  18. package/dist/agent-runtime/src/tool-metadata.cjs +25 -6
  19. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  20. package/dist/agent-runtime/src/tool-metadata.js +25 -6
  21. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  22. package/dist/agent-runtime/src/use-deep-agent-config.cjs +117 -1
  23. package/dist/agent-runtime/src/use-deep-agent-config.cjs.map +1 -1
  24. package/dist/agent-runtime/src/use-deep-agent-config.js +118 -2
  25. package/dist/agent-runtime/src/use-deep-agent-config.js.map +1 -1
  26. package/dist/assets/AthenaAssetEmbed.cjs +6 -1
  27. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  28. package/dist/assets/AthenaAssetEmbed.d.ts +2 -0
  29. package/dist/assets/AthenaAssetEmbed.js +7 -2
  30. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  31. package/dist/assets/asset-embed-messages.cjs +6 -0
  32. package/dist/assets/asset-embed-messages.cjs.map +1 -1
  33. package/dist/assets/asset-embed-messages.d.ts +7 -0
  34. package/dist/assets/asset-embed-messages.js +6 -0
  35. package/dist/assets/asset-embed-messages.js.map +1 -1
  36. package/dist/chat/AthenaChat.cjs +41 -26
  37. package/dist/chat/AthenaChat.cjs.map +1 -1
  38. package/dist/chat/AthenaChat.js +41 -26
  39. package/dist/chat/AthenaChat.js.map +1 -1
  40. package/dist/chat/super-grouping/SuperGroupingCard.cjs +31 -11
  41. package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
  42. package/dist/chat/super-grouping/SuperGroupingCard.d.ts +14 -3
  43. package/dist/chat/super-grouping/SuperGroupingCard.js +32 -12
  44. package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
  45. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +80 -53
  46. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
  47. package/dist/chat/super-grouping/SuperGroupingTranscript.d.ts +2 -2
  48. package/dist/chat/super-grouping/SuperGroupingTranscript.js +81 -54
  49. package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
  50. package/dist/chat/super-grouping/activity-timeline.cjs +19 -4
  51. package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
  52. package/dist/chat/super-grouping/activity-timeline.d.ts +20 -1
  53. package/dist/chat/super-grouping/activity-timeline.js +19 -4
  54. package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
  55. package/dist/chat/super-grouping/index.cjs +2 -0
  56. package/dist/chat/super-grouping/index.cjs.map +1 -1
  57. package/dist/chat/super-grouping/index.d.ts +1 -1
  58. package/dist/chat/super-grouping/index.js +3 -1
  59. package/dist/chat/super-grouping/toolkit-map.cjs +1 -0
  60. package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
  61. package/dist/chat/super-grouping/toolkit-map.js +1 -0
  62. package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
  63. package/dist/chat-ui/composer/tiptap-composer.cjs +40 -4
  64. package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
  65. package/dist/chat-ui/composer/tiptap-composer.js +40 -4
  66. package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
  67. package/dist/chat-ui/mentions/mention-chip.cjs +29 -2
  68. package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
  69. package/dist/chat-ui/mentions/mention-chip.d.ts +2 -5
  70. package/dist/chat-ui/mentions/mention-chip.js +30 -3
  71. package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
  72. package/dist/chat-ui/mentions/mention-extension.cjs +1 -1
  73. package/dist/chat-ui/mentions/mention-extension.cjs.map +1 -1
  74. package/dist/chat-ui/mentions/mention-extension.js +1 -1
  75. package/dist/chat-ui/mentions/mention-extension.js.map +1 -1
  76. package/dist/chat-ui/mentions/use-account-workspace-id.cjs +61 -0
  77. package/dist/chat-ui/mentions/use-account-workspace-id.cjs.map +1 -0
  78. package/dist/chat-ui/mentions/use-account-workspace-id.d.ts +3 -0
  79. package/dist/chat-ui/mentions/use-account-workspace-id.js +61 -0
  80. package/dist/chat-ui/mentions/use-account-workspace-id.js.map +1 -0
  81. package/dist/chat-ui/mentions/use-member-avatar.cjs +75 -0
  82. package/dist/chat-ui/mentions/use-member-avatar.cjs.map +1 -0
  83. package/dist/chat-ui/mentions/use-member-avatar.d.ts +2 -0
  84. package/dist/chat-ui/mentions/use-member-avatar.js +75 -0
  85. package/dist/chat-ui/mentions/use-member-avatar.js.map +1 -0
  86. package/dist/collab/client.cjs +80 -19
  87. package/dist/collab/client.cjs.map +1 -1
  88. package/dist/collab/client.d.ts +22 -0
  89. package/dist/collab/client.js +80 -19
  90. package/dist/collab/client.js.map +1 -1
  91. package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
  92. package/dist/diagnostics/athena-diagnostics.d.ts +3 -1
  93. package/dist/diagnostics/athena-diagnostics.js.map +1 -1
  94. package/dist/index.cjs +4 -0
  95. package/dist/index.cjs.map +1 -1
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.js +5 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/provider/AthenaProvider.cjs +34 -4
  100. package/dist/provider/AthenaProvider.cjs.map +1 -1
  101. package/dist/provider/AthenaProvider.d.ts +7 -1
  102. package/dist/provider/AthenaProvider.js +36 -6
  103. package/dist/provider/AthenaProvider.js.map +1 -1
  104. package/dist/react/package.json.cjs +1 -1
  105. package/dist/react/package.json.js +1 -1
  106. package/dist/runtime/StatewireModelLaneSync.cjs +96 -0
  107. package/dist/runtime/StatewireModelLaneSync.cjs.map +1 -0
  108. package/dist/runtime/StatewireModelLaneSync.d.ts +45 -0
  109. package/dist/runtime/StatewireModelLaneSync.js +96 -0
  110. package/dist/runtime/StatewireModelLaneSync.js.map +1 -0
  111. package/dist/runtime/statewire-lifecycle.cjs +5 -2
  112. package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
  113. package/dist/runtime/statewire-lifecycle.d.ts +5 -0
  114. package/dist/runtime/statewire-lifecycle.js +5 -2
  115. package/dist/runtime/statewire-lifecycle.js.map +1 -1
  116. package/dist/runtime/statewire-model-lane.cjs +28 -0
  117. package/dist/runtime/statewire-model-lane.cjs.map +1 -0
  118. package/dist/runtime/statewire-model-lane.d.ts +64 -0
  119. package/dist/runtime/statewire-model-lane.js +28 -0
  120. package/dist/runtime/statewire-model-lane.js.map +1 -0
  121. package/dist/runtime/statewire-run-config.cjs +16 -2
  122. package/dist/runtime/statewire-run-config.cjs.map +1 -1
  123. package/dist/runtime/statewire-run-config.d.ts +10 -0
  124. package/dist/runtime/statewire-run-config.js +16 -2
  125. package/dist/runtime/statewire-run-config.js.map +1 -1
  126. package/dist/runtime/statewire-server-lane-context.cjs +24 -0
  127. package/dist/runtime/statewire-server-lane-context.cjs.map +1 -0
  128. package/dist/runtime/statewire-server-lane-context.d.ts +30 -0
  129. package/dist/runtime/statewire-server-lane-context.js +24 -0
  130. package/dist/runtime/statewire-server-lane-context.js.map +1 -0
  131. package/dist/runtime/useAthenaStatewireRuntime.cjs +38 -18
  132. package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
  133. package/dist/runtime/useAthenaStatewireRuntime.d.ts +8 -1
  134. package/dist/runtime/useAthenaStatewireRuntime.js +39 -19
  135. package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
  136. package/dist/styles.css +1 -1
  137. package/dist/threads/api.cjs +2 -1
  138. package/dist/threads/api.cjs.map +1 -1
  139. package/dist/threads/api.d.ts +1 -0
  140. package/dist/threads/api.js +2 -1
  141. package/dist/threads/api.js.map +1 -1
  142. package/dist/threads/use-athena-thread-model.cjs +14 -0
  143. package/dist/threads/use-athena-thread-model.cjs.map +1 -0
  144. package/dist/threads/use-athena-thread-model.d.ts +22 -0
  145. package/dist/threads/use-athena-thread-model.js +14 -0
  146. package/dist/threads/use-athena-thread-model.js.map +1 -0
  147. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","sources":["../../src/collab/client.ts"],"sourcesContent":["/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n *\n * A host that keeps a local copy of the document (`localCopy`) gets it applied\n * before the first connection, keeps its unacknowledged edits waiting across\n * sessions, and can hold a connection that would send edits made without one\n * until it has checked them against the server (`beforeConnect`). Offline, a\n * mint that fails for want of the network is retried (`retryMintError`)\n * instead of ending the handle, and past `offlineGateAfterMs` without a socket\n * the handle reconnects itself, re-minting each time, so every reconnect after\n * a real outage re-checks access and passes the host's gate.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\nimport { MAX_CONSECUTIVE_REMINTS, MIN_REFRESH_DELAY_MS, refreshDelayMs } from './token-refresh';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /**\n * Whether the initial server sync has completed for the current connection.\n * `status === 'connected'` fires before the server state arrives, so a doc\n * read immediately after connect can still be empty — gate first reads on\n * this (or `waitForSync`).\n */\n readonly synced: boolean;\n /**\n * Whether every change made to this document has reached the server: `false` from a local edit\n * until the server sends it back (`@y/websocket`'s `syncStatus.localUpdatesSynced`; yhub echoes\n * every update to its sender). Kept across reconnects and re-mints: a replaced provider hands its\n * unconfirmed edits to the next, and an edit made between providers waits for one. A host that\n * closes the document while this is `false` loses those edits, unless a local copy keeps them.\n */\n readonly localChangesSynced: boolean;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /**\n * Whether the document holds what the host's local copy stored (always `true` without one).\n * Mount readers after it: until then the document is still empty.\n */\n readonly prepared: boolean;\n /**\n * A connection the host's `beforeConnect` held: the handle waits, sending nothing, until the\n * host calls `resume()` (or destroys the handle).\n */\n readonly held: boolean;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n /**\n * Resolve once the initial server sync completes; reject on timeout, on a\n * terminal connection state, or if the handle is destroyed first.\n */\n waitForSync: (timeoutMs?: number) => Promise<void>;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session. Without a connection it\n * reconnects now.\n */\n refreshNow: () => Promise<void>;\n /** Connect past a hold, without asking `beforeConnect` again. */\n resume: () => void;\n /** Local edits the server has not echoed yet (handed between providers too), or `null`. */\n unconfirmedEdits: () => Y.ContentIds | null;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\n/**\n * Issues a room-bound Keryx token for one asset. The default calls the public\n * `/collab-token` route with the handle's `auth`; a host with its own trusted\n * mint (a desktop app's main process holding the user's key) passes one\n * instead, and the handle uses it for the first mint and every re-mint.\n */\nexport type CollabMint = (params: {\n assetId: string;\n access: 'view' | 'edit';\n}) => Promise<CollabTokenResponse>;\n\n/** What a host's local copy reads of the handle's transport. */\nexport interface GenericDocTransportView {\n /** True for a transaction the transport applied (the server's state, a peer's edit). */\n isRemoteOrigin: (origin: unknown) => boolean;\n /** Local edits the server has not echoed yet, or `null`. */\n getUnconfirmed: () => Y.ContentIds | null;\n /** Connected, past the initial sync, and nothing unconfirmed. */\n isFullySynced: () => boolean;\n}\n\n/** What `beforeConnect` is asked about. */\nexport interface GenericDocConnectGate {\n /** The grant the connection would use (its access is the server's current verdict). */\n grant: CollabTokenResponse;\n /** The handle has not connected yet. */\n firstConnection: boolean;\n /** How long the handle had been without a connection, when it had one before. */\n offlineForMs: number | null;\n /** Local edits made without a connection (or inherited from an earlier session) wait to be sent. */\n hasUnsyncedEdits: boolean;\n}\n\n/** A host's local copy of the document (`@athenaintel/local-copy` behind a host's glue). */\nexport interface GenericDocLocalCopy {\n /**\n * Called once, before the first mint: apply the stored copy to `doc` and return the local edits\n * it holds that the server never acknowledged. The handle sends them and waits for their echo\n * like its own. A failure here must resolve `{ unsyncedEdits: null }`: the handle then runs\n * without a copy.\n */\n prepare: (params: {\n doc: Y.Doc;\n transport: GenericDocTransportView;\n }) => Promise<{ unsyncedEdits: Y.ContentIds | null }>;\n /** The handle's unconfirmed edits or sync state changed. */\n transportChanged: () => void;\n /**\n * Asked after each mint and before the connection it would open: the first connection, a\n * reconnect after an outage longer than `offlineGateAfterMs`, and a re-mint after a revocation\n * (never a scheduled token refresh of a live connection, never past `resume()`). The host\n * checks what it must before anything is sent (the document still opens here; access; edits\n * made without a connection against the server's). `'hold'` keeps the handle from connecting\n * until `resume()`; a throw counts as \"not yet\" and is asked again on the next attempt.\n */\n beforeConnect?: (gate: GenericDocConnectGate) => Promise<'connect' | 'hold'>;\n}\n\n/** After this long without a socket, the handle reconnects itself (re-mint, gate) instead of the provider. */\nexport const DEFAULT_OFFLINE_GATE_AFTER_MS = 15_000;\n\n/** Retry delays for a mint that failed for want of the network. */\nconst MINT_RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000, 30_000, 60_000];\n\n/**\n * The default `retryMintError`: a network failure, a timeout, a rate limit or a server error is\n * worth retrying; a refusal (401/403/404) is the server's verdict and ends the handle.\n */\nexport function isRetryableMintError(error: unknown): boolean {\n if (error instanceof CollabTokenError) {\n return error.status >= 500 || error.status === 408 || error.status === 429;\n }\n return error instanceof TypeError || (error instanceof DOMException && error.name === 'TimeoutError');\n}\n\nfunction resolveMint({ auth, mint }: { auth?: CollabAuth; mint?: CollabMint }): CollabMint {\n if (mint) return mint;\n if (!auth) throw new Error('connectGenericDoc needs `auth` or `mint`');\n return ({ assetId, access }) => mintCollabToken({ auth, assetId, access });\n}\n\nexport function connectGenericDoc(args: {\n assetId: string;\n /** Credentials for the default mint; not used when `mint` is given. */\n auth?: CollabAuth;\n access?: 'view' | 'edit';\n mint?: CollabMint;\n /** A local copy of the document the host keeps (see {@link GenericDocLocalCopy}). */\n localCopy?: GenericDocLocalCopy;\n /** Whether a failed mint is worth retrying; defaults to {@link isRetryableMintError}. */\n retryMintError?: (error: unknown) => boolean;\n /** See {@link DEFAULT_OFFLINE_GATE_AFTER_MS}. */\n offlineGateAfterMs?: number;\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const mint = resolveMint({ auth: args.auth, mint: args.mint });\n const localCopy = args.localCopy;\n const retryMintError = args.retryMintError ?? isRetryableMintError;\n const offlineGateAfterMs = args.offlineGateAfterMs ?? DEFAULT_OFFLINE_GATE_AFTER_MS;\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let offlineTimer: ReturnType<typeof setTimeout> | null = null;\n /** The provider whose socket was still opening, with nothing to send, at the threshold. */\n let openingPastThreshold: WebsocketProvider | null = null;\n let mintRetries = 0;\n let destroyed = false;\n let status: GenericDocStatus = 'connecting';\n let synced = false;\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n let prepared = localCopy === undefined;\n let held = false;\n let heldGrant: CollabTokenResponse | null = null;\n let everConnected = false;\n let disconnectedSince: number | null = null;\n /** Bumped by every connect attempt: an attempt that finds it changed after an await stops. */\n let connectSeq = 0;\n /** Local edits no provider holds: handed on by a replaced provider, or made with none. */\n let carriedUpdates: WebsocketProvider['unconfirmedUpdates'] = [];\n let localChangesSynced = true;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const unconfirmedEdits = (): Y.ContentIds | null => {\n const all = [...carriedUpdates, ...(provider?.unconfirmedUpdates ?? [])].map(\n (entry) => entry.ids as Y.ContentIds\n );\n if (all.length === 0) return null;\n return all.length === 1 ? (all[0] as Y.ContentIds) : Y.mergeContentIds(all);\n };\n\n const transport: GenericDocTransportView = {\n isRemoteOrigin: (origin) => origin instanceof WebsocketProvider,\n getUnconfirmed: unconfirmedEdits,\n isFullySynced: () => status === 'connected' && synced && localChangesSynced,\n };\n\n const transportChanged = () => {\n localCopy?.transportChanged();\n };\n\n const refreshLocalChangesSynced = () => {\n const next = carriedUpdates.length === 0 && (provider?.unconfirmedUpdates.length ?? 0) === 0;\n if (next !== localChangesSynced) {\n localChangesSynced = next;\n notify();\n }\n // After the flag: the copy reads `isFullySynced()` in its callback.\n transportChanged();\n };\n\n // An edit made while no provider exists (a re-mint in flight, offline) is sent by the next one,\n // which takes it as unconfirmed. The local copy's own load is not an edit.\n doc.on('update', (update: Uint8Array, origin: unknown) => {\n if (!prepared || provider !== null || destroyed || origin instanceof WebsocketProvider) return;\n carriedUpdates = [\n ...carriedUpdates,\n { created: Date.now(), ids: Y.createContentIdsFromUpdate(update) },\n ];\n refreshLocalChangesSynced();\n });\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n const wasConnected = status === 'connected';\n status = next;\n error = err;\n if (next === 'connected') {\n everConnected = true;\n disconnectedSince = null;\n clearOfflineTimer();\n } else if (wasConnected || (everConnected && disconnectedSince === null)) {\n disconnectedSince = Date.now();\n }\n transportChanged();\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const clearRetryTimer = () => {\n if (retryTimer !== null) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n };\n\n function clearOfflineTimer() {\n if (offlineTimer !== null) {\n clearTimeout(offlineTimer);\n offlineTimer = null;\n }\n }\n\n const teardownProvider = () => {\n clearRefreshTimer();\n // A future provider must complete its own initial sync; the doc keeps its\n // content either way. Subscribers must observe the flip immediately —\n // waiting for the next provider event would let them act on synced=true\n // across the replacement gap.\n if (synced) {\n synced = false;\n notify();\n }\n if (provider) {\n // Its unconfirmed edits outlive it: the next provider sends them and waits for their echo.\n carriedUpdates = [...carriedUpdates, ...provider.unconfirmedUpdates];\n provider.destroy();\n provider = null;\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n /** A failed attempt that is worth retrying: try again later, or as soon as the network returns. */\n const scheduleRetry = () => {\n clearRetryTimer();\n const delay = MINT_RETRY_DELAYS_MS[Math.min(mintRetries, MINT_RETRY_DELAYS_MS.length - 1)];\n mintRetries += 1;\n retryTimer = setTimeout(() => {\n retryTimer = null;\n void connect();\n }, delay);\n };\n\n const onBrowserOnline = () => {\n // Only a handle waiting to retry: a refused or permanently closed one stays stopped.\n if (destroyed || status === 'closed' || status === 'error' || provider !== null || held) return;\n mintRetries = 0;\n clearRetryTimer();\n void connect();\n };\n if (typeof window !== 'undefined') window.addEventListener('online', onBrowserOnline);\n\n /**\n * Past the gate's threshold without a socket, the handle reconnects itself (re-mint, gate). A\n * socket still being opened with nothing waiting to be sent is left to finish (a slow network\n * is not restarted every threshold), and the handle looks again a threshold later; if it opens\n * holding edits made since, they go through the gate first (`status`).\n */\n const armOfflineTakeover = () => {\n if (offlineTimer !== null || !localCopy?.beforeConnect) return;\n offlineTimer = setTimeout(() => {\n offlineTimer = null;\n if (destroyed || status === 'connected' || provider === null) return;\n if (provider.wsconnecting && !hasUnsyncedLocalEdits()) {\n openingPastThreshold = provider;\n armOfflineTakeover();\n return;\n }\n teardownProvider();\n void connect();\n }, offlineGateAfterMs);\n };\n\n const hasUnsyncedLocalEdits = () =>\n carriedUpdates.length > 0 || (provider?.unconfirmedUpdates.length ?? 0) > 0;\n\n\n const connect = async (opts?: {\n scheduledRefresh?: boolean;\n skipGate?: boolean;\n grant?: CollabTokenResponse;\n }): Promise<void> => {\n if (destroyed) return;\n await preparing;\n if (destroyed) return;\n const seq = ++connectSeq;\n // Only a live connection is refreshed in place (and skips the host's gate); a refresh while the\n // socket is down is a reconnect like any other.\n const scheduledRefresh = opts?.scheduledRefresh === true && status === 'connected';\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n clearRetryTimer();\n let grant: CollabTokenResponse;\n if (opts?.grant && opts.grant.expires_at_ms - Date.now() > MIN_REFRESH_DELAY_MS) {\n grant = opts.grant;\n } else {\n try {\n grant = await mint({ assetId: args.assetId, access });\n } catch (err) {\n if (destroyed || seq !== connectSeq) return;\n if (scheduledRefresh) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n if (retryMintError(err)) {\n // Offline (or the mint's server is): keep the document and try again.\n if (status !== 'disconnected') setStatus('disconnected');\n scheduleRetry();\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible).\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n }\n if (destroyed || seq !== connectSeq) return;\n mintRetries = 0;\n accessType = grant.access_type;\n const beforeConnect = localCopy?.beforeConnect;\n if (beforeConnect && !scheduledRefresh && !opts?.skipGate) {\n const firstConnection = !everConnected;\n const offlineForMs = disconnectedSince === null ? null : Date.now() - disconnectedSince;\n let decision: 'connect' | 'hold';\n try {\n decision = await beforeConnect({\n grant,\n firstConnection,\n offlineForMs,\n hasUnsyncedEdits: hasUnsyncedLocalEdits(),\n });\n } catch {\n if (destroyed || seq !== connectSeq) return;\n if (status !== 'disconnected') setStatus('disconnected');\n scheduleRetry();\n return;\n }\n if (destroyed || seq !== connectSeq) return;\n if (decision === 'hold') {\n held = true;\n heldGrant = grant;\n if (status !== 'disconnected') setStatus('disconnected');\n else notify();\n return;\n }\n }\n if (scheduledRefresh) {\n teardownProvider();\n }\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n // BroadcastChannel would sync same-origin clients locally, bypassing the\n // server's permission enforcement (a read-only tab could write into an\n // editor tab's doc). Permissioned docs must round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n nextProvider.unconfirmedUpdates = [...carriedUpdates, ...nextProvider.unconfirmedUpdates];\n carriedUpdates = [];\n refreshLocalChangesSynced();\n\n nextProvider.on('sync-status', () => {\n if (provider !== nextProvider) return;\n refreshLocalChangesSynced();\n });\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (\n event.status === 'connected' &&\n openingPastThreshold === nextProvider &&\n hasUnsyncedLocalEdits()\n ) {\n // Opened after the threshold holding edits made meanwhile: before its handshake sends\n // them (`@y/websocket` reports `connected` first), they go through the gate.\n teardownProvider();\n void connect();\n return;\n }\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n // `@y/websocket` reports `disconnected` only for a socket that had opened: one that never\n // opens (the server unreachable while the mint goes through) only ever reports `connecting`,\n // and is just as offline.\n if (event.status !== 'connected') armOfflineTakeover();\n });\n // Its first `connecting` came from its constructor, before this listener existed.\n if (!nextProvider.wsconnected) armOfflineTakeover();\n\n nextProvider.on('sync', (state: boolean) => {\n if (provider !== nextProvider) return;\n if (synced !== state) {\n synced = state;\n transportChanged();\n notify();\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it, until the offline takeover.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n const preparing: Promise<void> = localCopy\n ? localCopy\n .prepare({ doc, transport })\n .catch(() => ({ unsyncedEdits: null }))\n .then(({ unsyncedEdits }) => {\n if (destroyed) return;\n if (unsyncedEdits && !(unsyncedEdits.inserts.isEmpty() && unsyncedEdits.deletes.isEmpty())) {\n carriedUpdates = [...carriedUpdates, { created: Date.now(), ids: unsyncedEdits }];\n }\n prepared = true;\n refreshLocalChangesSynced();\n notify();\n })\n : Promise.resolve();\n\n void connect();\n\n const waitForSync = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const terminal = (): Error | null => {\n if (destroyed) return new Error('Handle destroyed before initial sync');\n if (status === 'closed' || status === 'error') {\n return error ?? new Error(`Connection ${status} before initial sync`);\n }\n return null;\n };\n if (synced) {\n resolve();\n return;\n }\n const immediate = terminal();\n if (immediate) {\n reject(immediate);\n return;\n }\n const cleanup = () => {\n clearTimeout(timer);\n listeners.delete(listener);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));\n }, timeoutMs);\n const listener = () => {\n if (synced) {\n cleanup();\n resolve();\n return;\n }\n const err = terminal();\n if (err) {\n cleanup();\n reject(err);\n }\n };\n listeners.add(listener);\n });\n\n return {\n doc,\n get status() {\n return status;\n },\n get synced() {\n return synced;\n },\n get localChangesSynced() {\n return localChangesSynced;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get prepared() {\n return prepared;\n },\n get held() {\n return held;\n },\n get version() {\n return version;\n },\n waitForSync,\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => {\n if (held) return Promise.resolve();\n return provider ? connect({ scheduledRefresh: true }) : connect();\n },\n resume: () => {\n if (!held || destroyed) return;\n held = false;\n const grant = heldGrant;\n heldGrant = null;\n notify();\n void connect({ skipGate: true, grant: grant ?? undefined });\n },\n unconfirmedEdits,\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n clearRetryTimer();\n clearOfflineTimer();\n if (typeof window !== 'undefined') window.removeEventListener('online', onBrowserOnline);\n status = 'closed';\n // Final notification so pending waitForSync callers reject instead of\n // hanging on listeners that are about to be dropped.\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n"],"names":["CollabTokenError","mint","mintCollabToken","Y","WebsocketProvider","refreshDelayMs","MIN_REFRESH_DELAY_MS","classifyClose","MAX_CONSECUTIVE_REMINTS"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","sources":["../../src/collab/client.ts"],"sourcesContent":["/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n *\n * A host that keeps a local copy of the document (`localCopy`) gets it applied\n * before the first connection, keeps its unacknowledged edits waiting across\n * sessions, and can hold a connection that would send edits made without one\n * until it has checked them against the server (`beforeConnect`). Offline, a\n * mint that fails for want of the network is retried (`retryMintError`)\n * instead of ending the handle, and past `offlineGateAfterMs` without a socket\n * the handle reconnects itself, re-minting each time, so every reconnect after\n * a real outage re-checks access and passes the host's gate.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\nimport { MAX_CONSECUTIVE_REMINTS, MIN_REFRESH_DELAY_MS, refreshDelayMs } from './token-refresh';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /**\n * Whether the initial server sync has completed for the current connection.\n * `status === 'connected'` fires before the server state arrives, so a doc\n * read immediately after connect can still be empty — gate first reads on\n * this (or `waitForSync`).\n */\n readonly synced: boolean;\n /**\n * Whether every change made to this document has reached the server: `false` from a local edit\n * until the server sends it back (`@y/websocket`'s `syncStatus.localUpdatesSynced`; yhub echoes\n * every update to its sender). Kept across reconnects and re-mints: a replaced provider hands its\n * unconfirmed edits to the next, and an edit made between providers waits for one. A host that\n * closes the document while this is `false` loses those edits, unless a local copy keeps them.\n */\n readonly localChangesSynced: boolean;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /**\n * Whether the document holds what the host's local copy stored (always `true` without one).\n * Mount readers after it: until then the document is still empty.\n */\n readonly prepared: boolean;\n /** Whether this host allows a live connection. Disabled documents remain editable locally. */\n readonly connectionEnabled: boolean;\n /**\n * A connection the host's `beforeConnect` held: the handle waits, sending nothing, until the\n * host calls `resume()` (or destroys the handle).\n */\n readonly held: boolean;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n /**\n * Resolve once the initial server sync completes; reject on timeout, on a\n * terminal connection state, or if the handle is destroyed first.\n */\n waitForSync: (timeoutMs?: number) => Promise<void>;\n /**\n * Resolve once connected, initially synced, and every local change has been acknowledged,\n * including any edits the local-copy hook restores after sync. The host must prevent further\n * editing while saving a checkpoint. Reject on timeout, a disabled connection, or a terminal state.\n */\n waitForAcknowledged: (timeoutMs?: number) => Promise<void>;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session. Without a connection it\n * reconnects now.\n */\n refreshNow: () => Promise<void>;\n /**\n * Disable all connections without losing the document or its pending edits. Enabling mints a\n * fresh grant and runs the host's beforeConnect gate; it never bypasses a permission or review.\n */\n setConnectionEnabled: (enabled: boolean) => void;\n /** Connect past a hold, without asking `beforeConnect` again. */\n resume: () => void;\n /** Local edits the server has not echoed yet (handed between providers too), or `null`. */\n unconfirmedEdits: () => Y.ContentIds | null;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\n/**\n * Issues a room-bound Keryx token for one asset. The default calls the public\n * `/collab-token` route with the handle's `auth`; a host with its own trusted\n * mint (a desktop app's main process holding the user's key) passes one\n * instead, and the handle uses it for the first mint and every re-mint.\n */\nexport type CollabMint = (params: {\n assetId: string;\n access: 'view' | 'edit';\n}) => Promise<CollabTokenResponse>;\n\n/** What a host's local copy reads of the handle's transport. */\nexport interface GenericDocTransportView {\n /** True for a transaction the transport applied (the server's state, a peer's edit). */\n isRemoteOrigin: (origin: unknown) => boolean;\n /** Local edits the server has not echoed yet, or `null`. */\n getUnconfirmed: () => Y.ContentIds | null;\n /** Connected, past the initial sync, and nothing unconfirmed. */\n isFullySynced: () => boolean;\n}\n\n/** What `beforeConnect` is asked about. */\nexport interface GenericDocConnectGate {\n /** The grant the connection would use (its access is the server's current verdict). */\n grant: CollabTokenResponse;\n /** The handle has not connected yet. */\n firstConnection: boolean;\n /** How long the handle had been without a connection, when it had one before. */\n offlineForMs: number | null;\n /** Local edits made without a connection (or inherited from an earlier session) wait to be sent. */\n hasUnsyncedEdits: boolean;\n}\n\n/** A host's local copy of the document (`@athenaintel/local-copy` behind a host's glue). */\nexport interface GenericDocLocalCopy {\n /**\n * Called once, before the first mint: apply the stored copy to `doc` and return the local edits\n * it holds that the server never acknowledged. The handle sends them and waits for their echo\n * like its own. A failure here must resolve `{ unsyncedEdits: null }`: the handle then runs\n * without a copy.\n */\n prepare: (params: {\n doc: Y.Doc;\n transport: GenericDocTransportView;\n }) => Promise<{ unsyncedEdits: Y.ContentIds | null }>;\n /** The handle's unconfirmed edits or sync state changed. */\n transportChanged: () => void;\n /**\n * An explicit host pause/resume policy changed. Invalidate decisions that must be checked afresh\n * before a later manual save. A pause runs after its provider is destroyed and its status is\n * disconnected, so edits restored by the hook stay local. A resume runs before minting.\n * Network retries and an immediate conflict `resume()` do not call it.\n */\n connectionPolicyChanged?: (enabled: boolean) => void;\n /**\n * Asked after each mint and before the connection it would open: the first connection, a\n * reconnect after an outage longer than `offlineGateAfterMs`, and a re-mint after a revocation\n * (never a scheduled token refresh of a live connection, never past `resume()`). The host\n * checks what it must before anything is sent (the document still opens here; access; edits\n * made without a connection against the server's). `'hold'` keeps the handle from connecting\n * until `resume()`; a throw counts as \"not yet\" and is asked again on the next attempt.\n */\n beforeConnect?: (gate: GenericDocConnectGate) => Promise<'connect' | 'hold'>;\n}\n\n/** After this long without a socket, the handle reconnects itself (re-mint, gate) instead of the provider. */\nexport const DEFAULT_OFFLINE_GATE_AFTER_MS = 15_000;\n\n/** Retry delays for a mint that failed for want of the network. */\nconst MINT_RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000, 30_000, 60_000];\n\n/**\n * The default `retryMintError`: a network failure, a timeout, a rate limit or a server error is\n * worth retrying; a refusal (401/403/404) is the server's verdict and ends the handle.\n */\nexport function isRetryableMintError(error: unknown): boolean {\n if (error instanceof CollabTokenError) {\n return error.status >= 500 || error.status === 408 || error.status === 429;\n }\n return error instanceof TypeError || (error instanceof DOMException && error.name === 'TimeoutError');\n}\n\nfunction resolveMint({ auth, mint }: { auth?: CollabAuth; mint?: CollabMint }): CollabMint {\n if (mint) return mint;\n if (!auth) throw new Error('connectGenericDoc needs `auth` or `mint`');\n return ({ assetId, access }) => mintCollabToken({ auth, assetId, access });\n}\n\nexport function connectGenericDoc(args: {\n assetId: string;\n /** Credentials for the default mint; not used when `mint` is given. */\n auth?: CollabAuth;\n access?: 'view' | 'edit';\n mint?: CollabMint;\n /** A local copy of the document the host keeps (see {@link GenericDocLocalCopy}). */\n localCopy?: GenericDocLocalCopy;\n /** Whether a failed mint is worth retrying; defaults to {@link isRetryableMintError}. */\n retryMintError?: (error: unknown) => boolean;\n /** See {@link DEFAULT_OFFLINE_GATE_AFTER_MS}. */\n offlineGateAfterMs?: number;\n /** Start without connecting or minting (for a host's manual-save mode). Defaults to true. */\n connectionEnabled?: boolean;\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const mint = resolveMint({ auth: args.auth, mint: args.mint });\n const localCopy = args.localCopy;\n const retryMintError = args.retryMintError ?? isRetryableMintError;\n const offlineGateAfterMs = args.offlineGateAfterMs ?? DEFAULT_OFFLINE_GATE_AFTER_MS;\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let offlineTimer: ReturnType<typeof setTimeout> | null = null;\n /** The provider whose socket was still opening, with nothing to send, at the threshold. */\n let openingPastThreshold: WebsocketProvider | null = null;\n let mintRetries = 0;\n let destroyed = false;\n let connectionEnabled = args.connectionEnabled ?? true;\n let status: GenericDocStatus = connectionEnabled ? 'connecting' : 'disconnected';\n let synced = false;\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n let prepared = localCopy === undefined;\n let held = false;\n let heldGrant: CollabTokenResponse | null = null;\n let everConnected = false;\n let disconnectedSince: number | null = null;\n /** Bumped by every connect attempt: an attempt that finds it changed after an await stops. */\n let connectSeq = 0;\n /** Local edits no provider holds: handed on by a replaced provider, or made with none. */\n let carriedUpdates: WebsocketProvider['unconfirmedUpdates'] = [];\n let localChangesSynced = true;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const unconfirmedEdits = (): Y.ContentIds | null => {\n const all = [...carriedUpdates, ...(provider?.unconfirmedUpdates ?? [])].map(\n (entry) => entry.ids as Y.ContentIds\n );\n if (all.length === 0) return null;\n return all.length === 1 ? (all[0] as Y.ContentIds) : Y.mergeContentIds(all);\n };\n\n const transport: GenericDocTransportView = {\n isRemoteOrigin: (origin) => origin instanceof WebsocketProvider,\n getUnconfirmed: unconfirmedEdits,\n isFullySynced: () => status === 'connected' && synced && localChangesSynced,\n };\n\n const transportChanged = () => {\n localCopy?.transportChanged();\n };\n\n const refreshLocalChangesSynced = () => {\n const next = carriedUpdates.length === 0 && (provider?.unconfirmedUpdates.length ?? 0) === 0;\n const changed = next !== localChangesSynced;\n localChangesSynced = next;\n // After the flag: the copy reads `isFullySynced()` in its callback.\n // Before notifying: the callback can restore values and create more unconfirmed edits.\n transportChanged();\n if (changed) notify();\n };\n\n // An edit made while no provider exists (a re-mint in flight, offline) is sent by the next one,\n // which takes it as unconfirmed. The local copy's own load is not an edit.\n doc.on('update', (update: Uint8Array, origin: unknown) => {\n if (!prepared || provider !== null || destroyed || origin instanceof WebsocketProvider) return;\n carriedUpdates = [\n ...carriedUpdates,\n { created: Date.now(), ids: Y.createContentIdsFromUpdate(update) },\n ];\n refreshLocalChangesSynced();\n });\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n const wasConnected = status === 'connected';\n status = next;\n error = err;\n if (next === 'connected') {\n everConnected = true;\n disconnectedSince = null;\n clearOfflineTimer();\n } else if (wasConnected || (everConnected && disconnectedSince === null)) {\n disconnectedSince = Date.now();\n }\n transportChanged();\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const clearRetryTimer = () => {\n if (retryTimer !== null) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n };\n\n function clearOfflineTimer() {\n if (offlineTimer !== null) {\n clearTimeout(offlineTimer);\n offlineTimer = null;\n }\n }\n\n const teardownProvider = () => {\n clearRefreshTimer();\n // A future provider must complete its own initial sync; the doc keeps its\n // content either way. Subscribers must observe the flip immediately —\n // waiting for the next provider event would let them act on synced=true\n // across the replacement gap.\n if (synced) {\n synced = false;\n notify();\n }\n if (provider) {\n // Its unconfirmed edits outlive it: the next provider sends them and waits for their echo.\n const previous = provider;\n provider = null;\n carriedUpdates = [...carriedUpdates, ...previous.unconfirmedUpdates];\n previous.destroy();\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = refreshDelayMs({ expiresAtMs: grant.expires_at_ms, now: Date.now() });\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n /** A failed attempt that is worth retrying: try again later, or as soon as the network returns. */\n const scheduleRetry = () => {\n clearRetryTimer();\n const delay = MINT_RETRY_DELAYS_MS[Math.min(mintRetries, MINT_RETRY_DELAYS_MS.length - 1)];\n mintRetries += 1;\n retryTimer = setTimeout(() => {\n retryTimer = null;\n void connect();\n }, delay);\n };\n\n const onBrowserOnline = () => {\n // Only a handle waiting to retry: a refused or permanently closed one stays stopped.\n if (\n destroyed || !connectionEnabled || status === 'closed' || status === 'error' ||\n provider !== null || held\n ) return;\n mintRetries = 0;\n clearRetryTimer();\n void connect();\n };\n if (typeof window !== 'undefined') window.addEventListener('online', onBrowserOnline);\n\n /**\n * Past the gate's threshold without a socket, the handle reconnects itself (re-mint, gate). A\n * socket still being opened with nothing waiting to be sent is left to finish (a slow network\n * is not restarted every threshold), and the handle looks again a threshold later; if it opens\n * holding edits made since, they go through the gate first (`status`).\n */\n const armOfflineTakeover = () => {\n if (!connectionEnabled || offlineTimer !== null || !localCopy?.beforeConnect) return;\n offlineTimer = setTimeout(() => {\n offlineTimer = null;\n if (destroyed || !connectionEnabled || status === 'connected' || provider === null) return;\n if (provider.wsconnecting && !hasUnsyncedLocalEdits()) {\n openingPastThreshold = provider;\n armOfflineTakeover();\n return;\n }\n teardownProvider();\n void connect();\n }, offlineGateAfterMs);\n };\n\n const hasUnsyncedLocalEdits = () =>\n carriedUpdates.length > 0 || (provider?.unconfirmedUpdates.length ?? 0) > 0;\n\n\n const connect = async (opts?: {\n scheduledRefresh?: boolean;\n skipGate?: boolean;\n grant?: CollabTokenResponse;\n }): Promise<void> => {\n if (destroyed || !connectionEnabled) return;\n const seq = ++connectSeq;\n await preparing;\n if (destroyed || !connectionEnabled || seq !== connectSeq) return;\n // Only a live connection is refreshed in place (and skips the host's gate); a refresh while the\n // socket is down is a reconnect like any other.\n const scheduledRefresh = opts?.scheduledRefresh === true && status === 'connected';\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n clearRetryTimer();\n if (destroyed || !connectionEnabled || seq !== connectSeq) return;\n let grant: CollabTokenResponse;\n if (opts?.grant && opts.grant.expires_at_ms - Date.now() > MIN_REFRESH_DELAY_MS) {\n grant = opts.grant;\n } else {\n try {\n grant = await mint({ assetId: args.assetId, access });\n } catch (err) {\n if (destroyed || !connectionEnabled || seq !== connectSeq) return;\n if (scheduledRefresh) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n if (retryMintError(err)) {\n // Offline (or the mint's server is): keep the document and try again.\n if (status !== 'disconnected') setStatus('disconnected');\n scheduleRetry();\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible).\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n }\n if (destroyed || !connectionEnabled || seq !== connectSeq) return;\n mintRetries = 0;\n accessType = grant.access_type;\n const beforeConnect = localCopy?.beforeConnect;\n if (beforeConnect && !scheduledRefresh && !opts?.skipGate) {\n const firstConnection = !everConnected;\n const offlineForMs = disconnectedSince === null ? null : Date.now() - disconnectedSince;\n let decision: 'connect' | 'hold';\n try {\n decision = await beforeConnect({\n grant,\n firstConnection,\n offlineForMs,\n hasUnsyncedEdits: hasUnsyncedLocalEdits(),\n });\n } catch {\n if (destroyed || !connectionEnabled || seq !== connectSeq) return;\n if (status !== 'disconnected') setStatus('disconnected');\n scheduleRetry();\n return;\n }\n if (destroyed || !connectionEnabled || seq !== connectSeq) return;\n if (decision === 'hold') {\n held = true;\n heldGrant = grant;\n if (status !== 'disconnected') setStatus('disconnected');\n else notify();\n return;\n }\n }\n if (scheduledRefresh) {\n teardownProvider();\n }\n if (destroyed || !connectionEnabled || seq !== connectSeq) return;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n // BroadcastChannel would sync same-origin clients locally, bypassing the\n // server's permission enforcement (a read-only tab could write into an\n // editor tab's doc). Permissioned docs must round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n nextProvider.unconfirmedUpdates = [...carriedUpdates, ...nextProvider.unconfirmedUpdates];\n carriedUpdates = [];\n refreshLocalChangesSynced();\n\n nextProvider.on('sync-status', () => {\n if (provider !== nextProvider) return;\n refreshLocalChangesSynced();\n });\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (\n event.status === 'connected' &&\n openingPastThreshold === nextProvider &&\n hasUnsyncedLocalEdits()\n ) {\n // Opened after the threshold holding edits made meanwhile: before its handshake sends\n // them (`@y/websocket` reports `connected` first), they go through the gate.\n teardownProvider();\n void connect();\n return;\n }\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n // `@y/websocket` reports `disconnected` only for a socket that had opened: one that never\n // opens (the server unreachable while the mint goes through) only ever reports `connecting`,\n // and is just as offline.\n if (event.status !== 'connected') armOfflineTakeover();\n });\n // Its first `connecting` came from its constructor, before this listener existed.\n if (!nextProvider.wsconnected) armOfflineTakeover();\n\n nextProvider.on('sync', (state: boolean) => {\n if (provider !== nextProvider) return;\n if (synced !== state) {\n synced = state;\n transportChanged();\n notify();\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it, until the offline takeover.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n const preparing: Promise<void> = localCopy\n ? localCopy\n .prepare({ doc, transport })\n .catch(() => ({ unsyncedEdits: null }))\n .then(({ unsyncedEdits }) => {\n if (destroyed) return;\n if (unsyncedEdits && !(unsyncedEdits.inserts.isEmpty() && unsyncedEdits.deletes.isEmpty())) {\n carriedUpdates = [...carriedUpdates, { created: Date.now(), ids: unsyncedEdits }];\n }\n prepared = true;\n refreshLocalChangesSynced();\n notify();\n })\n : Promise.resolve();\n\n void connect();\n\n const waitForSync = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const terminal = (): Error | null => {\n if (destroyed) return new Error('Handle destroyed before initial sync');\n if (status === 'closed' || status === 'error') {\n return error ?? new Error(`Connection ${status} before initial sync`);\n }\n return null;\n };\n if (synced) {\n resolve();\n return;\n }\n const immediate = terminal();\n if (immediate) {\n reject(immediate);\n return;\n }\n const cleanup = () => {\n clearTimeout(timer);\n listeners.delete(listener);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));\n }, timeoutMs);\n const listener = () => {\n if (synced) {\n cleanup();\n resolve();\n return;\n }\n const err = terminal();\n if (err) {\n cleanup();\n reject(err);\n }\n };\n listeners.add(listener);\n });\n\n const waitForAcknowledged = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n let finished = false;\n let checkQueued = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const finish = (failure?: Error) => {\n if (finished) return;\n finished = true;\n if (timer !== null) clearTimeout(timer);\n listeners.delete(check);\n if (failure) reject(failure);\n else resolve();\n };\n const check = () => {\n if (finished || checkQueued) return;\n checkQueued = true;\n // All synchronous transport hooks and change listeners finish before a save settles.\n queueMicrotask(() => {\n checkQueued = false;\n if (finished) return;\n if (destroyed) {\n finish(new Error('Handle destroyed before local changes were acknowledged'));\n } else if (!connectionEnabled) {\n finish(new Error('Connection disabled before local changes were acknowledged'));\n } else if (status === 'closed' || status === 'error') {\n finish(error ?? new Error(`Connection ${status} before local changes were acknowledged`));\n } else if (prepared && status === 'connected' && synced && localChangesSynced) {\n finish();\n }\n });\n };\n timer = setTimeout(() => {\n finish(new Error(`Timed out after ${timeoutMs}ms waiting for local changes to be acknowledged`));\n }, timeoutMs);\n listeners.add(check);\n check();\n });\n\n return {\n doc,\n get status() {\n return status;\n },\n get synced() {\n return synced;\n },\n get localChangesSynced() {\n return localChangesSynced;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get prepared() {\n return prepared;\n },\n get connectionEnabled() {\n return connectionEnabled;\n },\n get held() {\n return held;\n },\n get version() {\n return version;\n },\n waitForSync,\n waitForAcknowledged,\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => {\n if (!connectionEnabled || held) return Promise.resolve();\n return provider ? connect({ scheduledRefresh: true }) : connect();\n },\n setConnectionEnabled: (enabled) => {\n if (destroyed || enabled === connectionEnabled) return;\n connectionEnabled = enabled;\n if (!enabled) {\n // Invalidate every mint/gate already in flight before tearing down the live provider.\n connectSeq += 1;\n clearRetryTimer();\n clearOfflineTimer();\n teardownProvider();\n setStatus('disconnected');\n localCopy?.connectionPolicyChanged?.(false);\n return;\n }\n localCopy?.connectionPolicyChanged?.(true);\n held = false;\n heldGrant = null;\n setStatus('connecting');\n void connect();\n },\n resume: () => {\n if (!held || destroyed || !connectionEnabled) return;\n held = false;\n const grant = heldGrant;\n heldGrant = null;\n notify();\n void connect({ skipGate: true, grant: grant ?? undefined });\n },\n unconfirmedEdits,\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n clearRetryTimer();\n clearOfflineTimer();\n if (typeof window !== 'undefined') window.removeEventListener('online', onBrowserOnline);\n status = 'closed';\n // Final notification so pending waitForSync callers reject instead of\n // hanging on listeners that are about to be dropped.\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n"],"names":["CollabTokenError","mint","mintCollabToken","Y","WebsocketProvider","refreshDelayMs","MIN_REFRESH_DELAY_MS","classifyClose","MAX_CONSECUTIVE_REMINTS"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqLO,MAAM,gCAAgC;AAG7C,MAAM,uBAAuB,CAAC,KAAO,KAAO,KAAO,KAAO,MAAQ,KAAQ,GAAM;AAMzE,SAAS,qBAAqB,OAAyB;AAC5D,MAAI,iBAAiBA,KAAAA,kBAAkB;AACrC,WAAO,MAAM,UAAU,OAAO,MAAM,WAAW,OAAO,MAAM,WAAW;AAAA,EACzE;AACA,SAAO,iBAAiB,aAAc,iBAAiB,gBAAgB,MAAM,SAAS;AACxF;AAEA,SAAS,YAAY,EAAE,MAAA,MAAMC,UAA8D;AACzF,MAAIA,OAAM,QAAOA;AACjB,MAAI,CAAC,KAAM,OAAM,IAAI,MAAM,0CAA0C;AACrE,SAAO,CAAC,EAAE,SAAS,OAAA,MAAaC,KAAAA,gBAAgB,EAAE,MAAM,SAAS,QAAQ;AAC3E;AAEO,SAAS,kBAAkB,MAcb;AACnB,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAMD,QAAO,YAAY,EAAE,MAAM,KAAK,MAAM,MAAM,KAAK,MAAM;AAC7D,QAAM,YAAY,KAAK;AACvB,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,qBAAqB,KAAK,sBAAsB;AACtD,QAAM,MAAM,IAAIE,aAAE,IAAA;AAClB,QAAM,gCAAgB,IAAA;AAEtB,MAAI,WAAqC;AACzC,MAAI,eAAqD;AACzD,MAAI,aAAmD;AACvD,MAAI,eAAqD;AAEzD,MAAI,uBAAiD;AACrD,MAAI,cAAc;AAClB,MAAI,YAAY;AAChB,MAAI,oBAAoB,KAAK,qBAAqB;AAClD,MAAI,SAA2B,oBAAoB,eAAe;AAClE,MAAI,SAAS;AACb,MAAI,aAAgC;AACpC,MAAI,QAAsB;AAC1B,MAAI,eAA+C;AACnD,MAAI,UAAU;AACd,MAAI,qBAAqB;AACzB,MAAI,WAAW,cAAc;AAC7B,MAAI,OAAO;AACX,MAAI,YAAwC;AAC5C,MAAI,gBAAgB;AACpB,MAAI,oBAAmC;AAEvC,MAAI,aAAa;AAEjB,MAAI,iBAA0D,CAAA;AAC9D,MAAI,qBAAqB;AAEzB,QAAM,SAAS,MAAM;AACnB,eAAW;AACX,eAAW,YAAY,UAAW,UAAA;AAAA,EACpC;AAEA,QAAM,mBAAmB,MAA2B;AAClD,UAAM,MAAM,CAAC,GAAG,gBAAgB,IAAI,qCAAU,uBAAsB,CAAA,CAAG,EAAE;AAAA,MACvE,CAAC,UAAU,MAAM;AAAA,IAAA;AAEnB,QAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,WAAO,IAAI,WAAW,IAAK,IAAI,CAAC,IAAqBA,aAAE,gBAAgB,GAAG;AAAA,EAC5E;AAEA,QAAM,YAAqC;AAAA,IACzC,gBAAgB,CAAC,WAAW,kBAAkBC,UAAAA;AAAAA,IAC9C,gBAAgB;AAAA,IAChB,eAAe,MAAM,WAAW,eAAe,UAAU;AAAA,EAAA;AAG3D,QAAM,mBAAmB,MAAM;AAC7B,2CAAW;AAAA,EACb;AAEA,QAAM,4BAA4B,MAAM;AACtC,UAAM,OAAO,eAAe,WAAW,OAAM,qCAAU,mBAAmB,WAAU,OAAO;AAC3F,UAAM,UAAU,SAAS;AACzB,yBAAqB;AAGrB,qBAAA;AACA,QAAI,QAAS,QAAA;AAAA,EACf;AAIA,MAAI,GAAG,UAAU,CAAC,QAAoB,WAAoB;AACxD,QAAI,CAAC,YAAY,aAAa,QAAQ,aAAa,kBAAkBA,4BAAmB;AACxF,qBAAiB;AAAA,MACf,GAAG;AAAA,MACH,EAAE,SAAS,KAAK,IAAA,GAAO,KAAKD,aAAE,2BAA2B,MAAM,EAAA;AAAA,IAAE;AAEnE,8BAAA;AAAA,EACF,CAAC;AAED,QAAM,YAAY,CAAC,MAAwB,MAAoB,SAAS;AACtE,QAAI,aAAa,SAAS,SAAU;AACpC,UAAM,eAAe,WAAW;AAChC,aAAS;AACT,YAAQ;AACR,QAAI,SAAS,aAAa;AACxB,sBAAgB;AAChB,0BAAoB;AACpB,wBAAA;AAAA,IACF,WAAW,gBAAiB,iBAAiB,sBAAsB,MAAO;AACxE,0BAAoB,KAAK,IAAA;AAAA,IAC3B;AACA,qBAAA;AACA,WAAA;AAAA,EACF;AAEA,QAAM,oBAAoB,MAAM;AAC9B,QAAI,iBAAiB,MAAM;AACzB,mBAAa,YAAY;AACzB,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,kBAAkB,MAAM;AAC5B,QAAI,eAAe,MAAM;AACvB,mBAAa,UAAU;AACvB,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,WAAS,oBAAoB;AAC3B,QAAI,iBAAiB,MAAM;AACzB,mBAAa,YAAY;AACzB,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,mBAAmB,MAAM;AAC7B,sBAAA;AAKA,QAAI,QAAQ;AACV,eAAS;AACT,aAAA;AAAA,IACF;AACA,QAAI,UAAU;AAEZ,YAAM,WAAW;AACjB,iBAAW;AACX,uBAAiB,CAAC,GAAG,gBAAgB,GAAG,SAAS,kBAAkB;AACnE,eAAS,QAAA;AAAA,IACX;AAAA,EACF;AAEA,QAAM,kBAAkB,CAAC,UAA+B;AACtD,sBAAA;AACA,UAAM,QAAQE,4BAAe,EAAE,aAAa,MAAM,eAAe,KAAK,KAAK,IAAA,GAAO;AAClF,mBAAe,WAAW,MAAM;AAC9B,WAAK,QAAQ,EAAE,kBAAkB,MAAM;AAAA,IACzC,GAAG,KAAK;AAAA,EACV;AAGA,QAAM,gBAAgB,MAAM;AAC1B,oBAAA;AACA,UAAM,QAAQ,qBAAqB,KAAK,IAAI,aAAa,qBAAqB,SAAS,CAAC,CAAC;AACzF,mBAAe;AACf,iBAAa,WAAW,MAAM;AAC5B,mBAAa;AACb,WAAK,QAAA;AAAA,IACP,GAAG,KAAK;AAAA,EACV;AAEA,QAAM,kBAAkB,MAAM;AAE5B,QACE,aAAa,CAAC,qBAAqB,WAAW,YAAY,WAAW,WACrE,aAAa,QAAQ,KACrB;AACF,kBAAc;AACd,oBAAA;AACA,SAAK,QAAA;AAAA,EACP;AACA,MAAI,OAAO,WAAW,YAAa,QAAO,iBAAiB,UAAU,eAAe;AAQpF,QAAM,qBAAqB,MAAM;AAC/B,QAAI,CAAC,qBAAqB,iBAAiB,QAAQ,EAAC,uCAAW,eAAe;AAC9E,mBAAe,WAAW,MAAM;AAC9B,qBAAe;AACf,UAAI,aAAa,CAAC,qBAAqB,WAAW,eAAe,aAAa,KAAM;AACpF,UAAI,SAAS,gBAAgB,CAAC,yBAAyB;AACrD,+BAAuB;AACvB,2BAAA;AACA;AAAA,MACF;AACA,uBAAA;AACA,WAAK,QAAA;AAAA,IACP,GAAG,kBAAkB;AAAA,EACvB;AAEA,QAAM,wBAAwB,MAC5B,eAAe,SAAS,OAAM,qCAAU,mBAAmB,WAAU,KAAK;AAG5E,QAAM,UAAU,OAAO,SAIF;AACnB,QAAI,aAAa,CAAC,kBAAmB;AACrC,UAAM,MAAM,EAAE;AACd,UAAM;AACN,QAAI,aAAa,CAAC,qBAAqB,QAAQ,WAAY;AAG3D,UAAM,oBAAmB,6BAAM,sBAAqB,QAAQ,WAAW;AAIvE,QAAI,CAAC,kBAAkB;AACrB,uBAAA;AAAA,IACF,OAAO;AACL,wBAAA;AAAA,IACF;AACA,oBAAA;AACA,QAAI,aAAa,CAAC,qBAAqB,QAAQ,WAAY;AAC3D,QAAI;AACJ,SAAI,6BAAM,UAAS,KAAK,MAAM,gBAAgB,KAAK,IAAA,IAAQC,mCAAsB;AAC/E,cAAQ,KAAK;AAAA,IACf,OAAO;AACL,UAAI;AACF,gBAAQ,MAAML,MAAK,EAAE,SAAS,KAAK,SAAS,QAAQ;AAAA,MACtD,SAAS,KAAK;AACZ,YAAI,aAAa,CAAC,qBAAqB,QAAQ,WAAY;AAC3D,YAAI,kBAAkB;AACpB,yBAAe,WAAW,MAAM;AAC9B,iBAAK,QAAQ,EAAE,kBAAkB,MAAM;AAAA,UACzC,GAAGK,aAAAA,oBAAoB;AACvB;AAAA,QACF;AACA,YAAI,eAAe,GAAG,GAAG;AAEvB,cAAI,WAAW,eAAgB,WAAU,cAAc;AACvD,wBAAA;AACA;AAAA,QACF;AAEA,kBAAU,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AACtE;AAAA,MACF;AAAA,IACF;AACA,QAAI,aAAa,CAAC,qBAAqB,QAAQ,WAAY;AAC3D,kBAAc;AACd,iBAAa,MAAM;AACnB,UAAM,gBAAgB,uCAAW;AACjC,QAAI,iBAAiB,CAAC,oBAAoB,EAAC,6BAAM,WAAU;AACzD,YAAM,kBAAkB,CAAC;AACzB,YAAM,eAAe,sBAAsB,OAAO,OAAO,KAAK,QAAQ;AACtE,UAAI;AACJ,UAAI;AACF,mBAAW,MAAM,cAAc;AAAA,UAC7B;AAAA,UACA;AAAA,UACA;AAAA,UACA,kBAAkB,sBAAA;AAAA,QAAsB,CACzC;AAAA,MACH,QAAQ;AACN,YAAI,aAAa,CAAC,qBAAqB,QAAQ,WAAY;AAC3D,YAAI,WAAW,eAAgB,WAAU,cAAc;AACvD,sBAAA;AACA;AAAA,MACF;AACA,UAAI,aAAa,CAAC,qBAAqB,QAAQ,WAAY;AAC3D,UAAI,aAAa,QAAQ;AACvB,eAAO;AACP,oBAAY;AACZ,YAAI,WAAW,eAAgB,WAAU,cAAc;AAAA,YAClD,QAAA;AACL;AAAA,MACF;AAAA,IACF;AACA,QAAI,kBAAkB;AACpB,uBAAA;AAAA,IACF;AACA,QAAI,aAAa,CAAC,qBAAqB,QAAQ,WAAY;AAE3D,UAAM,eAAe,IAAIF,UAAAA,kBAAkB,MAAM,QAAQ,GAAG,MAAM,GAAG,IAAI,MAAM,MAAM,IAAI,KAAK;AAAA,MAC5F,QAAQ;AAAA,QACN,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd,IAAI;AAAA,MAAA;AAAA;AAAA;AAAA;AAAA,MAKN,WAAW;AAAA,IAAA,CACZ;AACD,eAAW;AACX,iBAAa,qBAAqB,CAAC,GAAG,gBAAgB,GAAG,aAAa,kBAAkB;AACxF,qBAAiB,CAAA;AACjB,8BAAA;AAEA,iBAAa,GAAG,eAAe,MAAM;AACnC,UAAI,aAAa,aAAc;AAC/B,gCAAA;AAAA,IACF,CAAC;AAED,iBAAa,GAAG,UAAU,CAAC,UAAmE;AAC5F,UAAI,aAAa,aAAc;AAC/B,UACE,MAAM,WAAW,eACjB,yBAAyB,gBACzB,yBACA;AAGA,yBAAA;AACA,aAAK,QAAA;AACL;AAAA,MACF;AACA,UAAI,MAAM,WAAW,aAAa;AAChC,6BAAqB;AAAA,MACvB;AACA,gBAAU,MAAM,MAAM;AACtB,UAAI,MAAM,WAAW,eAAe,iBAAiB,MAAM;AACzD,qBAAa,UAAU,cAAc,YAAY;AAAA,MACnD;AAIA,UAAI,MAAM,WAAW,YAAa,oBAAA;AAAA,IACpC,CAAC;AAED,QAAI,CAAC,aAAa,YAAa,oBAAA;AAE/B,iBAAa,GAAG,QAAQ,CAAC,UAAmB;AAC1C,UAAI,aAAa,aAAc;AAC/B,UAAI,WAAW,OAAO;AACpB,iBAAS;AACT,yBAAA;AACA,eAAA;AAAA,MACF;AAAA,IACF,CAAC;AAED,iBAAa,GAAG,oBAAoB,CAAC,UAA6B;AAChE,UAAI,aAAa,aAAc;AAC/B,YAAM,SAASG,YAAAA,cAAc,+BAAO,IAAI;AACxC,UAAI,WAAW,UAAU;AAMvB,8BAAsB;AACtB,yBAAA;AACA,YAAI,qBAAqBC,aAAAA,yBAAyB;AAChD;AAAA,YACE;AAAA,YACA,IAAI,MAAM,0DAA0D;AAAA,UAAA;AAEtE;AAAA,QACF;AACA,aAAK,QAAA;AAAA,MACP,WAAW,WAAW,QAAQ;AAC5B,yBAAA;AACA;AAAA,UACE;AAAA,UACA,IAAI,MAAM,wCAAuC,+BAAO,SAAQ,SAAS,GAAG;AAAA,QAAA;AAAA,MAEhF;AAAA,IAEF,CAAC;AAED,iBAAa,UAAU,GAAG,UAAU,MAAM;AACxC,UAAI,aAAa,aAAc;AAC/B,aAAA;AAAA,IACF,CAAC;AAED,oBAAgB,KAAK;AAAA,EACvB;AAEA,QAAM,YAA2B,YAC7B,UACG,QAAQ,EAAE,KAAK,WAAW,EAC1B,MAAM,OAAO,EAAE,eAAe,OAAO,EACrC,KAAK,CAAC,EAAE,oBAAoB;AAC3B,QAAI,UAAW;AACf,QAAI,iBAAiB,EAAE,cAAc,QAAQ,aAAa,cAAc,QAAQ,QAAA,IAAY;AAC1F,uBAAiB,CAAC,GAAG,gBAAgB,EAAE,SAAS,KAAK,IAAA,GAAO,KAAK,eAAe;AAAA,IAClF;AACA,eAAW;AACX,8BAAA;AACA,WAAA;AAAA,EACF,CAAC,IACH,QAAQ,QAAA;AAEZ,OAAK,QAAA;AAEL,QAAM,cAAc,CAAC,YAAY,SAC/B,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,UAAM,WAAW,MAAoB;AACnC,UAAI,UAAW,QAAO,IAAI,MAAM,sCAAsC;AACtE,UAAI,WAAW,YAAY,WAAW,SAAS;AAC7C,eAAO,SAAS,IAAI,MAAM,cAAc,MAAM,sBAAsB;AAAA,MACtE;AACA,aAAO;AAAA,IACT;AACA,QAAI,QAAQ;AACV,cAAA;AACA;AAAA,IACF;AACA,UAAM,YAAY,SAAA;AAClB,QAAI,WAAW;AACb,aAAO,SAAS;AAChB;AAAA,IACF;AACA,UAAM,UAAU,MAAM;AACpB,mBAAa,KAAK;AAClB,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AACA,UAAM,QAAQ,WAAW,MAAM;AAC7B,cAAA;AACA,aAAO,IAAI,MAAM,mBAAmB,SAAS,6BAA6B,CAAC;AAAA,IAC7E,GAAG,SAAS;AACZ,UAAM,WAAW,MAAM;AACrB,UAAI,QAAQ;AACV,gBAAA;AACA,gBAAA;AACA;AAAA,MACF;AACA,YAAM,MAAM,SAAA;AACZ,UAAI,KAAK;AACP,gBAAA;AACA,eAAO,GAAG;AAAA,MACZ;AAAA,IACF;AACA,cAAU,IAAI,QAAQ;AAAA,EACxB,CAAC;AAEH,QAAM,sBAAsB,CAAC,YAAY,SACvC,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,QAAI,WAAW;AACf,QAAI,cAAc;AAClB,QAAI,QAA8C;AAClD,UAAM,SAAS,CAAC,YAAoB;AAClC,UAAI,SAAU;AACd,iBAAW;AACX,UAAI,UAAU,KAAM,cAAa,KAAK;AACtC,gBAAU,OAAO,KAAK;AACtB,UAAI,gBAAgB,OAAO;AAAA,UACtB,SAAA;AAAA,IACP;AACA,UAAM,QAAQ,MAAM;AAClB,UAAI,YAAY,YAAa;AAC7B,oBAAc;AAEd,qBAAe,MAAM;AACnB,sBAAc;AACd,YAAI,SAAU;AACd,YAAI,WAAW;AACb,iBAAO,IAAI,MAAM,yDAAyD,CAAC;AAAA,QAC7E,WAAW,CAAC,mBAAmB;AAC7B,iBAAO,IAAI,MAAM,4DAA4D,CAAC;AAAA,QAChF,WAAW,WAAW,YAAY,WAAW,SAAS;AACpD,iBAAO,SAAS,IAAI,MAAM,cAAc,MAAM,yCAAyC,CAAC;AAAA,QAC1F,WAAW,YAAY,WAAW,eAAe,UAAU,oBAAoB;AAC7E,iBAAA;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AACA,YAAQ,WAAW,MAAM;AACvB,aAAO,IAAI,MAAM,mBAAmB,SAAS,iDAAiD,CAAC;AAAA,IACjG,GAAG,SAAS;AACZ,cAAU,IAAI,KAAK;AACnB,UAAA;AAAA,EACF,CAAC;AAEH,SAAO;AAAA,IACL;AAAA,IACA,IAAI,SAAS;AACX,aAAO;AAAA,IACT;AAAA,IACA,IAAI,SAAS;AACX,aAAO;AAAA,IACT;AAAA,IACA,IAAI,qBAAqB;AACvB,aAAO;AAAA,IACT;AAAA,IACA,IAAI,aAAa;AACf,aAAO;AAAA,IACT;AAAA,IACA,IAAI,QAAQ;AACV,aAAO;AAAA,IACT;AAAA,IACA,IAAI,WAAW;AACb,aAAO;AAAA,IACT;AAAA,IACA,IAAI,oBAAoB;AACtB,aAAO;AAAA,IACT;AAAA,IACA,IAAI,OAAO;AACT,aAAO;AAAA,IACT;AAAA,IACA,IAAI,UAAU;AACZ,aAAO;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,MAAM;AACjB,UAAI,CAAC,SAAU,QAAO,CAAA;AACtB,YAAM,UAA2B,CAAA;AACjC,eAAS,UAAU,UAAA,EAAY,QAAQ,CAAC,OAAO,aAAa;AAC1D,gBAAQ,KAAK,EAAE,UAAU,MAAA,CAAyC;AAAA,MACpE,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IACA,aAAa,CAAC,UAAU;AACtB,qBAAe;AACf,2CAAU,UAAU,cAAc;AAAA,IACpC;AAAA,IACA,YAAY,MAAM;AAChB,UAAI,CAAC,qBAAqB,KAAM,QAAO,QAAQ,QAAA;AAC/C,aAAO,WAAW,QAAQ,EAAE,kBAAkB,KAAA,CAAM,IAAI,QAAA;AAAA,IAC1D;AAAA,IACA,sBAAsB,CAAC,YAAY;;AACjC,UAAI,aAAa,YAAY,kBAAmB;AAChD,0BAAoB;AACpB,UAAI,CAAC,SAAS;AAEZ,sBAAc;AACd,wBAAA;AACA,0BAAA;AACA,yBAAA;AACA,kBAAU,cAAc;AACxB,qDAAW,4BAAX,mCAAqC;AACrC;AAAA,MACF;AACA,mDAAW,4BAAX,mCAAqC;AACrC,aAAO;AACP,kBAAY;AACZ,gBAAU,YAAY;AACtB,WAAK,QAAA;AAAA,IACP;AAAA,IACA,QAAQ,MAAM;AACZ,UAAI,CAAC,QAAQ,aAAa,CAAC,kBAAmB;AAC9C,aAAO;AACP,YAAM,QAAQ;AACd,kBAAY;AACZ,aAAA;AACA,WAAK,QAAQ,EAAE,UAAU,MAAM,OAAO,SAAS,QAAW;AAAA,IAC5D;AAAA,IACA;AAAA,IACA,UAAU,CAAC,aAAa;AACtB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,SAAS,MAAM;AACb,UAAI,UAAW;AACf,kBAAY;AACZ,uBAAA;AACA,sBAAA;AACA,wBAAA;AACA,UAAI,OAAO,WAAW,YAAa,QAAO,oBAAoB,UAAU,eAAe;AACvF,eAAS;AAGT,iBAAW,YAAY,CAAC,GAAG,SAAS,EAAG,UAAA;AACvC,gBAAU,MAAA;AACV,UAAI,QAAA;AAAA,IACN;AAAA,EAAA;AAEJ;;;;;"}
@@ -53,6 +53,8 @@ export interface GenericDocHandle {
53
53
  * Mount readers after it: until then the document is still empty.
54
54
  */
55
55
  readonly prepared: boolean;
56
+ /** Whether this host allows a live connection. Disabled documents remain editable locally. */
57
+ readonly connectionEnabled: boolean;
56
58
  /**
57
59
  * A connection the host's `beforeConnect` held: the handle waits, sending nothing, until the
58
60
  * host calls `resume()` (or destroys the handle).
@@ -65,6 +67,12 @@ export interface GenericDocHandle {
65
67
  * terminal connection state, or if the handle is destroyed first.
66
68
  */
67
69
  waitForSync: (timeoutMs?: number) => Promise<void>;
70
+ /**
71
+ * Resolve once connected, initially synced, and every local change has been acknowledged,
72
+ * including any edits the local-copy hook restores after sync. The host must prevent further
73
+ * editing while saving a checkpoint. Reject on timeout, a disabled connection, or a terminal state.
74
+ */
75
+ waitForAcknowledged: (timeoutMs?: number) => Promise<void>;
68
76
  getPresence: () => PresenceEntry[];
69
77
  /** Broadcast this client's presence state (requires a live connection). */
70
78
  setPresence: (state: Record<string, unknown> | null) => void;
@@ -75,6 +83,11 @@ export interface GenericDocHandle {
75
83
  * reconnects now.
76
84
  */
77
85
  refreshNow: () => Promise<void>;
86
+ /**
87
+ * Disable all connections without losing the document or its pending edits. Enabling mints a
88
+ * fresh grant and runs the host's beforeConnect gate; it never bypasses a permission or review.
89
+ */
90
+ setConnectionEnabled: (enabled: boolean) => void;
78
91
  /** Connect past a hold, without asking `beforeConnect` again. */
79
92
  resume: () => void;
80
93
  /** Local edits the server has not echoed yet (handed between providers too), or `null`. */
@@ -128,6 +141,13 @@ export interface GenericDocLocalCopy {
128
141
  }>;
129
142
  /** The handle's unconfirmed edits or sync state changed. */
130
143
  transportChanged: () => void;
144
+ /**
145
+ * An explicit host pause/resume policy changed. Invalidate decisions that must be checked afresh
146
+ * before a later manual save. A pause runs after its provider is destroyed and its status is
147
+ * disconnected, so edits restored by the hook stay local. A resume runs before minting.
148
+ * Network retries and an immediate conflict `resume()` do not call it.
149
+ */
150
+ connectionPolicyChanged?: (enabled: boolean) => void;
131
151
  /**
132
152
  * Asked after each mint and before the connection it would open: the first connection, a
133
153
  * reconnect after an outage longer than `offlineGateAfterMs`, and a re-mint after a revocation
@@ -157,6 +177,8 @@ export declare function connectGenericDoc(args: {
157
177
  retryMintError?: (error: unknown) => boolean;
158
178
  /** See {@link DEFAULT_OFFLINE_GATE_AFTER_MS}. */
159
179
  offlineGateAfterMs?: number;
180
+ /** Start without connecting or minting (for a host's manual-save mode). Defaults to true. */
181
+ connectionEnabled?: boolean;
160
182
  }): GenericDocHandle;
161
183
  export { CollabTokenError };
162
184
  export type { CollabAuth, CollabTokenResponse };
@@ -31,7 +31,8 @@ function connectGenericDoc(args) {
31
31
  let openingPastThreshold = null;
32
32
  let mintRetries = 0;
33
33
  let destroyed = false;
34
- let status = "connecting";
34
+ let connectionEnabled = args.connectionEnabled ?? true;
35
+ let status = connectionEnabled ? "connecting" : "disconnected";
35
36
  let synced = false;
36
37
  let accessType = null;
37
38
  let error = null;
@@ -67,11 +68,10 @@ function connectGenericDoc(args) {
67
68
  };
68
69
  const refreshLocalChangesSynced = () => {
69
70
  const next = carriedUpdates.length === 0 && ((provider == null ? void 0 : provider.unconfirmedUpdates.length) ?? 0) === 0;
70
- if (next !== localChangesSynced) {
71
- localChangesSynced = next;
72
- notify();
73
- }
71
+ const changed = next !== localChangesSynced;
72
+ localChangesSynced = next;
74
73
  transportChanged();
74
+ if (changed) notify();
75
75
  };
76
76
  doc.on("update", (update, origin) => {
77
77
  if (!prepared || provider !== null || destroyed || origin instanceof WebsocketProvider) return;
@@ -121,9 +121,10 @@ function connectGenericDoc(args) {
121
121
  notify();
122
122
  }
123
123
  if (provider) {
124
- carriedUpdates = [...carriedUpdates, ...provider.unconfirmedUpdates];
125
- provider.destroy();
124
+ const previous = provider;
126
125
  provider = null;
126
+ carriedUpdates = [...carriedUpdates, ...previous.unconfirmedUpdates];
127
+ previous.destroy();
127
128
  }
128
129
  };
129
130
  const scheduleRefresh = (grant) => {
@@ -143,17 +144,17 @@ function connectGenericDoc(args) {
143
144
  }, delay);
144
145
  };
145
146
  const onBrowserOnline = () => {
146
- if (destroyed || status === "closed" || status === "error" || provider !== null || held) return;
147
+ if (destroyed || !connectionEnabled || status === "closed" || status === "error" || provider !== null || held) return;
147
148
  mintRetries = 0;
148
149
  clearRetryTimer();
149
150
  void connect();
150
151
  };
151
152
  if (typeof window !== "undefined") window.addEventListener("online", onBrowserOnline);
152
153
  const armOfflineTakeover = () => {
153
- if (offlineTimer !== null || !(localCopy == null ? void 0 : localCopy.beforeConnect)) return;
154
+ if (!connectionEnabled || offlineTimer !== null || !(localCopy == null ? void 0 : localCopy.beforeConnect)) return;
154
155
  offlineTimer = setTimeout(() => {
155
156
  offlineTimer = null;
156
- if (destroyed || status === "connected" || provider === null) return;
157
+ if (destroyed || !connectionEnabled || status === "connected" || provider === null) return;
157
158
  if (provider.wsconnecting && !hasUnsyncedLocalEdits()) {
158
159
  openingPastThreshold = provider;
159
160
  armOfflineTakeover();
@@ -165,10 +166,10 @@ function connectGenericDoc(args) {
165
166
  };
166
167
  const hasUnsyncedLocalEdits = () => carriedUpdates.length > 0 || ((provider == null ? void 0 : provider.unconfirmedUpdates.length) ?? 0) > 0;
167
168
  const connect = async (opts) => {
168
- if (destroyed) return;
169
- await preparing;
170
- if (destroyed) return;
169
+ if (destroyed || !connectionEnabled) return;
171
170
  const seq = ++connectSeq;
171
+ await preparing;
172
+ if (destroyed || !connectionEnabled || seq !== connectSeq) return;
172
173
  const scheduledRefresh = (opts == null ? void 0 : opts.scheduledRefresh) === true && status === "connected";
173
174
  if (!scheduledRefresh) {
174
175
  teardownProvider();
@@ -176,6 +177,7 @@ function connectGenericDoc(args) {
176
177
  clearRefreshTimer();
177
178
  }
178
179
  clearRetryTimer();
180
+ if (destroyed || !connectionEnabled || seq !== connectSeq) return;
179
181
  let grant;
180
182
  if ((opts == null ? void 0 : opts.grant) && opts.grant.expires_at_ms - Date.now() > MIN_REFRESH_DELAY_MS) {
181
183
  grant = opts.grant;
@@ -183,7 +185,7 @@ function connectGenericDoc(args) {
183
185
  try {
184
186
  grant = await mint({ assetId: args.assetId, access });
185
187
  } catch (err) {
186
- if (destroyed || seq !== connectSeq) return;
188
+ if (destroyed || !connectionEnabled || seq !== connectSeq) return;
187
189
  if (scheduledRefresh) {
188
190
  refreshTimer = setTimeout(() => {
189
191
  void connect({ scheduledRefresh: true });
@@ -199,7 +201,7 @@ function connectGenericDoc(args) {
199
201
  return;
200
202
  }
201
203
  }
202
- if (destroyed || seq !== connectSeq) return;
204
+ if (destroyed || !connectionEnabled || seq !== connectSeq) return;
203
205
  mintRetries = 0;
204
206
  accessType = grant.access_type;
205
207
  const beforeConnect = localCopy == null ? void 0 : localCopy.beforeConnect;
@@ -215,12 +217,12 @@ function connectGenericDoc(args) {
215
217
  hasUnsyncedEdits: hasUnsyncedLocalEdits()
216
218
  });
217
219
  } catch {
218
- if (destroyed || seq !== connectSeq) return;
220
+ if (destroyed || !connectionEnabled || seq !== connectSeq) return;
219
221
  if (status !== "disconnected") setStatus("disconnected");
220
222
  scheduleRetry();
221
223
  return;
222
224
  }
223
- if (destroyed || seq !== connectSeq) return;
225
+ if (destroyed || !connectionEnabled || seq !== connectSeq) return;
224
226
  if (decision === "hold") {
225
227
  held = true;
226
228
  heldGrant = grant;
@@ -232,6 +234,7 @@ function connectGenericDoc(args) {
232
234
  if (scheduledRefresh) {
233
235
  teardownProvider();
234
236
  }
237
+ if (destroyed || !connectionEnabled || seq !== connectSeq) return;
235
238
  const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {
236
239
  params: {
237
240
  yauth: grant.token,
@@ -353,6 +356,41 @@ function connectGenericDoc(args) {
353
356
  };
354
357
  listeners.add(listener);
355
358
  });
359
+ const waitForAcknowledged = (timeoutMs = 15e3) => new Promise((resolve, reject) => {
360
+ let finished = false;
361
+ let checkQueued = false;
362
+ let timer = null;
363
+ const finish = (failure) => {
364
+ if (finished) return;
365
+ finished = true;
366
+ if (timer !== null) clearTimeout(timer);
367
+ listeners.delete(check);
368
+ if (failure) reject(failure);
369
+ else resolve();
370
+ };
371
+ const check = () => {
372
+ if (finished || checkQueued) return;
373
+ checkQueued = true;
374
+ queueMicrotask(() => {
375
+ checkQueued = false;
376
+ if (finished) return;
377
+ if (destroyed) {
378
+ finish(new Error("Handle destroyed before local changes were acknowledged"));
379
+ } else if (!connectionEnabled) {
380
+ finish(new Error("Connection disabled before local changes were acknowledged"));
381
+ } else if (status === "closed" || status === "error") {
382
+ finish(error ?? new Error(`Connection ${status} before local changes were acknowledged`));
383
+ } else if (prepared && status === "connected" && synced && localChangesSynced) {
384
+ finish();
385
+ }
386
+ });
387
+ };
388
+ timer = setTimeout(() => {
389
+ finish(new Error(`Timed out after ${timeoutMs}ms waiting for local changes to be acknowledged`));
390
+ }, timeoutMs);
391
+ listeners.add(check);
392
+ check();
393
+ });
356
394
  return {
357
395
  doc,
358
396
  get status() {
@@ -373,6 +411,9 @@ function connectGenericDoc(args) {
373
411
  get prepared() {
374
412
  return prepared;
375
413
  },
414
+ get connectionEnabled() {
415
+ return connectionEnabled;
416
+ },
376
417
  get held() {
377
418
  return held;
378
419
  },
@@ -380,6 +421,7 @@ function connectGenericDoc(args) {
380
421
  return version;
381
422
  },
382
423
  waitForSync,
424
+ waitForAcknowledged,
383
425
  getPresence: () => {
384
426
  if (!provider) return [];
385
427
  const entries = [];
@@ -393,11 +435,30 @@ function connectGenericDoc(args) {
393
435
  provider == null ? void 0 : provider.awareness.setLocalState(state);
394
436
  },
395
437
  refreshNow: () => {
396
- if (held) return Promise.resolve();
438
+ if (!connectionEnabled || held) return Promise.resolve();
397
439
  return provider ? connect({ scheduledRefresh: true }) : connect();
398
440
  },
441
+ setConnectionEnabled: (enabled) => {
442
+ var _a, _b;
443
+ if (destroyed || enabled === connectionEnabled) return;
444
+ connectionEnabled = enabled;
445
+ if (!enabled) {
446
+ connectSeq += 1;
447
+ clearRetryTimer();
448
+ clearOfflineTimer();
449
+ teardownProvider();
450
+ setStatus("disconnected");
451
+ (_a = localCopy == null ? void 0 : localCopy.connectionPolicyChanged) == null ? void 0 : _a.call(localCopy, false);
452
+ return;
453
+ }
454
+ (_b = localCopy == null ? void 0 : localCopy.connectionPolicyChanged) == null ? void 0 : _b.call(localCopy, true);
455
+ held = false;
456
+ heldGrant = null;
457
+ setStatus("connecting");
458
+ void connect();
459
+ },
399
460
  resume: () => {
400
- if (!held || destroyed) return;
461
+ if (!held || destroyed || !connectionEnabled) return;
401
462
  held = false;
402
463
  const grant = heldGrant;
403
464
  heldGrant = null;