@k2b/cloud 0.43.0 → 0.45.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 (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,714 @@
1
+ import { encoding } from "@k2b/stdlib";
2
+ import * as awarenessProtocol from "y-protocols/awareness";
3
+ import * as Y from "yjs";
4
+ import { createSharedTextActionHistory, type SharedTextInverseRequest } from "./shared-text-history";
5
+
6
+ /** Application-independent error; adapters retain their own wire codes and messages. */
7
+ export type SharedTextProviderError = {
8
+ code: string;
9
+ message: string;
10
+ docId?: string;
11
+ /** Closed/replaced generations require recovery in a fresh editor; resume cannot revive them. */
12
+ reopen?: boolean;
13
+ };
14
+
15
+ export type SharedTextProviderUpdate = {
16
+ payload: string;
17
+ cursor?: string;
18
+ originPeerId?: string | null;
19
+ actionId?: string;
20
+ retainedColumn?: boolean;
21
+ generation?: string;
22
+ };
23
+
24
+ /** Normalized inbound packets. The application adapter validates its wire format and resource IDs. */
25
+ export type SharedTextProviderMessage<Event = never, Participant = never> =
26
+ | { type: "error"; payload: SharedTextProviderError }
27
+ | { type: "eventError"; payload: SharedTextProviderError; terminal: boolean }
28
+ | { type: "eventReady"; payload: Record<string, never> }
29
+ | { type: "event"; payload: { event: Event; cursor: string | null; historyIncomplete?: boolean } }
30
+ | { type: "replayReady"; payload: { docId: string; generation: string | null; cursor?: string; peerId?: string } }
31
+ | { type: "presence"; payload: { docId: string; participants: Participant[]; generation?: string } }
32
+ | {
33
+ type: "syncPush" | "awarenessPush";
34
+ payload: {
35
+ docId: string;
36
+ generation?: string;
37
+ historyIncomplete?: boolean;
38
+ snapshot?: boolean;
39
+ updates: SharedTextProviderUpdate[];
40
+ };
41
+ };
42
+
43
+ /** Normalized outbound packets; encode supplies the application's wire names and fields. */
44
+ export type SharedTextProviderRequest =
45
+ | { type: "replayRequest"; payload: { docId: string; fromCursor: string | null; generation: string | null } }
46
+ | { type: "syncPublish"; payload: { docId: string; payload: string; generation: string; baseCursor: string | null } }
47
+ | { type: "awarenessPublish"; payload: { docId: string; payload: string; generation: string | null } }
48
+ | { type: "subscribe"; payload: { fromCursor: string | null } };
49
+
50
+ /** Wire conversion only: connection, ordering, history and recovery belong to the provider. */
51
+ export type SharedTextProviderTransport<Event = never, Participant = never> = {
52
+ /** Absolute ws: or wss: URL. */
53
+ url: string;
54
+ encode: (request: SharedTextProviderRequest) => unknown;
55
+ decode: (packet: unknown) => SharedTextProviderMessage<Event, Participant> | null;
56
+ /** Include application-specific terminal reasons; nonterminal drops reconnect. */
57
+ closeError: (code: number, reason: string) => SharedTextProviderError | null;
58
+ };
59
+
60
+ /** Exact local state for durable offline storage, bound to one document generation. */
61
+ export type SharedTextProviderPendingState = {
62
+ docId: string;
63
+ generation: string | null;
64
+ state: Uint8Array;
65
+ text: string;
66
+ pending: { payload: string; baseCursor: string | null }[];
67
+ baseCursor: string | null | undefined;
68
+ };
69
+
70
+ /** A server snapshot only. Transport echoes and replay-ready never report durable coverage. */
71
+ export type SharedTextProviderSnapshot = {
72
+ docId: string;
73
+ generation: string;
74
+ state: Uint8Array;
75
+ cursor: string | null;
76
+ };
77
+
78
+ export type SharedTextProviderOptions<Event = never, Participant = never> = {
79
+ doc: Y.Doc;
80
+ undoManager?: Y.UndoManager;
81
+ awareness: awarenessProtocol.Awareness;
82
+ docId: string;
83
+ transport: SharedTextProviderTransport<Event, Participant>;
84
+ /** Existing snapshot and cursor must belong to this opaque generation. */
85
+ initialGeneration?: string;
86
+ initialCursor?: string | null;
87
+ /** Hydrate before connecting. A different document or generation is rejected without applying bytes. */
88
+ initialPending?: SharedTextProviderPendingState;
89
+ /** Existing transport's base64 payload limit; pending budget remains twice this limit. */
90
+ maxSyncPayloadLength: number;
91
+ terminalErrorCodes: readonly string[];
92
+ reconnectDelayMs: (failedAttempts: number) => number;
93
+ /** Optional only for transports with ordered generations and retained pre-restore events. */
94
+ isEarlierGeneration?: (incoming: string, current: string) => boolean;
95
+ /** Existing transports only: accept updates without generation metadata. New transports must retain the fence. */
96
+ allowUnversionedUpdates?: boolean;
97
+ /** Application wording for a generation fence; server-supplied messages pass through unchanged. */
98
+ generationMessages?: { changed: string; restored: string };
99
+ onConnectionChange?: (connected: boolean) => void;
100
+ onPresenceChange?: (participants: Participant[]) => void;
101
+ events?: {
102
+ initialCursor?: string | null;
103
+ onEvent: (event: Event, cursor: string | null) => void | Promise<void>;
104
+ onCursorChange?: (cursor: string) => void;
105
+ onError?: (error: SharedTextProviderError) => void;
106
+ };
107
+ onError?: (error: SharedTextProviderError) => void;
108
+ onFatal?: (error: SharedTextProviderError) => void;
109
+ onHistoryIncomplete?: () => void;
110
+ /** Replay reached the live tail, before pending writes and awareness are published. */
111
+ onReady?: () => void;
112
+ /** Coalesced notification of transport pending/history changes, including echoes. */
113
+ onUnsentChange?: () => void;
114
+ onRetainedColumn?: () => void;
115
+ /** Local input only, before publication. Persist this separately from transport acknowledgements. */
116
+ onLocalUpdate?: (pending: SharedTextProviderPendingState) => void;
117
+ /** Validated snapshot bytes, excluding any local input not covered by that server snapshot. */
118
+ onSnapshot?: (snapshot: SharedTextProviderSnapshot) => void;
119
+ };
120
+
121
+ /** Consecutive storage refusals before termination; replay alone does not reset the budget. */
122
+ export const MAX_SHARED_TEXT_STORAGE_RETRIES = 5;
123
+
124
+ /** Browser Yjs lifecycle for one document and generation, with an application-owned wire adapter. */
125
+ export function createSharedTextProvider<Event = never, Participant = never>(opts: SharedTextProviderOptions<Event, Participant>) {
126
+ const { doc, awareness } = opts;
127
+ if (!opts.docId) throw new TypeError("docId is required");
128
+ if (opts.initialCursor && opts.initialGeneration === undefined) throw new TypeError("initialCursor requires initialGeneration");
129
+ if (opts.initialGeneration !== undefined && !opts.initialGeneration) throw new TypeError("Invalid initialGeneration");
130
+ if (doc.store.clients.size > 0 && opts.initialGeneration === undefined)
131
+ throw new TypeError("An initial document requires initialGeneration");
132
+ if (!Number.isSafeInteger(opts.maxSyncPayloadLength) || opts.maxSyncPayloadLength <= 0)
133
+ throw new TypeError("maxSyncPayloadLength must be a positive safe integer");
134
+ if (opts.initialPending) {
135
+ if (opts.initialPending.docId !== opts.docId || opts.initialPending.generation !== opts.initialGeneration)
136
+ throw new TypeError("Pending input belongs to another document or generation");
137
+ // Validate everything before mutating the host document or attaching listeners.
138
+ Y.decodeUpdate(opts.initialPending.state);
139
+ for (const entry of opts.initialPending.pending) Y.decodeUpdate(encoding.fromBase64(entry.payload));
140
+ }
141
+ const activeDocId = opts.docId;
142
+ const terminalErrorCodes = new Set(opts.terminalErrorCodes);
143
+ let ws: WebSocket | null = null;
144
+ let isDisposed = false;
145
+ let isTerminated = false;
146
+ let needsReopen = false;
147
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
148
+ let failedConnects = 0;
149
+ let storageFailures = 0;
150
+ let lastCursor = opts.initialCursor ?? null;
151
+ let lastEventCursor = opts.events?.initialCursor ?? null;
152
+ let eventStreamEventQueue = Promise.resolve();
153
+ let eventStreamGeneration = 0;
154
+ let replayReady = false;
155
+ let awarenessPeerId: string | null = null;
156
+ let generation: string | null = opts.initialGeneration ?? null;
157
+ let localStateSent = false;
158
+ let needsFullResync = false;
159
+ const unconfirmed = new Map<string, string | null>();
160
+ let unconfirmedBytes = 0;
161
+ const forgetPending = (payload: string) => {
162
+ if (unconfirmed.delete(payload)) unconfirmedBytes -= payload.length;
163
+ };
164
+ let unsentBase: string | null | undefined;
165
+ let recoveryText: string | undefined;
166
+ if (opts.initialPending) {
167
+ Y.applyUpdate(doc, opts.initialPending.state, "remote");
168
+ for (const entry of opts.initialPending.pending) {
169
+ Y.applyUpdate(doc, encoding.fromBase64(entry.payload), "remote");
170
+ if (!unconfirmed.has(entry.payload)) unconfirmedBytes += entry.payload.length;
171
+ unconfirmed.set(entry.payload, entry.baseCursor);
172
+ }
173
+ unsentBase =
174
+ opts.initialPending.baseCursor === undefined ? (opts.initialPending.pending[0]?.baseCursor ?? null) : opts.initialPending.baseCursor;
175
+ recoveryText = opts.initialPending.text;
176
+ // Even echoed input retained in offline storage must be reconciled again.
177
+ needsFullResync = true;
178
+ }
179
+ const pendingState = (): SharedTextProviderPendingState => ({
180
+ docId: activeDocId,
181
+ generation,
182
+ state: Y.encodeStateAsUpdate(doc),
183
+ text: recoveryText ?? doc.getText("codemirror").toString(),
184
+ pending: [...unconfirmed].map(([payload, baseCursor]) => ({ payload, baseCursor })),
185
+ baseCursor: unsentBase,
186
+ });
187
+ // Current resend groups reference original pending bytes. Rebuilding a
188
+ // generation releases old groups; only an echo releases the covered input.
189
+ const covering = new Map<string, string[]>();
190
+ let notifyingPending = false;
191
+ const pendingChanged = () => {
192
+ if (notifyingPending || isDisposed) return;
193
+ notifyingPending = true;
194
+ queueMicrotask(() => {
195
+ notifyingPending = false;
196
+ if (!isDisposed) opts.onUnsentChange?.();
197
+ });
198
+ };
199
+ const actionHistory = opts.undoManager
200
+ ? createSharedTextActionHistory(doc, opts.undoManager, pendingChanged, opts.onRetainedColumn)
201
+ : null;
202
+
203
+ const send = (request: SharedTextProviderRequest): boolean => {
204
+ if (isDisposed || isTerminated) return false;
205
+ if (ws?.readyState === WebSocket.OPEN) {
206
+ ws.send(JSON.stringify(opts.transport.encode(request)));
207
+ return true;
208
+ }
209
+ return false;
210
+ };
211
+
212
+ const sendSyncPublish = (
213
+ data: Uint8Array,
214
+ baseCursor = unsentBase === undefined ? (unconfirmed.size ? unconfirmed.values().next().value! : lastCursor) : unsentBase,
215
+ ): boolean => {
216
+ if (generation === null) return false;
217
+ const payload = encoding.toBase64(data);
218
+ if (payload.length > opts.maxSyncPayloadLength) {
219
+ // The exact unsent state remains in recoveryInput().state; sending an
220
+ // oversized update would only loop on the server's existing limit.
221
+ unsentBase = baseCursor;
222
+ needsFullResync = true;
223
+ needsReopen = true;
224
+ pendingChanged();
225
+ // Stop collecting immediately; notify/reconfigure the host only after
226
+ // the native edit which exceeded the budget has left EditorView.update.
227
+ replayReady = false;
228
+ queueMicrotask(() => {
229
+ if (!isDisposed) terminate({ code: "BACKPRESSURE", message: "Websocket backpressure", docId: activeDocId });
230
+ });
231
+ return false;
232
+ }
233
+ // Budget for an original transport envelope plus its covering reconnect
234
+ // envelope. Preserve the triggering update too, then stop through existing
235
+ // recovery. Echo delays cannot grow an unbounded original-payload queue.
236
+ if (!unconfirmed.has(payload) && unconfirmedBytes + payload.length > 2 * opts.maxSyncPayloadLength) {
237
+ unconfirmed.set(payload, baseCursor);
238
+ unconfirmedBytes += payload.length;
239
+ unsentBase = baseCursor;
240
+ needsFullResync = true;
241
+ needsReopen = true;
242
+ pendingChanged();
243
+ // Stop collecting immediately; notify/reconfigure the host only after
244
+ // the native edit which exceeded the budget has left EditorView.update.
245
+ replayReady = false;
246
+ queueMicrotask(() => {
247
+ if (!isDisposed) terminate({ code: "BACKPRESSURE", message: "Websocket backpressure", docId: activeDocId });
248
+ });
249
+ return false;
250
+ }
251
+ if (!send({ type: "syncPublish", payload: { docId: activeDocId, payload, generation, baseCursor } })) return false;
252
+ if (!unconfirmed.has(payload)) unconfirmedBytes += payload.length;
253
+ unconfirmed.set(payload, baseCursor);
254
+ pendingChanged();
255
+ return true;
256
+ };
257
+
258
+ const sendAwarenessPublish = (data: Uint8Array): boolean =>
259
+ send({ type: "awarenessPublish", payload: { docId: activeDocId, payload: encoding.toBase64(data), generation } });
260
+
261
+ const sendReplayRequest = (fromCursor: string | null): boolean =>
262
+ send({ type: "replayRequest", payload: { docId: activeDocId, fromCursor, generation } });
263
+
264
+ const sendEventSubscribe = (): boolean => {
265
+ if (!opts.events) return false;
266
+ return send({ type: "subscribe", payload: { fromCursor: lastEventCursor } });
267
+ };
268
+
269
+ const sendLocalStateIfNeeded = () => {
270
+ if (!replayReady || localStateSent) return;
271
+ if (needsFullResync) {
272
+ // The full state keeps its original base; replay must never relabel stale
273
+ // unconfirmed input with the server's newly delivered cursor.
274
+ const localState = Y.encodeStateAsUpdate(doc);
275
+ if (localState.length > 0 && !sendSyncPublish(localState)) return;
276
+ covering.clear();
277
+ covering.set(encoding.toBase64(localState), [...unconfirmed.keys()]);
278
+ } else {
279
+ // Preserve the existing payload-sized resend batches and original bytes.
280
+ const groups: { payloads: string[]; size: number; baseCursor: string | null }[] = [];
281
+ for (const [payload, baseCursor] of unconfirmed) {
282
+ if (!groups.length || groups.at(-1)!.size + payload.length > opts.maxSyncPayloadLength)
283
+ groups.push({ payloads: [], size: 0, baseCursor });
284
+ const group = groups.at(-1)!;
285
+ group.payloads.push(payload);
286
+ group.size += payload.length;
287
+ }
288
+ covering.clear();
289
+ for (const group of groups) {
290
+ const payload =
291
+ group.payloads.length === 1 ? group.payloads[0]! : encoding.toBase64(Y.mergeUpdates(group.payloads.map(encoding.fromBase64)));
292
+ covering.set(payload, group.payloads);
293
+ if (
294
+ generation === null ||
295
+ !send({ type: "syncPublish", payload: { docId: activeDocId, payload, generation, baseCursor: group.baseCursor } })
296
+ )
297
+ return;
298
+ }
299
+ }
300
+ localStateSent = true;
301
+ if (needsFullResync) {
302
+ needsFullResync = false;
303
+ pendingChanged();
304
+ }
305
+ };
306
+
307
+ const terminate = (error: SharedTextProviderError) => {
308
+ if (isTerminated) return;
309
+ if (error.reopen) needsReopen = true;
310
+ isTerminated = true;
311
+ actionHistory?.cancelPending(new Error(error.message));
312
+ replayReady = false;
313
+ localStateSent = false;
314
+ if (reconnectTimer) {
315
+ clearTimeout(reconnectTimer);
316
+ reconnectTimer = null;
317
+ }
318
+ if (ws && ws.readyState <= WebSocket.OPEN) {
319
+ try {
320
+ ws.close();
321
+ } catch {
322
+ // Ignore close failures. Terminal flow is already enforced via isTerminated.
323
+ }
324
+ }
325
+ opts.onFatal?.(error);
326
+ };
327
+
328
+ const onDocUpdate = (update: Uint8Array, origin: unknown) => {
329
+ if (isDisposed) return;
330
+ if (origin === "remote" || actionHistory?.isRemoteOrigin(origin)) return;
331
+ recoveryText = doc.getText("codemirror").toString();
332
+ if (opts.onLocalUpdate) {
333
+ const baseCursor = unsentBase === undefined ? (unconfirmed.size ? unconfirmed.values().next().value! : lastCursor) : unsentBase;
334
+ opts.onLocalUpdate({ ...pendingState(), baseCursor });
335
+ }
336
+ if (isTerminated || !replayReady) {
337
+ if (unsentBase === undefined) unsentBase = unconfirmed.size ? unconfirmed.values().next().value : lastCursor;
338
+ needsFullResync = true;
339
+ pendingChanged();
340
+ return;
341
+ }
342
+ if (!sendSyncPublish(update)) {
343
+ if (unsentBase === undefined) unsentBase = unconfirmed.size ? unconfirmed.values().next().value : lastCursor;
344
+ needsFullResync = true;
345
+ pendingChanged();
346
+ }
347
+ };
348
+
349
+ const onAwarenessUpdate = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => {
350
+ if (isDisposed || isTerminated) return;
351
+ if (origin === "remote") return;
352
+ if (!replayReady) return;
353
+ if (!added.includes(doc.clientID) && !updated.includes(doc.clientID) && !removed.includes(doc.clientID)) return;
354
+ sendAwarenessPublish(awarenessProtocol.encodeAwarenessUpdate(awareness, [doc.clientID]));
355
+ };
356
+
357
+ const parseJsonMessage = (raw: string): SharedTextProviderMessage<Event, Participant> | null => {
358
+ try {
359
+ return opts.transport.decode(JSON.parse(raw));
360
+ } catch {
361
+ return null;
362
+ }
363
+ };
364
+
365
+ const handleProviderErrorMessage = (msg: SharedTextProviderMessage<Event, Participant>): boolean => {
366
+ if (msg.type !== "error") return false;
367
+ const error = msg.payload;
368
+ opts.onError?.(error);
369
+ if (error.reopen === true) {
370
+ needsReopen = true;
371
+ terminate(error);
372
+ return true;
373
+ }
374
+ if (error.code === "STORAGE_EXHAUSTED") {
375
+ storageFailures += 1;
376
+ if (storageFailures >= MAX_SHARED_TEXT_STORAGE_RETRIES) terminate(error);
377
+ return true;
378
+ }
379
+ if (error.code === "RESYNC_REQUIRED") {
380
+ lastCursor = null;
381
+ replayReady = false;
382
+ }
383
+ if (terminalErrorCodes.has(error.code)) {
384
+ terminate(error);
385
+ }
386
+ return true;
387
+ };
388
+
389
+ const handleEventErrorMessage = (msg: SharedTextProviderMessage<Event, Participant>): boolean => {
390
+ if (msg.type !== "eventError") return false;
391
+ const error = msg.payload;
392
+ if (error.code === "RESYNC_REQUIRED") lastEventCursor = null;
393
+ opts.events?.onError?.(error);
394
+ if (msg.terminal) terminate(error);
395
+ else ws?.close();
396
+ return true;
397
+ };
398
+
399
+ const handleEventReadyMessage = (msg: SharedTextProviderMessage<Event, Participant>): boolean => msg.type === "eventReady";
400
+
401
+ const handleEventMessage = (msg: SharedTextProviderMessage<Event, Participant>): boolean => {
402
+ if (msg.type !== "event") return false;
403
+ if (!opts.events) return true;
404
+ const { event, cursor } = msg.payload;
405
+ if (msg.payload.historyIncomplete) opts.onHistoryIncomplete?.();
406
+ const connection = eventStreamGeneration;
407
+ eventStreamEventQueue = eventStreamEventQueue
408
+ .then(async () => {
409
+ if (connection !== eventStreamGeneration || isDisposed || isTerminated) return;
410
+ if (!opts.events?.onEvent) throw new Error("Workspace event handler missing");
411
+ await opts.events.onEvent(event, cursor);
412
+ if (connection === eventStreamGeneration && cursor) {
413
+ lastEventCursor = cursor;
414
+ opts.events.onCursorChange?.(cursor);
415
+ }
416
+ })
417
+ .catch(() => {
418
+ if (connection !== eventStreamGeneration || isDisposed || isTerminated) return;
419
+ ws?.close();
420
+ });
421
+ return true;
422
+ };
423
+
424
+ const handleReplayReadyMessage = (msg: SharedTextProviderMessage<Event, Participant>): boolean => {
425
+ if (msg.type !== "replayReady") return false;
426
+ const payload = msg.payload;
427
+ // A socket can still deliver queued messages after a terminal error; the editor must stay stopped.
428
+ if (payload.docId !== activeDocId || isTerminated) return true;
429
+ if (typeof payload.generation !== "string" || !payload.generation || (generation !== null && generation !== payload.generation)) {
430
+ needsReopen = true;
431
+ terminate({
432
+ code: "RESYNC_REQUIRED",
433
+ message: opts.generationMessages?.changed ?? "The document changed. Copy unsent text before reopening it",
434
+ docId: activeDocId,
435
+ });
436
+ return true;
437
+ }
438
+ generation = payload.generation;
439
+ awarenessPeerId = typeof payload.peerId === "string" ? payload.peerId : null;
440
+ replayReady = true;
441
+ if (typeof payload.cursor === "string") lastCursor = payload.cursor;
442
+ // Reading works again, but with edits still unsent only their echo proves that storage accepts writes,
443
+ // so backoff and storage budget keep counting until then.
444
+ if (!needsFullResync && unconfirmed.size === 0) {
445
+ failedConnects = 0;
446
+ storageFailures = 0;
447
+ }
448
+ opts.onReady?.();
449
+ sendLocalStateIfNeeded();
450
+ sendAwarenessPublish(awarenessProtocol.encodeAwarenessUpdate(awareness, [doc.clientID]));
451
+ return true;
452
+ };
453
+
454
+ const handlePresenceMessage = (msg: SharedTextProviderMessage<Event, Participant>): boolean => {
455
+ if (msg.type !== "presence") return false;
456
+ if (isTerminated || msg.payload.docId !== activeDocId) return true;
457
+ if (msg.payload.generation !== undefined && msg.payload.generation !== generation) return true;
458
+ opts.onPresenceChange?.(msg.payload.participants);
459
+ return true;
460
+ };
461
+
462
+ const handlePushMessage = (msg: SharedTextProviderMessage<Event, Participant>): boolean => {
463
+ if (msg.type !== "syncPush" && msg.type !== "awarenessPush") return false;
464
+
465
+ const payload = msg.payload;
466
+
467
+ if (isTerminated || payload.docId !== activeDocId || !Array.isArray(payload.updates)) return true;
468
+ if (typeof payload.generation === "string") {
469
+ if ((!opts.allowUnversionedUpdates && !payload.generation) || (generation !== null && generation !== payload.generation)) {
470
+ needsReopen = true;
471
+ terminate({
472
+ code: "RESYNC_REQUIRED",
473
+ message: opts.generationMessages?.restored ?? "The document was restored. Copy unsent text before reopening it",
474
+ docId: activeDocId,
475
+ });
476
+ return true;
477
+ }
478
+ if (payload.snapshot === true || generation === null) generation = payload.generation;
479
+ }
480
+ if (msg.type === "syncPush" && payload.historyIncomplete === true) opts.onHistoryIncomplete?.();
481
+ const snapshotUpdates: Uint8Array[] = [];
482
+ let snapshotCursor: string | null = null;
483
+ for (const update of payload.updates) {
484
+ if (typeof update.payload !== "string") continue;
485
+ try {
486
+ if (
487
+ (!opts.allowUnversionedUpdates && !(update.generation ?? payload.generation)) ||
488
+ (generation !== null && typeof update.generation === "string" && update.generation !== generation)
489
+ ) {
490
+ if (generation !== null && update.generation !== undefined && opts.isEarlierGeneration?.(update.generation, generation)) {
491
+ // A server action from before a restore must never touch its replacement.
492
+ if (msg.type === "syncPush" && typeof update.cursor === "string") lastCursor = update.cursor;
493
+ continue;
494
+ }
495
+ needsReopen = true;
496
+ terminate({
497
+ code: "RESYNC_REQUIRED",
498
+ message: opts.generationMessages?.restored ?? "The document was restored. Copy unsent text before reopening it",
499
+ docId: activeDocId,
500
+ });
501
+ return true;
502
+ }
503
+ if (msg.type === "syncPush") {
504
+ if (payload.snapshot === true && actionHistory?.hasPending())
505
+ throw new Error("The table action could not be confirmed before reloading its history");
506
+ const bytes = encoding.fromBase64(update.payload);
507
+ // New adapters may put the lineage on each ordered update. Bind only
508
+ // after validating the first payload, before a later ready can label it.
509
+ if (generation === null && update.generation) {
510
+ Y.decodeUpdate(bytes);
511
+ generation = update.generation;
512
+ }
513
+ if (actionHistory)
514
+ actionHistory.apply(
515
+ bytes,
516
+ typeof update.actionId === "string" ? update.actionId : undefined,
517
+ typeof update.cursor === "string" ? update.cursor : undefined,
518
+ update.retainedColumn === true,
519
+ );
520
+ else {
521
+ Y.decodeUpdate(bytes);
522
+ Y.applyUpdate(doc, bytes, "remote");
523
+ }
524
+ if (payload.snapshot === true && opts.onSnapshot) {
525
+ snapshotUpdates.push(bytes);
526
+ if (typeof update.cursor === "string") snapshotCursor = update.cursor;
527
+ }
528
+ const covered = covering.get(update.payload);
529
+ const echoed = unconfirmed.has(update.payload) || covered !== undefined;
530
+ if (covered) {
531
+ for (const payload of covered) forgetPending(payload);
532
+ covering.delete(update.payload);
533
+ } else forgetPending(update.payload);
534
+ if (echoed) {
535
+ if (!unconfirmed.size) covering.clear();
536
+ failedConnects = 0;
537
+ storageFailures = 0;
538
+ if (!unconfirmed.size && !needsFullResync) {
539
+ unsentBase = undefined;
540
+ recoveryText = undefined;
541
+ }
542
+ pendingChanged();
543
+ }
544
+ } else {
545
+ if (awarenessPeerId !== null && update.originPeerId === awarenessPeerId) continue;
546
+ awarenessProtocol.applyAwarenessUpdate(awareness, encoding.fromBase64(update.payload), "remote");
547
+ }
548
+ } catch {
549
+ if (msg.type === "syncPush") {
550
+ const error = {
551
+ code: "INVALID_PAYLOAD",
552
+ message: "Shared editing stopped because an update could not be applied",
553
+ docId: activeDocId,
554
+ };
555
+ opts.onError?.(error);
556
+ terminate(error);
557
+ return true;
558
+ }
559
+ // Awareness does not advance document history.
560
+ }
561
+
562
+ if (msg.type === "syncPush" && typeof update.cursor === "string") {
563
+ lastCursor = update.cursor;
564
+ }
565
+ }
566
+
567
+ if (msg.type === "syncPush" && payload.snapshot === true && snapshotUpdates.length && generation !== null) {
568
+ opts.onSnapshot?.({ docId: activeDocId, generation, state: Y.mergeUpdates(snapshotUpdates), cursor: snapshotCursor });
569
+ }
570
+
571
+ return true;
572
+ };
573
+
574
+ const handleJsonMessage = (raw: string) => {
575
+ const msg = parseJsonMessage(raw);
576
+ if (!msg) return;
577
+
578
+ if (handleProviderErrorMessage(msg)) return;
579
+ if (handleEventErrorMessage(msg)) return;
580
+ if (handleEventReadyMessage(msg)) return;
581
+ if (handleEventMessage(msg)) return;
582
+ if (handleReplayReadyMessage(msg)) return;
583
+ if (handlePresenceMessage(msg)) return;
584
+ handlePushMessage(msg);
585
+ };
586
+
587
+ const connect = () => {
588
+ if (isDisposed || isTerminated) return;
589
+
590
+ eventStreamGeneration += 1;
591
+ eventStreamEventQueue = Promise.resolve();
592
+ const socket = new WebSocket(opts.transport.url);
593
+ ws = socket;
594
+
595
+ socket.onopen = () => {
596
+ if (ws !== socket) return;
597
+ opts.onConnectionChange?.(true);
598
+ replayReady = false;
599
+ localStateSent = false;
600
+ sendEventSubscribe();
601
+ sendReplayRequest(lastCursor);
602
+ };
603
+
604
+ socket.onmessage = (event) => {
605
+ if (ws !== socket) return;
606
+ if (typeof event.data === "string") handleJsonMessage(event.data);
607
+ };
608
+
609
+ socket.onclose = (event) => {
610
+ if (ws !== socket) return;
611
+ opts.onConnectionChange?.(false);
612
+ replayReady = false;
613
+ localStateSent = false;
614
+
615
+ const closeError = opts.transport.closeError(event.code, event.reason ?? "");
616
+ if (closeError) {
617
+ opts.onError?.(closeError);
618
+ terminate(closeError);
619
+ return;
620
+ }
621
+
622
+ // `STREAM_FAILED` and plain drops keep `lastCursor`: the next replay
623
+ // resumes after it instead of resending the stored snapshot. Only
624
+ // `RESYNC_REQUIRED` cleared the cursor above.
625
+ if (!isDisposed && !isTerminated) {
626
+ reconnectTimer = setTimeout(connect, opts.reconnectDelayMs(failedConnects));
627
+ failedConnects += 1;
628
+ }
629
+ };
630
+
631
+ socket.onerror = () => {
632
+ if (ws !== socket) return;
633
+ socket.close();
634
+ };
635
+ };
636
+
637
+ doc.on("update", onDocUpdate);
638
+ awareness.on("update", onAwarenessUpdate);
639
+
640
+ return {
641
+ connect,
642
+ /** Retry a dropped connection immediately, without reviving terminal sessions. */
643
+ reconnectNow: (): void => {
644
+ if (isDisposed || isTerminated || (ws && ws.readyState <= WebSocket.OPEN)) return;
645
+ if (reconnectTimer) {
646
+ clearTimeout(reconnectTimer);
647
+ reconnectTimer = null;
648
+ }
649
+ failedConnects = 0;
650
+ connect();
651
+ },
652
+ undo: () => {
653
+ if (actionHistory?.nextStructural("undo") && (isDisposed || isTerminated || !replayReady || needsFullResync || unconfirmed.size))
654
+ return Promise.reject(new Error("The editor must finish synchronizing before changing the table structure"));
655
+ return actionHistory?.undo() ?? Promise.resolve(null);
656
+ },
657
+ redo: () => {
658
+ if (actionHistory?.nextStructural("redo") && (isDisposed || isTerminated || !replayReady || needsFullResync || unconfirmed.size))
659
+ return Promise.reject(new Error("The editor must finish synchronizing before changing the table structure"));
660
+ return actionHistory?.redo() ?? Promise.resolve(null);
661
+ },
662
+ canUndo: () => actionHistory?.canUndo() ?? false,
663
+ canRedo: () => actionHistory?.canRedo() ?? false,
664
+ setInverseHandler: (handler: (request: SharedTextInverseRequest) => Promise<void>) => actionHistory?.setInverseHandler(handler),
665
+ /** Exporting state does not acknowledge durable storage, even when hasUnsentChanges() is false. */
666
+ pendingState,
667
+ recoveryInput: () => ({
668
+ state: Y.encodeStateAsUpdate(doc),
669
+ text: recoveryText ?? doc.getText("codemirror").toString(),
670
+ pending: [...unconfirmed].map(([payload, baseCursor]) => ({ payload, baseCursor })),
671
+ baseCursor: unsentBase,
672
+ }),
673
+ /** Server actions wait for replay coverage and all local updates to be echoed. */
674
+ canStructure: (): boolean =>
675
+ !isDisposed &&
676
+ !isTerminated &&
677
+ replayReady &&
678
+ generation !== null &&
679
+ !needsFullResync &&
680
+ unconfirmed.size === 0 &&
681
+ !!actionHistory &&
682
+ !actionHistory.hasPending(),
683
+ generation: (): string | null => generation,
684
+ beginAction: (actionId: string) => {
685
+ if (isDisposed || isTerminated || !replayReady || generation === null || needsFullResync || unconfirmed.size || !actionHistory)
686
+ throw new Error("The editor must finish synchronizing before changing the table structure");
687
+ return actionHistory.begin(actionId);
688
+ },
689
+ /** Local edits the server has not echoed yet. */
690
+ hasUnsentChanges: (): boolean => needsFullResync || unconfirmed.size > 0,
691
+ /** Reconnect after a terminal error, used after the person signs in again. */
692
+ resume: (): void => {
693
+ if (!isTerminated || isDisposed || needsReopen) return;
694
+ isTerminated = false;
695
+ failedConnects = 0;
696
+ connect();
697
+ },
698
+ dispose: () => {
699
+ isDisposed = true;
700
+ actionHistory?.dispose();
701
+ if (reconnectTimer) clearTimeout(reconnectTimer);
702
+ awarenessProtocol.removeAwarenessStates(awareness, [doc.clientID], "disconnect");
703
+ doc.off("update", onDocUpdate);
704
+ awareness.off("update", onAwarenessUpdate);
705
+ if (ws) {
706
+ ws.close();
707
+ ws = null;
708
+ }
709
+ },
710
+ };
711
+ }
712
+
713
+ /** Provider handle: transport controls, own history, generation and exact pending recovery state. */
714
+ export type SharedTextProvider = ReturnType<typeof createSharedTextProvider>;