@abloatai/ablo 0.36.0 → 0.37.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 (597) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +71 -2013
  3. package/NOTICE +2 -2
  4. package/README.md +25 -71
  5. package/assets/banner.png +0 -0
  6. package/dist/auth.d.ts +2 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +2 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/client.d.ts +3 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +3 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coordination.d.ts +2 -0
  15. package/dist/coordination.d.ts.map +1 -0
  16. package/dist/coordination.js +2 -0
  17. package/dist/coordination.js.map +1 -0
  18. package/dist/index.d.ts +3 -114
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -163
  21. package/dist/index.js.map +1 -0
  22. package/dist/react.d.ts +4 -0
  23. package/dist/react.d.ts.map +1 -0
  24. package/dist/react.js +3 -0
  25. package/dist/react.js.map +1 -0
  26. package/dist/schema.d.ts +2 -0
  27. package/dist/schema.d.ts.map +1 -0
  28. package/dist/schema.js +2 -0
  29. package/dist/schema.js.map +1 -0
  30. package/dist/server.d.ts +2 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +2 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/source-conformance.d.ts +2 -0
  35. package/dist/source-conformance.d.ts.map +1 -0
  36. package/dist/source-conformance.js +2 -0
  37. package/dist/source-conformance.js.map +1 -0
  38. package/dist/source-drizzle.d.ts +2 -0
  39. package/dist/source-drizzle.d.ts.map +1 -0
  40. package/dist/source-drizzle.js +2 -0
  41. package/dist/source-drizzle.js.map +1 -0
  42. package/dist/source-kysely.d.ts +2 -0
  43. package/dist/source-kysely.d.ts.map +1 -0
  44. package/dist/source-kysely.js +2 -0
  45. package/dist/source-kysely.js.map +1 -0
  46. package/dist/source-next.d.ts +2 -0
  47. package/dist/source-next.d.ts.map +1 -0
  48. package/dist/source-next.js +2 -0
  49. package/dist/source-next.js.map +1 -0
  50. package/dist/source.d.ts +2 -0
  51. package/dist/source.d.ts.map +1 -0
  52. package/dist/source.js +2 -0
  53. package/dist/source.js.map +1 -0
  54. package/dist/wire.d.ts +2 -0
  55. package/dist/wire.d.ts.map +1 -0
  56. package/dist/wire.js +2 -0
  57. package/dist/wire.js.map +1 -0
  58. package/docs/agents.md +1 -1
  59. package/docs/api-keys.md +7 -6
  60. package/docs/api.md +10 -10
  61. package/docs/client-behavior.md +5 -5
  62. package/docs/coordination.md +52 -62
  63. package/docs/data-sources.md +1 -1
  64. package/docs/examples/agent-human.md +4 -4
  65. package/docs/examples/ai-sdk-tool.md +1 -1
  66. package/docs/examples/existing-python-backend.md +15 -4
  67. package/docs/examples/nextjs.md +27 -6
  68. package/docs/examples/server-agent.md +2 -2
  69. package/docs/how-it-works.md +4 -4
  70. package/docs/identity.md +2 -1
  71. package/docs/integration-guide.md +24 -13
  72. package/docs/internal/README.md +18 -0
  73. package/docs/internal/agent-fleet-coordination-design.md +171 -0
  74. package/docs/internal/agent-orchestration.md +58 -0
  75. package/docs/internal/commit-identifiers.md +91 -0
  76. package/docs/internal/concurrency-open-decisions.md +37 -0
  77. package/docs/internal/data-source-reverse-channel.md +150 -0
  78. package/docs/internal/per-field-conflict-detection.md +165 -0
  79. package/docs/internal/postgres-replication.md +64 -0
  80. package/docs/internal/serializable-schema.md +119 -0
  81. package/docs/internal/structure.md +32 -0
  82. package/docs/mcp.md +2 -2
  83. package/docs/migration.md +3 -3
  84. package/docs/quickstart.md +2 -2
  85. package/docs/react.md +5 -5
  86. package/docs/schema-contract.md +3 -3
  87. package/docs/sessions.md +91 -37
  88. package/examples/README.md +2 -2
  89. package/examples/data-source/README.md +1 -1
  90. package/examples/data-source/ablo-driver.ts +1 -1
  91. package/examples/data-source/customer-server.ts +1 -1
  92. package/examples/data-source/run.ts +1 -1
  93. package/examples/data-source/schema.ts +1 -1
  94. package/examples/quickstart.ts +2 -2
  95. package/llms.txt +8 -8
  96. package/package.json +63 -166
  97. package/bin/ablo.cjs +0 -39
  98. package/dist/BaseSyncedStore.d.ts +0 -843
  99. package/dist/BaseSyncedStore.js +0 -1971
  100. package/dist/Database.d.ts +0 -323
  101. package/dist/Database.js +0 -1502
  102. package/dist/InstanceCache.d.ts +0 -237
  103. package/dist/InstanceCache.js +0 -1166
  104. package/dist/LazyReferenceCollection.d.ts +0 -177
  105. package/dist/LazyReferenceCollection.js +0 -461
  106. package/dist/Model.d.ts +0 -454
  107. package/dist/Model.js +0 -919
  108. package/dist/ModelRegistry.d.ts +0 -225
  109. package/dist/ModelRegistry.js +0 -539
  110. package/dist/NetworkMonitor.d.ts +0 -28
  111. package/dist/NetworkMonitor.js +0 -79
  112. package/dist/RuntimeContext.d.ts +0 -52
  113. package/dist/RuntimeContext.js +0 -80
  114. package/dist/SyncClient.d.ts +0 -541
  115. package/dist/SyncClient.js +0 -2202
  116. package/dist/adapters/alwaysOnline.d.ts +0 -14
  117. package/dist/adapters/alwaysOnline.js +0 -17
  118. package/dist/adapters/inMemoryStorage.d.ts +0 -31
  119. package/dist/adapters/inMemoryStorage.js +0 -110
  120. package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
  121. package/dist/ai-sdk/coordinatedTool.js +0 -134
  122. package/dist/ai-sdk/coordinationContext.d.ts +0 -46
  123. package/dist/ai-sdk/coordinationContext.js +0 -106
  124. package/dist/ai-sdk/index.d.ts +0 -121
  125. package/dist/ai-sdk/index.js +0 -121
  126. package/dist/ai-sdk/wrap.d.ts +0 -65
  127. package/dist/ai-sdk/wrap.js +0 -39
  128. package/dist/auth/index.d.ts +0 -1
  129. package/dist/auth/index.js +0 -8
  130. package/dist/batching/index.d.ts +0 -55
  131. package/dist/batching/index.js +0 -147
  132. package/dist/client/Ablo.d.ts +0 -231
  133. package/dist/client/Ablo.js +0 -160
  134. package/dist/client/abloClient.d.ts +0 -309
  135. package/dist/client/abloClient.js +0 -13
  136. package/dist/client/clientPrelude.d.ts +0 -52
  137. package/dist/client/clientPrelude.js +0 -60
  138. package/dist/client/consoleLogger.d.ts +0 -35
  139. package/dist/client/consoleLogger.js +0 -44
  140. package/dist/client/coreClient.d.ts +0 -60
  141. package/dist/client/coreClient.js +0 -118
  142. package/dist/client/createInternalComponents.d.ts +0 -50
  143. package/dist/client/createInternalComponents.js +0 -98
  144. package/dist/client/createModelProxy.d.ts +0 -248
  145. package/dist/client/createModelProxy.js +0 -884
  146. package/dist/client/humans.d.ts +0 -69
  147. package/dist/client/humans.js +0 -78
  148. package/dist/client/modelRegistration.d.ts +0 -10
  149. package/dist/client/modelRegistration.js +0 -312
  150. package/dist/client/options.d.ts +0 -461
  151. package/dist/client/options.js +0 -7
  152. package/dist/client/reactiveEngine.d.ts +0 -53
  153. package/dist/client/reactiveEngine.js +0 -688
  154. package/dist/client/resourceTypes.d.ts +0 -12
  155. package/dist/client/resourceTypes.js +0 -10
  156. package/dist/client/schemaConfig.d.ts +0 -44
  157. package/dist/client/schemaConfig.js +0 -185
  158. package/dist/client/storeCluster.d.ts +0 -47
  159. package/dist/client/storeCluster.js +0 -118
  160. package/dist/client/storeLifecycle.d.ts +0 -61
  161. package/dist/client/storeLifecycle.js +0 -231
  162. package/dist/client/validateAbloOptions.d.ts +0 -42
  163. package/dist/client/validateAbloOptions.js +0 -43
  164. package/dist/client/wsMutationExecutor.d.ts +0 -27
  165. package/dist/client/wsMutationExecutor.js +0 -72
  166. package/dist/context.d.ts +0 -42
  167. package/dist/context.js +0 -81
  168. package/dist/coordination/ClaimLog.d.ts +0 -26
  169. package/dist/coordination/ClaimLog.js +0 -32
  170. package/dist/coordination/index.d.ts +0 -1
  171. package/dist/coordination/index.js +0 -8
  172. package/dist/core/index.d.ts +0 -33
  173. package/dist/core/index.js +0 -48
  174. package/dist/docs/catalog.d.ts +0 -72
  175. package/dist/docs/catalog.js +0 -230
  176. package/dist/docs/index.d.ts +0 -10
  177. package/dist/docs/index.js +0 -10
  178. package/dist/environment.d.ts +0 -1
  179. package/dist/environment.js +0 -8
  180. package/dist/interfaces/index.d.ts +0 -311
  181. package/dist/interfaces/index.js +0 -9
  182. package/dist/keys/index.d.ts +0 -1
  183. package/dist/keys/index.js +0 -8
  184. package/dist/mutators/RecordingMutation.d.ts +0 -36
  185. package/dist/mutators/RecordingMutation.js +0 -182
  186. package/dist/mutators/Transaction.d.ts +0 -40
  187. package/dist/mutators/Transaction.js +0 -58
  188. package/dist/mutators/UndoManager.d.ts +0 -258
  189. package/dist/mutators/UndoManager.js +0 -658
  190. package/dist/mutators/defineMutators.d.ts +0 -60
  191. package/dist/mutators/defineMutators.js +0 -18
  192. package/dist/mutators/inverseOp.d.ts +0 -126
  193. package/dist/mutators/inverseOp.js +0 -71
  194. package/dist/mutators/mutateActions.d.ts +0 -45
  195. package/dist/mutators/mutateActions.js +0 -105
  196. package/dist/mutators/readerActions.d.ts +0 -33
  197. package/dist/mutators/readerActions.js +0 -57
  198. package/dist/mutators/undoApply.d.ts +0 -51
  199. package/dist/mutators/undoApply.js +0 -117
  200. package/dist/policy/index.d.ts +0 -21
  201. package/dist/policy/index.js +0 -20
  202. package/dist/query/QueryProcessor.d.ts +0 -75
  203. package/dist/query/QueryProcessor.js +0 -255
  204. package/dist/query/client.d.ts +0 -64
  205. package/dist/query/client.js +0 -138
  206. package/dist/query/types.d.ts +0 -85
  207. package/dist/query/types.js +0 -16
  208. package/dist/react/AbloProvider.d.ts +0 -242
  209. package/dist/react/AbloProvider.js +0 -458
  210. package/dist/react/ClientSideSuspense.d.ts +0 -36
  211. package/dist/react/ClientSideSuspense.js +0 -17
  212. package/dist/react/DefaultFallback.d.ts +0 -24
  213. package/dist/react/DefaultFallback.js +0 -43
  214. package/dist/react/context.d.ts +0 -55
  215. package/dist/react/context.js +0 -29
  216. package/dist/react/createAbloReact.d.ts +0 -56
  217. package/dist/react/createAbloReact.js +0 -51
  218. package/dist/react/index.d.ts +0 -62
  219. package/dist/react/index.js +0 -69
  220. package/dist/react/internalContext.d.ts +0 -33
  221. package/dist/react/internalContext.js +0 -3
  222. package/dist/react/useAblo.d.ts +0 -82
  223. package/dist/react/useAblo.js +0 -120
  224. package/dist/react/useCurrentUserId.d.ts +0 -22
  225. package/dist/react/useCurrentUserId.js +0 -34
  226. package/dist/react/useErrorListener.d.ts +0 -20
  227. package/dist/react/useErrorListener.js +0 -38
  228. package/dist/react/useMutationFailureListener.d.ts +0 -26
  229. package/dist/react/useMutationFailureListener.js +0 -38
  230. package/dist/react/useMutators.d.ts +0 -56
  231. package/dist/react/useMutators.js +0 -84
  232. package/dist/react/useReactive.d.ts +0 -35
  233. package/dist/react/useReactive.js +0 -123
  234. package/dist/react/useSyncStatus.d.ts +0 -59
  235. package/dist/react/useSyncStatus.js +0 -76
  236. package/dist/react/useUndoScope.d.ts +0 -34
  237. package/dist/react/useUndoScope.js +0 -81
  238. package/dist/schema/coordination.d.ts +0 -112
  239. package/dist/schema/coordination.js +0 -133
  240. package/dist/schema/ddl.d.ts +0 -97
  241. package/dist/schema/ddl.js +0 -491
  242. package/dist/schema/ddlLock.d.ts +0 -35
  243. package/dist/schema/ddlLock.js +0 -46
  244. package/dist/schema/diff.d.ts +0 -225
  245. package/dist/schema/diff.js +0 -289
  246. package/dist/schema/generate.d.ts +0 -19
  247. package/dist/schema/generate.js +0 -86
  248. package/dist/schema/index.d.ts +0 -42
  249. package/dist/schema/index.js +0 -80
  250. package/dist/schema/queries.d.ts +0 -201
  251. package/dist/schema/queries.js +0 -144
  252. package/dist/schema/select.d.ts +0 -40
  253. package/dist/schema/select.js +0 -90
  254. package/dist/schema/serialize.d.ts +0 -115
  255. package/dist/schema/serialize.js +0 -265
  256. package/dist/schema/sugar.d.ts +0 -109
  257. package/dist/schema/sugar.js +0 -83
  258. package/dist/schema/syncDeltaRow.d.ts +0 -6
  259. package/dist/schema/syncDeltaRow.js +0 -6
  260. package/dist/server/adapter.d.ts +0 -173
  261. package/dist/server/adapter.js +0 -18
  262. package/dist/server/commit.d.ts +0 -107
  263. package/dist/server/commit.js +0 -1
  264. package/dist/server/index.d.ts +0 -14
  265. package/dist/server/index.js +0 -2
  266. package/dist/server/readConfig.d.ts +0 -80
  267. package/dist/server/readConfig.js +0 -8
  268. package/dist/server/storageMode.d.ts +0 -23
  269. package/dist/server/storageMode.js +0 -17
  270. package/dist/source/adapter.d.ts +0 -83
  271. package/dist/source/adapter.js +0 -24
  272. package/dist/source/adapters/drizzle.d.ts +0 -48
  273. package/dist/source/adapters/drizzle.js +0 -219
  274. package/dist/source/adapters/kysely.d.ts +0 -42
  275. package/dist/source/adapters/kysely.js +0 -205
  276. package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
  277. package/dist/source/adapters/kyselyMutationCore.js +0 -125
  278. package/dist/source/adapters/memory.d.ts +0 -13
  279. package/dist/source/adapters/memory.js +0 -130
  280. package/dist/source/adapters/prisma.d.ts +0 -63
  281. package/dist/source/adapters/prisma.js +0 -202
  282. package/dist/source/conformance.d.ts +0 -37
  283. package/dist/source/conformance.js +0 -215
  284. package/dist/source/connector.d.ts +0 -95
  285. package/dist/source/connector.js +0 -266
  286. package/dist/source/connectorProtocol.d.ts +0 -154
  287. package/dist/source/connectorProtocol.js +0 -163
  288. package/dist/source/contract.d.ts +0 -195
  289. package/dist/source/contract.js +0 -164
  290. package/dist/source/factory.d.ts +0 -92
  291. package/dist/source/factory.js +0 -286
  292. package/dist/source/idempotency.d.ts +0 -61
  293. package/dist/source/idempotency.js +0 -144
  294. package/dist/source/index.d.ts +0 -23
  295. package/dist/source/index.js +0 -30
  296. package/dist/source/migrations.d.ts +0 -21
  297. package/dist/source/migrations.js +0 -103
  298. package/dist/source/next.d.ts +0 -32
  299. package/dist/source/next.js +0 -25
  300. package/dist/source/pushQueue.d.ts +0 -134
  301. package/dist/source/pushQueue.js +0 -256
  302. package/dist/source/signing.d.ts +0 -92
  303. package/dist/source/signing.js +0 -162
  304. package/dist/source/types.d.ts +0 -401
  305. package/dist/source/types.js +0 -59
  306. package/dist/storeContract.d.ts +0 -145
  307. package/dist/storeContract.js +0 -12
  308. package/dist/stores/DatabaseManager.d.ts +0 -107
  309. package/dist/stores/DatabaseManager.js +0 -388
  310. package/dist/stores/ObjectStore.d.ts +0 -115
  311. package/dist/stores/ObjectStore.js +0 -393
  312. package/dist/stores/ObjectStoreContract.d.ts +0 -38
  313. package/dist/stores/ObjectStoreContract.js +0 -1
  314. package/dist/stores/StoreManager.d.ts +0 -114
  315. package/dist/stores/StoreManager.js +0 -304
  316. package/dist/stores/SyncActionStore.d.ts +0 -99
  317. package/dist/stores/SyncActionStore.js +0 -506
  318. package/dist/stores/openIDBWithTimeout.d.ts +0 -65
  319. package/dist/stores/openIDBWithTimeout.js +0 -153
  320. package/dist/stores/syncAction.d.ts +0 -26
  321. package/dist/stores/syncAction.js +0 -16
  322. package/dist/surface.d.ts +0 -36
  323. package/dist/surface.js +0 -75
  324. package/dist/sync/BootstrapFetcher.d.ts +0 -284
  325. package/dist/sync/BootstrapFetcher.js +0 -964
  326. package/dist/sync/ConnectionManager.d.ts +0 -8
  327. package/dist/sync/ConnectionManager.js +0 -8
  328. package/dist/sync/OnDemandLoader.d.ts +0 -231
  329. package/dist/sync/OnDemandLoader.js +0 -743
  330. package/dist/sync/SubscriptionManager.d.ts +0 -159
  331. package/dist/sync/SubscriptionManager.js +0 -243
  332. package/dist/sync/SyncWebSocket.d.ts +0 -173
  333. package/dist/sync/SyncWebSocket.js +0 -438
  334. package/dist/sync/awaitClaimGrant.d.ts +0 -6
  335. package/dist/sync/awaitClaimGrant.js +0 -6
  336. package/dist/sync/bootstrapApply.d.ts +0 -73
  337. package/dist/sync/bootstrapApply.js +0 -73
  338. package/dist/sync/commitFrames.d.ts +0 -8
  339. package/dist/sync/commitFrames.js +0 -8
  340. package/dist/sync/contextPorts.d.ts +0 -18
  341. package/dist/sync/contextPorts.js +0 -31
  342. package/dist/sync/createClaimStream.d.ts +0 -7
  343. package/dist/sync/createClaimStream.js +0 -7
  344. package/dist/sync/createPresenceStream.d.ts +0 -69
  345. package/dist/sync/createPresenceStream.js +0 -200
  346. package/dist/sync/createSnapshot.d.ts +0 -29
  347. package/dist/sync/createSnapshot.js +0 -118
  348. package/dist/sync/credentialLifecycle.d.ts +0 -7
  349. package/dist/sync/credentialLifecycle.js +0 -7
  350. package/dist/sync/deltaPipeline.d.ts +0 -114
  351. package/dist/sync/deltaPipeline.js +0 -278
  352. package/dist/sync/groupChange.d.ts +0 -116
  353. package/dist/sync/groupChange.js +0 -244
  354. package/dist/sync/participants.d.ts +0 -132
  355. package/dist/sync/participants.js +0 -346
  356. package/dist/sync/persistedPrefix.d.ts +0 -12
  357. package/dist/sync/persistedPrefix.js +0 -22
  358. package/dist/sync/schemaDrift.d.ts +0 -55
  359. package/dist/sync/schemaDrift.js +0 -53
  360. package/dist/sync/schemas.d.ts +0 -71
  361. package/dist/sync/schemas.js +0 -94
  362. package/dist/sync/syncCursor.d.ts +0 -40
  363. package/dist/sync/syncCursor.js +0 -55
  364. package/dist/sync/syncPlan.d.ts +0 -54
  365. package/dist/sync/syncPlan.js +0 -50
  366. package/dist/sync/wsFrameHandlers.d.ts +0 -8
  367. package/dist/sync/wsFrameHandlers.js +0 -8
  368. package/dist/syncLog/contract.d.ts +0 -20
  369. package/dist/syncLog/contract.js +0 -19
  370. package/dist/syncLog/index.d.ts +0 -1
  371. package/dist/syncLog/index.js +0 -1
  372. package/dist/transaction/ablo.d.ts +0 -88
  373. package/dist/transaction/ablo.js +0 -33
  374. package/dist/transaction/auth/apiKey.d.ts +0 -152
  375. package/dist/transaction/auth/apiKey.js +0 -419
  376. package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
  377. package/dist/transaction/auth/bootstrapScope.js +0 -1
  378. package/dist/transaction/auth/capability.d.ts +0 -212
  379. package/dist/transaction/auth/capability.js +0 -224
  380. package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
  381. package/dist/transaction/auth/credentialEndpoint.js +0 -86
  382. package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
  383. package/dist/transaction/auth/credentialPolicy.js +0 -125
  384. package/dist/transaction/auth/credentialSource.d.ts +0 -30
  385. package/dist/transaction/auth/credentialSource.js +0 -55
  386. package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
  387. package/dist/transaction/auth/hostedEndpoints.js +0 -21
  388. package/dist/transaction/auth/identity.d.ts +0 -55
  389. package/dist/transaction/auth/identity.js +0 -210
  390. package/dist/transaction/auth/index.d.ts +0 -162
  391. package/dist/transaction/auth/index.js +0 -304
  392. package/dist/transaction/auth/schemas.d.ts +0 -59
  393. package/dist/transaction/auth/schemas.js +0 -85
  394. package/dist/transaction/auth/sessionMint.d.ts +0 -28
  395. package/dist/transaction/auth/sessionMint.js +0 -85
  396. package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -56
  397. package/dist/transaction/coordination/awaitClaimGrant.js +0 -124
  398. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -84
  399. package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -108
  400. package/dist/transaction/coordination/claimMeta.d.ts +0 -49
  401. package/dist/transaction/coordination/claimMeta.js +0 -52
  402. package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
  403. package/dist/transaction/coordination/createClaimStream.js +0 -475
  404. package/dist/transaction/coordination/events.d.ts +0 -74
  405. package/dist/transaction/coordination/events.js +0 -7
  406. package/dist/transaction/coordination/index.d.ts +0 -19
  407. package/dist/transaction/coordination/index.js +0 -45
  408. package/dist/transaction/coordination/locator.d.ts +0 -104
  409. package/dist/transaction/coordination/locator.js +0 -102
  410. package/dist/transaction/coordination/schema.d.ts +0 -1536
  411. package/dist/transaction/coordination/schema.js +0 -1177
  412. package/dist/transaction/coordination/targetConflict.d.ts +0 -2
  413. package/dist/transaction/coordination/targetConflict.js +0 -107
  414. package/dist/transaction/coordination/trace.d.ts +0 -78
  415. package/dist/transaction/coordination/trace.js +0 -138
  416. package/dist/transaction/durableWrites.d.ts +0 -62
  417. package/dist/transaction/durableWrites.js +0 -71
  418. package/dist/transaction/environment.d.ts +0 -105
  419. package/dist/transaction/environment.js +0 -108
  420. package/dist/transaction/errorCodes.d.ts +0 -403
  421. package/dist/transaction/errorCodes.js +0 -484
  422. package/dist/transaction/errors.d.ts +0 -428
  423. package/dist/transaction/errors.js +0 -686
  424. package/dist/transaction/footprint.d.ts +0 -111
  425. package/dist/transaction/footprint.js +0 -0
  426. package/dist/transaction/index.d.ts +0 -20
  427. package/dist/transaction/index.js +0 -20
  428. package/dist/transaction/keys/index.d.ts +0 -87
  429. package/dist/transaction/keys/index.js +0 -207
  430. package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
  431. package/dist/transaction/log/syncDeltaRow.js +0 -95
  432. package/dist/transaction/logPosition.d.ts +0 -97
  433. package/dist/transaction/logPosition.js +0 -125
  434. package/dist/transaction/logger.d.ts +0 -16
  435. package/dist/transaction/logger.js +0 -7
  436. package/dist/transaction/observability.d.ts +0 -53
  437. package/dist/transaction/observability.js +0 -19
  438. package/dist/transaction/persistence.d.ts +0 -12
  439. package/dist/transaction/persistence.js +0 -11
  440. package/dist/transaction/plugin.d.ts +0 -285
  441. package/dist/transaction/plugin.js +0 -106
  442. package/dist/transaction/policy/types.d.ts +0 -217
  443. package/dist/transaction/policy/types.js +0 -126
  444. package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
  445. package/dist/transaction/resources/functionalUpdate.js +0 -87
  446. package/dist/transaction/resources/httpResources.d.ts +0 -321
  447. package/dist/transaction/resources/httpResources.js +0 -7
  448. package/dist/transaction/resources/modelOperations.d.ts +0 -427
  449. package/dist/transaction/resources/modelOperations.js +0 -12
  450. package/dist/transaction/resources/mutationOptions.d.ts +0 -66
  451. package/dist/transaction/resources/mutationOptions.js +0 -9
  452. package/dist/transaction/resources/where.d.ts +0 -101
  453. package/dist/transaction/resources/where.js +0 -115
  454. package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
  455. package/dist/transaction/resources/writeOptionsSchema.js +0 -73
  456. package/dist/transaction/schema/field.d.ts +0 -120
  457. package/dist/transaction/schema/field.js +0 -265
  458. package/dist/transaction/schema/fieldRef.d.ts +0 -38
  459. package/dist/transaction/schema/fieldRef.js +0 -11
  460. package/dist/transaction/schema/loadStrategy.d.ts +0 -45
  461. package/dist/transaction/schema/loadStrategy.js +0 -46
  462. package/dist/transaction/schema/model.d.ts +0 -379
  463. package/dist/transaction/schema/model.js +0 -123
  464. package/dist/transaction/schema/openapi.d.ts +0 -58
  465. package/dist/transaction/schema/openapi.js +0 -501
  466. package/dist/transaction/schema/relation.d.ts +0 -204
  467. package/dist/transaction/schema/relation.js +0 -104
  468. package/dist/transaction/schema/residency.d.ts +0 -29
  469. package/dist/transaction/schema/residency.js +0 -25
  470. package/dist/transaction/schema/roles.d.ts +0 -249
  471. package/dist/transaction/schema/roles.js +0 -230
  472. package/dist/transaction/schema/schema.d.ts +0 -351
  473. package/dist/transaction/schema/schema.js +0 -325
  474. package/dist/transaction/schema/tenancy.d.ts +0 -139
  475. package/dist/transaction/schema/tenancy.js +0 -190
  476. package/dist/transaction/transactionLayer.d.ts +0 -82
  477. package/dist/transaction/transactionLayer.js +0 -24
  478. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
  479. package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
  480. package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
  481. package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
  482. package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
  483. package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
  484. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
  485. package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
  486. package/dist/transaction/transport/commitFrames.d.ts +0 -90
  487. package/dist/transaction/transport/commitFrames.js +0 -134
  488. package/dist/transaction/transport/connectionManager.d.ts +0 -215
  489. package/dist/transaction/transport/connectionManager.js +0 -673
  490. package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
  491. package/dist/transaction/transport/credentialLifecycle.js +0 -324
  492. package/dist/transaction/transport/heartbeat.d.ts +0 -65
  493. package/dist/transaction/transport/heartbeat.js +0 -93
  494. package/dist/transaction/transport/httpClient.d.ts +0 -131
  495. package/dist/transaction/transport/httpClient.js +0 -146
  496. package/dist/transaction/transport/httpOptions.d.ts +0 -33
  497. package/dist/transaction/transport/httpOptions.js +0 -12
  498. package/dist/transaction/transport/httpTransport.d.ts +0 -8
  499. package/dist/transaction/transport/httpTransport.js +0 -1388
  500. package/dist/transaction/transport/networkProbe.d.ts +0 -84
  501. package/dist/transaction/transport/networkProbe.js +0 -207
  502. package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
  503. package/dist/transaction/transport/wsFrameHandlers.js +0 -429
  504. package/dist/transaction/transport/wsTransport.d.ts +0 -574
  505. package/dist/transaction/transport/wsTransport.js +0 -1023
  506. package/dist/transaction/types/assertExact.d.ts +0 -17
  507. package/dist/transaction/types/assertExact.js +0 -1
  508. package/dist/transaction/types/global.d.ts +0 -107
  509. package/dist/transaction/types/global.js +0 -40
  510. package/dist/transaction/types/index.d.ts +0 -205
  511. package/dist/transaction/types/index.js +0 -56
  512. package/dist/transaction/types/modelData.d.ts +0 -10
  513. package/dist/transaction/types/modelData.js +0 -9
  514. package/dist/transaction/types/participant.d.ts +0 -20
  515. package/dist/transaction/types/participant.js +0 -10
  516. package/dist/transaction/types/streams.d.ts +0 -550
  517. package/dist/transaction/types/streams.js +0 -11
  518. package/dist/transaction/utils/asyncIterator.d.ts +0 -34
  519. package/dist/transaction/utils/asyncIterator.js +0 -135
  520. package/dist/transaction/utils/duration.d.ts +0 -50
  521. package/dist/transaction/utils/duration.js +0 -77
  522. package/dist/transaction/utils/json.d.ts +0 -57
  523. package/dist/transaction/utils/json.js +0 -276
  524. package/dist/transaction/wire/accountResponses.d.ts +0 -420
  525. package/dist/transaction/wire/accountResponses.js +0 -290
  526. package/dist/transaction/wire/auth.d.ts +0 -56
  527. package/dist/transaction/wire/auth.js +0 -63
  528. package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
  529. package/dist/transaction/wire/bootstrapReason.js +0 -8
  530. package/dist/transaction/wire/claimEvent.d.ts +0 -76
  531. package/dist/transaction/wire/claimEvent.js +0 -73
  532. package/dist/transaction/wire/claims.d.ts +0 -530
  533. package/dist/transaction/wire/claims.js +0 -327
  534. package/dist/transaction/wire/commit.d.ts +0 -603
  535. package/dist/transaction/wire/commit.js +0 -321
  536. package/dist/transaction/wire/delta.d.ts +0 -250
  537. package/dist/transaction/wire/delta.js +0 -147
  538. package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
  539. package/dist/transaction/wire/errorEnvelope.js +0 -123
  540. package/dist/transaction/wire/feedCursor.d.ts +0 -60
  541. package/dist/transaction/wire/feedCursor.js +0 -82
  542. package/dist/transaction/wire/feedEvent.d.ts +0 -204
  543. package/dist/transaction/wire/feedEvent.js +0 -65
  544. package/dist/transaction/wire/frames.d.ts +0 -194
  545. package/dist/transaction/wire/frames.js +0 -50
  546. package/dist/transaction/wire/inboundFrames.d.ts +0 -562
  547. package/dist/transaction/wire/inboundFrames.js +0 -116
  548. package/dist/transaction/wire/index.d.ts +0 -54
  549. package/dist/transaction/wire/index.js +0 -83
  550. package/dist/transaction/wire/listEnvelope.d.ts +0 -37
  551. package/dist/transaction/wire/listEnvelope.js +0 -42
  552. package/dist/transaction/wire/modelMutations.d.ts +0 -31
  553. package/dist/transaction/wire/modelMutations.js +0 -52
  554. package/dist/transaction/wire/modelResponses.d.ts +0 -85
  555. package/dist/transaction/wire/modelResponses.js +0 -43
  556. package/dist/transaction/wire/modelShape.d.ts +0 -78
  557. package/dist/transaction/wire/modelShape.js +0 -74
  558. package/dist/transaction/wire/protocol.d.ts +0 -38
  559. package/dist/transaction/wire/protocol.js +0 -38
  560. package/dist/transaction/wire/protocolVersion.d.ts +0 -73
  561. package/dist/transaction/wire/protocolVersion.js +0 -83
  562. package/dist/transactions/mutations/MutationQueue.d.ts +0 -661
  563. package/dist/transactions/mutations/MutationQueue.js +0 -2807
  564. package/dist/transactions/mutations/MutationStore.d.ts +0 -20
  565. package/dist/transactions/mutations/MutationStore.js +0 -53
  566. package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
  567. package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
  568. package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
  569. package/dist/transactions/mutations/coalesceRules.js +0 -140
  570. package/dist/transactions/mutations/commitLatency.d.ts +0 -52
  571. package/dist/transactions/mutations/commitLatency.js +0 -130
  572. package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
  573. package/dist/transactions/mutations/commitOutboxStore.js +0 -26
  574. package/dist/transactions/mutations/commitPayload.d.ts +0 -165
  575. package/dist/transactions/mutations/commitPayload.js +0 -152
  576. package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -63
  577. package/dist/transactions/mutations/deltaConfirmation.js +0 -235
  578. package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
  579. package/dist/transactions/mutations/durableWriteStore.js +0 -12
  580. package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
  581. package/dist/transactions/mutations/optimisticApply.js +0 -65
  582. package/dist/transactions/mutations/replayValidation.d.ts +0 -187
  583. package/dist/transactions/mutations/replayValidation.js +0 -164
  584. package/dist/utils/mobxSetup.d.ts +0 -53
  585. package/dist/utils/mobxSetup.js +0 -330
  586. package/dist/views/QueryView.d.ts +0 -79
  587. package/dist/views/QueryView.js +0 -218
  588. package/dist/views/ViewRegistry.d.ts +0 -20
  589. package/dist/views/ViewRegistry.js +0 -55
  590. package/dist/views/incrementalView.d.ts +0 -45
  591. package/dist/views/incrementalView.js +0 -69
  592. package/dist/webhooks/events.d.ts +0 -43
  593. package/dist/webhooks/events.js +0 -42
  594. package/dist/webhooks/index.d.ts +0 -8
  595. package/dist/webhooks/index.js +0 -8
  596. package/dist/wire/index.d.ts +0 -1
  597. package/dist/wire/index.js +0 -8
@@ -1,1023 +0,0 @@
1
- /**
2
- * The duplex transport: a WebSocket connection to the sync server, extracted
3
- * out of the reactive engine's `SyncWebSocket` (ADR 0016). It owns the socket
4
- * lifecycle (connect, reconnect with exponential backoff, disconnect, the
5
- * application-level heartbeat), sends commits, claims, releases, and
6
- * subscription updates over the one connection, correlates their
7
- * acknowledgement frames back to awaiting callers, and dispatches every other
8
- * inbound frame through {@link dispatchWsFrame}.
9
- *
10
- * What it deliberately does not do is materialise: deltas, sync responses,
11
- * and bootstrap payloads are surfaced through protected frame hooks
12
- * ({@link handleDelta} and its siblings) whose defaults just emit, so a
13
- * server-side caller gets the push feed — claim grants, losses, deltas —
14
- * with no store, no cursor, and no renderer. The reactive engine subclasses
15
- * this and overrides the hooks with validation, cursor advancement, and
16
- * bootstrap handling. The membership test (ADR 0016): a caller with no socket
17
- * loses only push — it polls instead; a caller with no reactive layer loses
18
- * the local copy it never wanted.
19
- */
20
- import { EventEmitter } from 'events';
21
- import { AbloConnectionError, AbloError, AbloSessionError, AbloValidationError, toAbloError, } from '../errors.js';
22
- import { participantClaimPayloadSchema, updateSubscriptionPayloadSchema, } from '../coordination/schema.js';
23
- import { PROTOCOL_VERSION, WS_CLOSE_PROTOCOL_VERSION } from '../wire/protocolVersion.js';
24
- import { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL, } from '../auth/credentialSource.js';
25
- import { buildCommitFrame } from './commitFrames.js';
26
- import { dispatchWsFrame, readWsInboundFrame, } from './wsFrameHandlers.js';
27
- import { HeartbeatController } from './heartbeat.js';
28
- import { noopLogger } from '../logger.js';
29
- import { noopSocketObservability, } from '../observability.js';
30
- /**
31
- * Ceiling for the exponential reconnect backoff (`reconnectDelay * 2^n`,
32
- * ±15% jitter). A client-side setting, not part of the wire contract.
33
- */
34
- const MAX_RECONNECT_DELAY_MS = 30_000;
35
- // ---------------------------------------------------------------------------
36
- // Consumers pass their own event types as the TCollaboration generic parameter.
37
- export class WsTransport extends EventEmitter {
38
- /**
39
- * Subscribe to events with automatic cleanup.
40
- * Returns unsubscribe function for clean disposal.
41
- */
42
- subscribe(event, handler) {
43
- this.on(event, handler);
44
- return () => this.off(event, handler);
45
- }
46
- /**
47
- * Send a collaboration event (app-specific real-time message).
48
- * The wire format is `{ type: messageType, payload: { ...payload, timestamp } }`.
49
- */
50
- sendCollaborationEvent(messageType, payload) {
51
- if (this.ws?.readyState !== WebSocket.OPEN)
52
- return;
53
- this.send({
54
- type: messageType.replace(/:/g, '_'), // 'document:selection' → 'document_selection' wire format
55
- payload: { ...payload, timestamp: Date.now() },
56
- });
57
- }
58
- ws = null;
59
- options;
60
- /** The transport's reporting ports, shared with the subclassing engine. */
61
- logger;
62
- observability;
63
- onlineStatus;
64
- reconnectAttempts = 0;
65
- /** Stop retrying after this many consecutive failures (backoff caps at 30s, so ~7.5 min total) */
66
- static MAX_RECONNECT_ATTEMPTS = 15;
67
- reconnectTimer = null;
68
- /**
69
- * Application-level heartbeat: ping every 30 seconds and force-close after a
70
- * 10-second silence. The {@link HeartbeatController} holds the timing and the
71
- * zombie-socket rationale; the closures below are the only socket access it
72
- * gets.
73
- */
74
- heartbeat;
75
- isConnecting = false;
76
- isManualClose = false;
77
- /** True while the owner still holds the first connection (`deferConnect`).
78
- * `connect()` is ignored until {@link allowConnect} lifts the hold. */
79
- connectHeld;
80
- /** When true, a session error has been detected (from any path — WS close or HTTP bootstrap).
81
- * Suppresses reconnection and Sentry error capture to avoid cascading noise. */
82
- _sessionErrorDetected = false;
83
- /** True once `onopen` has fired at least once on the current socket. Reset each
84
- * time a new socket is created in `connect()`. Used by `onclose` to detect
85
- * handshake failures (close before open) — the one signal we have for "the
86
- * server rejected the upgrade" since browsers hide the HTTP status (e.g.
87
- * 401) behind the opaque 1006 close code. */
88
- _everOpened = false;
89
- /**
90
- * Diagnostic snapshot of the last connection lifecycle. Persisted across
91
- * the lifetime of the transport so that any subsequent "not connected"
92
- * rejection can quote the actual root cause (close code + reason + when)
93
- * instead of bottoming out at a generic error string. Browser WS code 1006
94
- * hides the real reason, so we layer on our own signals: `forceCloseReason`
95
- * captures heartbeat trips / send failures, `everOpened` distinguishes
96
- * handshake reject from mid-session drop, and `sessionErrorAt` tells us
97
- * whether reconnect is suppressed.
98
- */
99
- lastOpenAt = null;
100
- lastCloseAt = null;
101
- lastCloseCode = null;
102
- lastCloseReason = null;
103
- lastForceCloseReason = null;
104
- sessionErrorAt = null;
105
- /** Registered collaboration event keys (colon format) for dispatch in onmessage */
106
- collaborationEventTypes;
107
- /**
108
- * A minimal session adapter handed to the inbound frame dispatch table
109
- * ({@link dispatchWsFrame}). It exposes only the members the handlers touch;
110
- * the closure members read live state so a reassignment here (for example the
111
- * `pendingSubscriptions` reset on close) cannot strand a handler on a stale
112
- * reference. Built in the constructor, after the state it captures exists.
113
- */
114
- frameSession;
115
- /**
116
- * In-flight `commit` mutation requests keyed by clientTxId. Resolved when
117
- * a matching `mutation_result` frame arrives from the server, or rejected on
118
- * timeout / disconnect. Lets consumers await a server ack for mutations
119
- * sent over the same socket that streams deltas.
120
- */
121
- pendingMutations = new Map();
122
- /**
123
- * In-flight `claim` requests keyed by claimId. Resolved when the matching
124
- * `claim_ack` arrives, or rejected on timeout or disconnect — the same
125
- * request/response pattern as `pendingMutations`, multiplexed over the one
126
- * connection.
127
- */
128
- pendingClaims = new Map();
129
- /**
130
- * In-flight `update_subscription` frames awaiting `subscription_ack`.
131
- * A FIFO queue rather than a keyed Map because the wire ack carries no
132
- * correlation id — the server applies subscription updates in receive
133
- * order and acks in the same order, so `shift()` on ack matches the
134
- * oldest pending request. (Read-interest changes are infrequent and
135
- * usually settle before the next one, so depth is ~1 in practice.)
136
- */
137
- pendingSubscriptions = [];
138
- constructor(options) {
139
- super();
140
- // Construct the WebSocket URL from the base server URL.
141
- const baseUrl = options.baseUrl || options.url || "http://localhost:8080";
142
- const wsProtocol = baseUrl.startsWith('https') ? 'wss' : 'ws';
143
- const wsUrl = baseUrl.replace(/^https?/, wsProtocol) + '/api/sync/ws';
144
- const { logger, observability, onlineStatus, deferConnect, ...connectionOptions } = options;
145
- this.logger = logger ?? noopLogger;
146
- this.observability = observability ?? noopSocketObservability;
147
- this.onlineStatus = onlineStatus ?? { isOnline: () => true };
148
- this.connectHeld = deferConnect === true;
149
- this.options = {
150
- url: wsUrl,
151
- reconnectDelay: 1000,
152
- maxReconnectDelay: MAX_RECONNECT_DELAY_MS,
153
- collaborationEvents: [],
154
- syncGroups: [],
155
- lastSyncId: 0,
156
- userId: '',
157
- organizationId: '',
158
- capabilities: {
159
- partialBootstrap: true,
160
- compressedDeltas: true,
161
- streamingBootstrap: true,
162
- batchedDeltas: true,
163
- },
164
- ...connectionOptions,
165
- };
166
- this.heartbeat = new HeartbeatController({
167
- isSocketOpen: () => this.ws?.readyState === WebSocket.OPEN,
168
- // Optional-chained rather than asserted: the controller only calls
169
- // this synchronously after `isSocketOpen()`, so `ws` is present.
170
- sendPing: () => {
171
- this.ws?.send(JSON.stringify({ type: 'ping' }));
172
- },
173
- forceClose: (reason) => { this.forceClose(reason); },
174
- }, this.observability);
175
- this.collaborationEventTypes = new Set(options.collaborationEvents ?? []);
176
- // Session slice for the inbound frame dispatch table — see the field doc on
177
- // `frameSession` for why reassigned members are exposed through closures
178
- // instead of captured references. The four materialisation handlers route
179
- // through the protected hooks, so a subclass's overrides win.
180
- this.frameSession = {
181
- emit: (event, ...args) => this.emit(event, ...args),
182
- logger: this.logger,
183
- observability: this.observability,
184
- pendingMutations: this.pendingMutations,
185
- pendingClaims: this.pendingClaims,
186
- shiftPendingSubscription: () => this.pendingSubscriptions.shift(),
187
- options: this.options,
188
- collaborationEventTypes: this.collaborationEventTypes,
189
- handleDelta: (delta) => { this.handleDelta(delta); },
190
- handleSyncResponse: (payload) => { this.handleSyncResponse(payload); },
191
- handleBootstrapResponse: (payload) => { this.handleBootstrapResponse(payload); },
192
- handlePresenceUpdate: (message) => {
193
- this.handlePresenceUpdate(message);
194
- },
195
- };
196
- }
197
- // ── Materialisation hooks ─────────────────────────────────────────────
198
- //
199
- // The frame dispatch routes deltas, sync responses, bootstrap payloads, and
200
- // presence frames through these protected hooks. The defaults surface the
201
- // raw push feed as events — enough for a socketed agent that wants claim
202
- // push and change notifications without a local copy. The reactive engine
203
- // overrides them with wire validation, cursor advancement, and bootstrap
204
- // handling; anything it does not override keeps the transport default.
205
- /**
206
- * One inbound delta, straight off the wire and unvalidated. The default
207
- * emits it as-is; the reactive engine's override validates against the
208
- * canonical delta schema and drops anything malformed before emitting.
209
- */
210
- handleDelta(rawDelta) {
211
- this.emit('delta', rawDelta);
212
- }
213
- /** A `sync_response` frame. Meaningless without a resume cursor to advance,
214
- * so the transport default does nothing. */
215
- handleSyncResponse(_payload) { }
216
- /** A `bootstrap_response` frame. Bootstrap is materialisation, so the
217
- * transport default does nothing. */
218
- handleBootstrapResponse(_payload) { }
219
- /**
220
- * Handles a presence update from the server. The wire frame's payload is
221
- * forwarded as-is, so every consumer reads the same shape; stripping fields
222
- * here would drop `kind`, `activity`, `syncGroups`, and `isAgent` for
223
- * consumers that need them.
224
- *
225
- * The wire frame is:
226
- * { type: 'presence_update', payload: { kind, userId, status,
227
- * syncGroups, activity, isAgent, timestamp, activeClaims } }
228
- */
229
- handlePresenceUpdate(
230
- // Typed as a partial presence event as well as an envelope, because both
231
- // shapes genuinely arrive: the server's canonical `{ payload: {...} }`,
232
- // and legacy pathways (test fixtures) that put the fields at the top
233
- // level. Declaring the union up front is what lets the fallback below
234
- // stay a plain read instead of a checked-off cast.
235
- message) {
236
- const event =
237
- // Server canonical path: `{ payload: {...} }`. Some legacy
238
- // pathways emit fields at the top level (test fixtures) — fall
239
- // back to reading from the message itself.
240
- message.payload ?? message;
241
- this.emit('presence_update', event);
242
- }
243
- /**
244
- * Runs after the socket opens and `connected` is emitted, before the
245
- * heartbeat starts. The default does nothing; the reactive engine's
246
- * override runs its open ritual — presence, ack, incremental sync, and the
247
- * catch-up poll.
248
- */
249
- onOpened() { }
250
- /**
251
- * Runs inside the close handler, after the socket reference is cleared and
252
- * before in-flight requests are rejected. The default does nothing; the
253
- * reactive engine's override stops its catch-up poll.
254
- */
255
- onClosed() { }
256
- /**
257
- * The resume position sent as the `cursor` query parameter on the upgrade.
258
- * The transport itself holds no cursor — a bare connection resumes from
259
- * nothing — so the default is empty; the reactive engine's override supplies
260
- * its persisted sync cursor.
261
- */
262
- resumeCursor() {
263
- return '';
264
- }
265
- /**
266
- * Mark that a session error has been detected (e.g. 401 from HTTP bootstrap).
267
- * Suppresses further reconnection attempts and Sentry error capture.
268
- */
269
- setSessionErrorDetected() {
270
- this._sessionErrorDetected = true;
271
- this.sessionErrorAt = Date.now();
272
- }
273
- /**
274
- * Clear the session-error latch so `connect()` / `scheduleReconnect()`
275
- * work again. Called by the store's access-credential recovery path when
276
- * the close was a re-mintable `ek_`/`rk_` expiry (`4001 credential_expired`),
277
- * not a login loss — see `isAccessCredentialExpiryCloseReason`. Genuine
278
- * session losses never clear the latch; re-auth builds a fresh client.
279
- */
280
- clearSessionError() {
281
- this._sessionErrorDetected = false;
282
- this.sessionErrorAt = null;
283
- }
284
- /**
285
- * Lift the `deferConnect` hold. The owner calls this once the connection's
286
- * identity and read scope are seeded; from then on `connect()` works
287
- * normally, including every reconnect path.
288
- */
289
- allowConnect() {
290
- this.connectHeld = false;
291
- }
292
- /**
293
- * Connect to the sync engine WebSocket
294
- */
295
- connect() {
296
- if (this.connectHeld) {
297
- // Deliberately ignored, not queued: the decided answer to "connect()
298
- // before the host has seeded identity" is a no-op, so an early caller
299
- // can never open an unscoped connection (see deferConnect).
300
- this.logger.debug('WebSocket connect ignored — the connection is still held by its host');
301
- return;
302
- }
303
- if (this._sessionErrorDetected) {
304
- this.logger.debug('WebSocket connect suppressed — session error detected');
305
- return;
306
- }
307
- // CLOSING counts as busy: the socket's close teardown is still in
308
- // flight and its `onclose` (which runs `scheduleReconnect`) hasn't
309
- // fired yet. Overwriting `this.ws` mid-teardown is what produced the
310
- // orphaned-socket race — see the stale-socket guards in
311
- // `setupEventHandlers`.
312
- if (this.ws?.readyState === WebSocket.OPEN ||
313
- this.ws?.readyState === WebSocket.CLOSING ||
314
- this.isConnecting) {
315
- this.logger.debug('WebSocket already connected, connecting, or closing');
316
- return;
317
- }
318
- // Note: onlineStatus is advisory — we'll try to connect and let the WebSocket
319
- // handle failures. The default browser implementation reads navigator.onLine,
320
- // which is unreliable but the only signal available; in Node it returns true
321
- // (assume online) so the sidecar/agent path doesn't short-circuit here.
322
- if (!this.onlineStatus.isOnline()) {
323
- this.logger.debug('onlineStatus reports offline, but attempting connection anyway');
324
- }
325
- this.isConnecting = true;
326
- this.isManualClose = false;
327
- // One credential, server-resolved identity. The bearer travels in a
328
- // `Sec-WebSocket-Protocol` value (built below), not the URL. The server is
329
- // bearer-only and resolves identity from the verified token; userId and
330
- // organizationId are never read from URL parameters.
331
- const params = new URLSearchParams({
332
- // Intentionally omit lastSyncId, capabilities from URL; these are sent in sync_request
333
- // and ack messages to avoid stale baselines on reconnect.
334
- cursor: this.resumeCursor(),
335
- });
336
- // Participant kind — defaults to `user` for session connections. Agent
337
- // runtimes pass `'agent'` so the server's capability-token path activates
338
- // instead of session auth.
339
- if (this.options.kind && this.options.kind !== 'user') {
340
- params.set('kind', this.options.kind);
341
- }
342
- // Add sync groups if provided
343
- this.options.syncGroups.forEach((group) => {
344
- params.append('syncGroups', group);
345
- });
346
- const wsUrl = `${this.options.url}?${params.toString()}`;
347
- // Carry the bearer in a `Sec-WebSocket-Protocol` value, not the URL. A
348
- // browser cannot set an Authorization header on a WebSocket, but it can
349
- // offer subprotocols — and unlike the query string, those do not land in
350
- // load-balancer access logs, proxies, or browser history. The server reads
351
- // `ablo.bearer.<token>` and selects the real `ablo.sync.v1` protocol, never
352
- // echoing the token-bearing value back. The token is the raw `ek_`/`rk_`,
353
- // which is safe as a subprotocol value (alphanumerics and `_`).
354
- const authToken = this.resolveAuthToken();
355
- const protocols = authToken
356
- ? [`${WS_BEARER_SUBPROTOCOL_PREFIX}${authToken}`, WS_SYNC_SUBPROTOCOL]
357
- : [WS_SYNC_SUBPROTOCOL];
358
- try {
359
- // Reset the handshake flag before wiring the new socket. Each connect()
360
- // gets its own lifecycle — a prior successful open on a previous socket
361
- // must not mask a handshake failure on the new one.
362
- this._everOpened = false;
363
- this.ws = new WebSocket(wsUrl, protocols);
364
- this.setupEventHandlers();
365
- }
366
- catch (error) {
367
- // WebSocket constructor can throw if URL is invalid
368
- const errorMessage = error instanceof Error ? error.message : 'Failed to create WebSocket';
369
- this.observability.captureWebSocketError({ context: 'create-websocket', error: errorMessage });
370
- this.isConnecting = false;
371
- this.emit('error', new AbloConnectionError(errorMessage, { cause: error }));
372
- this.scheduleReconnect();
373
- }
374
- }
375
- /**
376
- * Setup WebSocket event handlers
377
- */
378
- setupEventHandlers() {
379
- // Capture the socket this call wires. Every handler below guards on
380
- // `this.ws === socket` (onclose additionally tolerates a nulled field —
381
- // see there), so a handler firing late, after `connect()` has replaced the
382
- // socket, can never clobber the new connection's shared state. Without this
383
- // guard, an old socket's `onclose` would unconditionally run `this.ws =
384
- // null; onClosed(); stopHeartbeat()` — a reconnect during close
385
- // teardown then orphaned the fresh socket (a zombie receiving deltas with
386
- // no timers and broken send paths).
387
- const socket = this.ws;
388
- if (!socket)
389
- return;
390
- socket.onopen = () => {
391
- if (this.ws !== socket)
392
- return; // stale socket — a newer connect() owns the state
393
- this.observability.breadcrumb('WebSocket connected', 'sync.websocket', 'info', {
394
- reconnectAttempts: this.reconnectAttempts,
395
- });
396
- this.isConnecting = false;
397
- this.reconnectAttempts = 0;
398
- this._everOpened = true;
399
- this.lastOpenAt = Date.now();
400
- this.emit('connected');
401
- // The subclass's open ritual (presence, ack, incremental sync,
402
- // catch-up poll) runs here, between the `connected` emit and the
403
- // heartbeat start — the exact position the inline code held before
404
- // the split.
405
- this.onOpened();
406
- // Start the application-level heartbeat (see HeartbeatController).
407
- this.heartbeat.start();
408
- };
409
- socket.onmessage = (event) => {
410
- if (this.ws !== socket)
411
- return; // stale socket — drop, don't feed shared state
412
- try {
413
- // Untrusted wire input: parse to `unknown`, then narrow through
414
- // the frame-envelope guard before dispatch. Payload-level
415
- // validation (deltas etc.) happens per-frame downstream.
416
- const message = JSON.parse(event.data);
417
- // Any inbound frame proves the socket is alive — clear the
418
- // heartbeat-timeout timer so we don't false-trip force-close
419
- // during normal traffic.
420
- this.heartbeat.clearHeartbeatTimeout();
421
- const frame = readWsInboundFrame(message);
422
- if (!frame) {
423
- this.logger.debug('[WsTransport] dropped malformed wire frame', {
424
- received: Array.isArray(message) ? 'array' : typeof message,
425
- });
426
- return;
427
- }
428
- // Dispatch by frame type (see dispatchWsFrame). The session adapter
429
- // exposes only the members the handlers touch; keepalives, the older
430
- // bare-delta form, and collaboration events are all routed there too.
431
- dispatchWsFrame(this.frameSession, frame);
432
- }
433
- catch (error) {
434
- this.observability.captureWebSocketError({
435
- context: 'parse-message',
436
- error: error instanceof Error ? error.message : String(error),
437
- });
438
- }
439
- };
440
- socket.onerror = (_event) => {
441
- if (this.ws !== socket)
442
- return; // stale socket — its errors are no longer ours
443
- // WebSocket errors are DOM Events, not Error objects
444
- // Check if we're offline first
445
- if (!this.onlineStatus.isOnline()) {
446
- this.observability.breadcrumb('WebSocket error: Network is offline', 'sync.websocket', 'warning');
447
- this.emit('error', new AbloConnectionError('Network is offline', { code: 'bootstrap_offline' }));
448
- return;
449
- }
450
- // After a session error, suppress error capture — the root cause is
451
- // already reported. Still emit so the store can update UI state.
452
- const error = new AbloConnectionError(`WebSocket connection failed`);
453
- if (!this._sessionErrorDetected) {
454
- this.observability.captureWebSocketError({
455
- context: 'connection-error',
456
- error: error.message,
457
- });
458
- }
459
- this.emit('error', error);
460
- };
461
- socket.onclose = (event) => {
462
- // Stale-socket close: a newer socket already owns the connection
463
- // state — don't null it, stop its timers, or schedule a duplicate
464
- // reconnect (the orphaning race this guard exists for). The one
465
- // deliberate asymmetry vs the other handlers: `this.ws === null`
466
- // (manual `disconnect()` nulls the field before the close event
467
- // lands) still runs the full body, so in-flight work is rejected
468
- // promptly and 'disconnected' reaches consumers — the pre-guard
469
- // behavior manual close always had.
470
- if (this.ws !== null && this.ws !== socket)
471
- return;
472
- const everOpened = this._everOpened;
473
- this.lastCloseAt = Date.now();
474
- this.lastCloseCode = event.code;
475
- this.lastCloseReason = event.reason || null;
476
- this.logger.info('WebSocket closed', {
477
- code: event.code,
478
- reason: event.reason,
479
- everOpened,
480
- reconnectAttempts: this.reconnectAttempts,
481
- forceCloseReason: this.lastForceCloseReason,
482
- msSinceOpen: this.lastOpenAt != null ? Date.now() - this.lastOpenAt : null,
483
- isManualClose: this.isManualClose,
484
- });
485
- this.isConnecting = false;
486
- this.ws = null;
487
- this.onClosed();
488
- this.heartbeat.stop();
489
- // Cancel in-flight mutations — the socket that was carrying them is
490
- // gone, and the server-side state may or may not have accepted each
491
- // one. Rejecting promptly is better than hanging the caller forever;
492
- // higher-level retry belongs to MutationQueue, not here.
493
- if (this.pendingMutations.size > 0) {
494
- for (const pending of this.pendingMutations.values()) {
495
- clearTimeout(pending.timeout);
496
- // AbloConnectionError → `isPermanentError` treats it as transient,
497
- // so MutationQueue retries the commit on reconnect rather than
498
- // rolling it back. `diagnostics` is preserved as a property (the
499
- // queue's failure log walks the cause chain for it).
500
- pending.reject(Object.assign(new AbloConnectionError(`WebSocket closed while commit was in flight (code=${event.code}` +
501
- (event.reason ? ` reason=${event.reason}` : '') +
502
- (this.lastForceCloseReason
503
- ? ` forceCloseReason=${this.lastForceCloseReason}`
504
- : '') +
505
- ')', { code: 'commit_no_result' }), { diagnostics: this.getConnectionDiagnostics() }));
506
- }
507
- this.pendingMutations.clear();
508
- }
509
- // Cancel in-flight claims — same rationale. Server-side
510
- // claims are bound to the connection; a reconnect will need
511
- // to re-claim. Higher-level retry belongs to whoever holds
512
- // the participant handle (typically the SDK's claim manager).
513
- if (this.pendingClaims.size > 0) {
514
- for (const pending of this.pendingClaims.values()) {
515
- clearTimeout(pending.timeout);
516
- pending.reject(new AbloConnectionError(`WebSocket closed while claim was in flight (code=${event.code})`));
517
- }
518
- this.pendingClaims.clear();
519
- }
520
- // Cancel in-flight subscription updates — the reconnect handshake
521
- // re-sends `options.syncGroups` (the last acked interest) in the
522
- // upgrade URL, so a pending change that never acked is simply
523
- // retried by the caller against the fresh connection.
524
- if (this.pendingSubscriptions.length > 0) {
525
- for (const pending of this.pendingSubscriptions) {
526
- clearTimeout(pending.timeout);
527
- pending.reject(new AbloConnectionError(`WebSocket closed while update_subscription was in flight (code=${event.code})`));
528
- }
529
- this.pendingSubscriptions = [];
530
- }
531
- // Protocol-version rejection (4010): terminal. Reconnecting cannot heal a
532
- // version mismatch — only upgrading the SDK, or rolling the server
533
- // forward, can — so a blind retry here would loop forever against the
534
- // same typed close. Surface it and stop.
535
- if (event.code === WS_CLOSE_PROTOCOL_VERSION) {
536
- this.observability.captureWebSocketError({
537
- context: 'protocol-version-close',
538
- code: event.code,
539
- reason: event.reason,
540
- });
541
- this.emit('protocol_mismatch', event);
542
- this.emit('disconnected', event);
543
- return;
544
- }
545
- // Check for session-related close codes
546
- // 1008 = Policy Violation (often auth)
547
- // 4001 = Unauthorized (custom)
548
- // 4003 = Forbidden (custom)
549
- const isSessionClose = event.code === 1008 ||
550
- event.code === 4001 ||
551
- event.code === 4003 ||
552
- AbloSessionError.isSessionError(event.reason || '');
553
- if (isSessionClose) {
554
- this._sessionErrorDetected = true;
555
- this.sessionErrorAt = Date.now();
556
- this.observability.captureWebSocketError({
557
- context: 'session-error-close',
558
- code: event.code,
559
- reason: event.reason,
560
- });
561
- this.emit('session_error', new AbloSessionError(event.reason || 'Session expired', event.code));
562
- // Don't reconnect from here. For a genuine session loss the user must
563
- // re-authenticate; for an expired access credential (`credential_expired`)
564
- // the store's session-error handler re-mints, clears the latch, and
565
- // drives the reconnect itself.
566
- this.emit('disconnected', event);
567
- return;
568
- }
569
- // Handshake failure: `onclose` fired before `onopen` ever did, so the
570
- // server rejected the upgrade (typically 401/403 on a bad cookie, but it
571
- // could also be a CORS/origin reject or a load-balancer 5xx). The browser
572
- // hides the HTTP status behind code 1006, so we cannot tell which from
573
- // here.
574
- //
575
- // Emit a dedicated event and skip the internal reconnect — the owner
576
- // should run an auth-validating HTTP probe to distinguish session expiry
577
- // from a transient network issue and transition the UI accordingly.
578
- // Reconnecting blindly is what produced the infinite
579
- // "offline → reconnecting → offline" loop on stale cookies.
580
- if (!everOpened && !this.isManualClose) {
581
- this.observability.captureWebSocketError({
582
- context: 'handshake-failed-close',
583
- code: event.code,
584
- reason: event.reason,
585
- });
586
- this.emit('handshake_failed', event);
587
- this.emit('disconnected', event);
588
- return;
589
- }
590
- this.emit('disconnected', event);
591
- // Reconnect if not manually closed
592
- if (!this.isManualClose) {
593
- this.scheduleReconnect();
594
- }
595
- };
596
- }
597
- /**
598
- * Send message to server
599
- */
600
- send(message) {
601
- if (this.ws?.readyState !== WebSocket.OPEN) {
602
- // Only log at debug level when offline - this is expected behavior, not an error
603
- if (this.onlineStatus.isOnline()) {
604
- this.observability.breadcrumb('WebSocket not connected, cannot send message', 'sync.websocket', 'warning');
605
- }
606
- else {
607
- this.logger.debug('WebSocket send skipped - offline');
608
- }
609
- return;
610
- }
611
- try {
612
- this.ws.send(JSON.stringify(message));
613
- }
614
- catch (error) {
615
- // Only log as error if we're online - offline send failures are expected
616
- if (this.onlineStatus.isOnline()) {
617
- this.observability.captureWebSocketError({
618
- context: 'send-message',
619
- error: error instanceof Error ? error.message : String(error),
620
- });
621
- }
622
- else {
623
- this.logger.debug('WebSocket send failed - offline');
624
- }
625
- }
626
- }
627
- /**
628
- * Sends a `commit` mutation request over the existing WebSocket and resolves
629
- * when the server's `mutation_result` frame comes back with the same
630
- * `clientTxId`. The wire frame is `{ type: 'commit', payload: { operations,
631
- * clientTxId } }`.
632
- *
633
- * Times out after 15 seconds of silence from the server. The socket may close
634
- * during an in-flight mutation (a network flap, a server restart); this does
635
- * not auto-retry — the caller's transaction queue owns retry and offline
636
- * replay, and the SDK does not duplicate that logic.
637
- */
638
- sendCommit(operations, clientTxId, timeoutMs = 15_000, reads, track) {
639
- if (this.ws?.readyState !== WebSocket.OPEN) {
640
- return Promise.reject(this.notConnectedError('commit'));
641
- }
642
- return new Promise((resolve, reject) => {
643
- const timeout = setTimeout(() => {
644
- this.pendingMutations.delete(clientTxId);
645
- reject(new AbloConnectionError(`commit timed out after ${timeoutMs}ms (clientTxId=${clientTxId})`, { code: 'commit_no_result' }));
646
- }, timeoutMs);
647
- this.pendingMutations.set(clientTxId, { resolve, reject, timeout });
648
- try {
649
- const frame = buildCommitFrame(operations, clientTxId, reads, track);
650
- this.ws.send(JSON.stringify(frame));
651
- }
652
- catch (error) {
653
- clearTimeout(timeout);
654
- this.pendingMutations.delete(clientTxId);
655
- reject(toAbloError(error));
656
- }
657
- });
658
- }
659
- /**
660
- * Send a commit frame without waiting for `mutation_result`.
661
- *
662
- * This backs the public `wait: 'queued'` API: the socket accepted the
663
- * frame for delivery, but the server has not confirmed it yet. The
664
- * eventual `mutation_result` frame is intentionally ignored by this
665
- * instance because no pending resolver is registered.
666
- */
667
- sendCommitQueued(operations, clientTxId, reads, track) {
668
- if (this.ws?.readyState !== WebSocket.OPEN) {
669
- throw this.notConnectedError('commit');
670
- }
671
- const frame = buildCommitFrame(operations, clientTxId, reads, track);
672
- this.ws.send(JSON.stringify(frame));
673
- }
674
- /**
675
- * Activates a participant claim on this connection. One connection can hold
676
- * several concurrent claims at once, each scoped to a different set of sync
677
- * groups, so the SDK reuses the existing connection instead of opening a
678
- * separate socket per scope.
679
- *
680
- * Returns a promise that resolves with the server-canonicalized `syncGroups`
681
- * and effective `ttlSeconds` once `claim_ack` arrives, or rejects with a typed
682
- * error on a failed ack, a timeout, or a disconnect.
683
- */
684
- sendClaim(claimId, syncGroups, options) {
685
- if (this.ws?.readyState !== WebSocket.OPEN) {
686
- return Promise.reject(this.notConnectedError('claim'));
687
- }
688
- // Checked against the schema the server ingests it with, for the same
689
- // reason `updateSubscription` below is: the two frames name their scopes
690
- // identically, so a group that would be refused there is refused here, at
691
- // the call that asked for it, rather than coming back as a failed ack a
692
- // round trip later with nothing to point at.
693
- const payload = participantClaimPayloadSchema.safeParse({
694
- claimId,
695
- syncGroups: [...syncGroups],
696
- capabilityToken: options?.capabilityToken,
697
- ttlSeconds: options?.ttlSeconds,
698
- });
699
- if (!payload.success) {
700
- return Promise.reject(new AbloValidationError(`join was given a sync group the protocol does not accept: ${payload.error.issues[0]?.message ?? 'unreadable'}. A group is 'default' or 'kind:id'.`, { code: 'malformed_claim' }));
701
- }
702
- const timeoutMs = options?.timeoutMs ?? 15_000;
703
- return new Promise((resolve, reject) => {
704
- const timeout = setTimeout(() => {
705
- this.pendingClaims.delete(claimId);
706
- reject(new AbloConnectionError(`claim timed out after ${timeoutMs}ms (claimId=${claimId})`, {
707
- code: 'wait_for_timeout',
708
- }));
709
- }, timeoutMs);
710
- this.pendingClaims.set(claimId, { resolve, reject, timeout });
711
- try {
712
- this.ws.send(JSON.stringify({ type: 'claim', payload: payload.data }));
713
- }
714
- catch (error) {
715
- clearTimeout(timeout);
716
- this.pendingClaims.delete(claimId);
717
- reject(toAbloError(error));
718
- }
719
- });
720
- }
721
- /**
722
- * Drop a previously-active claim. Idempotent — `release` is
723
- * fire-and-forget per the wire contract; the server accepts
724
- * unknown claimIds silently so disconnect-time release storms
725
- * never error. No ack is expected.
726
- *
727
- * If a claim's send promise is still pending (no claim_ack yet),
728
- * we reject it locally — the user explicitly chose to release.
729
- */
730
- sendRelease(claimId) {
731
- // Cancel any in-flight claim that hadn't acked yet — the user
732
- // changed their mind. Without this the timer would eventually
733
- // reject; doing it now matches the user's claim immediately.
734
- const pending = this.pendingClaims.get(claimId);
735
- if (pending) {
736
- clearTimeout(pending.timeout);
737
- this.pendingClaims.delete(claimId);
738
- pending.reject(new AbloError(`claim ${claimId} released before ack`, {
739
- code: 'claim_wait_aborted',
740
- httpStatus: 409,
741
- }));
742
- }
743
- if (this.ws?.readyState !== WebSocket.OPEN)
744
- return;
745
- try {
746
- this.ws.send(JSON.stringify({ type: 'release', payload: { claimId } }));
747
- }
748
- catch {
749
- // Idempotent contract — silent failure is acceptable here.
750
- }
751
- }
752
- /**
753
- * Moves this connection's read interest — replaces the connection-level sync
754
- * groups mid-session as the user opens and closes entities. This is the
755
- * area-of-interest navigation primitive: the server fans out deltas only for
756
- * the groups currently in view, rather than the fixed set chosen at connect.
757
- *
758
- * This is a full-set replace: pass the complete new group list, not a delta.
759
- * Resolves with the server's effective set once `subscription_ack` arrives;
760
- * rejects (with a typed error) on a scope denial (a restricted `rk_` key
761
- * requesting a group outside its allowlist), a timeout, or a disconnect. On
762
- * success the new set is recorded as `options.syncGroups`, so a later reconnect
763
- * re-subscribes to the current interest rather than the connect-time set.
764
- *
765
- * Distinct from {@link sendClaim} (a write claim, per operation, with a TTL):
766
- * this is the read side, carries no capability token of its own, and is
767
- * bounded by the connection credential's grant.
768
- */
769
- updateSubscription(syncGroups, options) {
770
- if (this.ws?.readyState !== WebSocket.OPEN) {
771
- return Promise.reject(this.notConnectedError('update_subscription'));
772
- }
773
- const timeoutMs = options?.timeoutMs ?? 15_000;
774
- return new Promise((resolve, reject) => {
775
- const entry = {
776
- resolve,
777
- reject,
778
- timeout: setTimeout(() => {
779
- const idx = this.pendingSubscriptions.indexOf(entry);
780
- if (idx !== -1)
781
- this.pendingSubscriptions.splice(idx, 1);
782
- reject(new AbloConnectionError(`update_subscription timed out after ${timeoutMs}ms`, { code: 'wait_for_timeout' }));
783
- }, timeoutMs),
784
- };
785
- // Check the payload against the schema the server ingests it with, so a
786
- // malformed sync group fails here — naming the group and the call that
787
- // asked for it — instead of coming back as a rejection ack a round trip
788
- // later, detached from the code that caused it.
789
- const payload = updateSubscriptionPayloadSchema.safeParse({
790
- syncGroups: [...syncGroups],
791
- });
792
- if (!payload.success) {
793
- clearTimeout(entry.timeout);
794
- reject(new AbloValidationError(`update_subscription was given a sync group the protocol does not accept: ${payload.error.issues[0]?.message ?? 'unreadable'}. A group is 'default' or 'kind:id'.`, { code: 'malformed_subscription' }));
795
- return;
796
- }
797
- this.pendingSubscriptions.push(entry);
798
- try {
799
- this.ws.send(JSON.stringify({ type: 'update_subscription', payload: payload.data }));
800
- }
801
- catch (error) {
802
- clearTimeout(entry.timeout);
803
- const idx = this.pendingSubscriptions.indexOf(entry);
804
- if (idx !== -1)
805
- this.pendingSubscriptions.splice(idx, 1);
806
- reject(toAbloError(error));
807
- }
808
- });
809
- }
810
- /**
811
- * Sets a fixed credential for callers that construct the socket directly. The
812
- * SDK instead supplies `getAuthToken`, so reconnects read the shared
813
- * credential source rather than this copied value.
814
- */
815
- setCapabilityToken(token) {
816
- this.options.capabilityToken = token;
817
- }
818
- /**
819
- * Seeds the participant kind after identity resolution. The kind rides the
820
- * upgrade URL and selects the server's auth path, and on the hosted path it
821
- * is derived from the credential's scope — known only once identity
822
- * resolves, after the socket is built. Call before `connect()`.
823
- */
824
- setKind(kind) {
825
- this.options.kind = kind;
826
- }
827
- getAuthToken() {
828
- return this.resolveAuthToken();
829
- }
830
- /**
831
- * Return the credential that will be used by the next WebSocket upgrade.
832
- * ConnectionManager reads this for HTTP auth probes so visibility/network
833
- * checks authenticate the same way reconnects do.
834
- */
835
- getCapabilityToken() {
836
- return this.resolveAuthToken();
837
- }
838
- resolveAuthToken = () => {
839
- return this.options.getAuthToken?.()
840
- ?? this.options.getCapabilityToken?.()
841
- ?? this.options.capabilityToken;
842
- };
843
- /**
844
- * Schedule reconnection with exponential backoff
845
- */
846
- scheduleReconnect() {
847
- if (this.reconnectTimer) {
848
- clearTimeout(this.reconnectTimer);
849
- }
850
- // Session error means the user needs to re-authenticate — don't reconnect.
851
- if (this._sessionErrorDetected) {
852
- return;
853
- }
854
- // Don't attempt reconnection while offline. The owning store manages the
855
- // offline→online transition: it bootstraps first, then calls `connect()`
856
- // explicitly. Self-reconnecting here would bypass that bootstrap gate and
857
- // surface stale data.
858
- if (!this.onlineStatus.isOnline()) {
859
- this.emit('reconnecting', { attempt: this.reconnectAttempts + 1, delay: 0 });
860
- return;
861
- }
862
- // Give up after MAX_RECONNECT_ATTEMPTS consecutive failures. The user can
863
- // recover by refreshing, or the store resets the attempt count and
864
- // reconnects when the network returns.
865
- if (this.reconnectAttempts >= WsTransport.MAX_RECONNECT_ATTEMPTS) {
866
- this.emit('reconnect_failed', { attempts: this.reconnectAttempts });
867
- return;
868
- }
869
- // Exponential backoff with ±15% jitter to prevent thundering herd
870
- const baseDelay = Math.min(this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts), this.options.maxReconnectDelay);
871
- const jitter = baseDelay * (0.85 + Math.random() * 0.3);
872
- const delay = Math.round(jitter);
873
- // Emit reconnecting event so UI can show reconnection status
874
- this.emit('reconnecting', { attempt: this.reconnectAttempts + 1, delay });
875
- this.reconnectTimer = setTimeout(() => {
876
- this.reconnectAttempts++;
877
- this.connect();
878
- }, delay);
879
- }
880
- /**
881
- * Reset reconnect attempt counter. Called when network comes back online
882
- * to allow a fresh reconnect cycle after the max was previously reached.
883
- */
884
- resetReconnectAttempts() {
885
- this.reconnectAttempts = 0;
886
- }
887
- /**
888
- * Disconnect from WebSocket
889
- */
890
- disconnect() {
891
- this.isManualClose = true;
892
- this.heartbeat.stop();
893
- if (this.reconnectTimer) {
894
- clearTimeout(this.reconnectTimer);
895
- this.reconnectTimer = null;
896
- }
897
- if (this.ws) {
898
- this.ws.close(1000, 'Manual disconnect');
899
- this.ws = null;
900
- }
901
- }
902
- /**
903
- * Force-close the socket from the client side using a private 4xxx
904
- * code. Callers expect `onclose` to fire; that handler runs the
905
- * existing reconnect / handshake-failed dispatch. Wrapped in
906
- * try/catch because `close()` on a CLOSING/CLOSED socket throws on
907
- * some browsers.
908
- */
909
- forceClose(reason) {
910
- if (!this.ws)
911
- return;
912
- this.lastForceCloseReason = reason;
913
- this.logger.debug('[WsTransport] forceClose', {
914
- reason,
915
- readyState: this.ws.readyState,
916
- msSinceOpen: this.lastOpenAt != null ? Date.now() - this.lastOpenAt : null,
917
- });
918
- try {
919
- this.ws.close(4000, reason);
920
- }
921
- catch {
922
- // Already closing / closed — onclose will still fire.
923
- }
924
- }
925
- /**
926
- * Get connection state
927
- */
928
- isConnected() {
929
- return this.ws?.readyState === WebSocket.OPEN;
930
- }
931
- /**
932
- * Snapshot of recent connection lifecycle state, for diagnostic logs
933
- * and error messages. Cheap to call (no I/O); safe to log every time
934
- * a send is rejected so we can attribute "not connected" rejections
935
- * to the actual root cause (handshake reject vs heartbeat zombie vs
936
- * session expiry vs explicit close).
937
- */
938
- getConnectionDiagnostics() {
939
- const now = Date.now();
940
- return {
941
- readyState: this.ws?.readyState ?? null,
942
- isConnecting: this.isConnecting,
943
- isManualClose: this.isManualClose,
944
- sessionErrorDetected: this._sessionErrorDetected,
945
- everOpened: this._everOpened,
946
- reconnectAttempts: this.reconnectAttempts,
947
- maxReconnectAttempts: WsTransport.MAX_RECONNECT_ATTEMPTS,
948
- lastOpenAt: this.lastOpenAt,
949
- lastCloseAt: this.lastCloseAt,
950
- lastCloseCode: this.lastCloseCode,
951
- lastCloseReason: this.lastCloseReason,
952
- lastForceCloseReason: this.lastForceCloseReason,
953
- sessionErrorAt: this.sessionErrorAt,
954
- msSinceLastOpen: this.lastOpenAt != null ? now - this.lastOpenAt : null,
955
- msSinceLastClose: this.lastCloseAt != null ? now - this.lastCloseAt : null,
956
- };
957
- }
958
- /**
959
- * Build a richly-diagnosed "not connected" error so callers (and the
960
- * logs they emit) can attribute the rejection. The message embeds the
961
- * dominant signal in human-readable form; the structured detail is
962
- * also attached as `error.diagnostics` for log scrapers.
963
- */
964
- notConnectedError(action) {
965
- const d = this.getConnectionDiagnostics();
966
- // A session-latched socket is not a transient transport hiccup: reconnection
967
- // is suppressed until re-auth (or the store's credential re-mint clears the
968
- // latch), so retrying can never succeed. Reject with the permanent session
969
- // error type — `isPermanentError` surfaces it to the caller as
970
- // "re-authenticate" instead of parking the write for a reconnect that will
971
- // never happen.
972
- if (d.sessionErrorDetected) {
973
- return Object.assign(new AbloSessionError(`SyncWebSocket not connected — cannot send ${action}: session expired` +
974
- (d.lastCloseReason ? ` (${d.lastCloseReason})` : '') +
975
- '; re-authenticate'), { diagnostics: d });
976
- }
977
- let detail;
978
- if (d.isManualClose) {
979
- detail = 'manual_close';
980
- }
981
- else if (d.isConnecting) {
982
- detail = 'still_connecting';
983
- }
984
- else if (!d.everOpened && d.lastCloseAt != null) {
985
- detail = `handshake_failed code=${d.lastCloseCode}`;
986
- }
987
- else if (d.lastForceCloseReason) {
988
- detail = `force_closed reason=${d.lastForceCloseReason}`;
989
- }
990
- else if (d.lastCloseAt != null) {
991
- detail =
992
- `closed code=${d.lastCloseCode}` +
993
- (d.lastCloseReason ? ` reason=${d.lastCloseReason}` : '') +
994
- (d.msSinceLastClose != null ? ` ${d.msSinceLastClose}ms ago` : '') +
995
- (d.reconnectAttempts > 0
996
- ? ` reconnectAttempts=${d.reconnectAttempts}/${d.maxReconnectAttempts}`
997
- : '');
998
- }
999
- else {
1000
- detail = 'never_connected';
1001
- }
1002
- // Typed so it lands in the AbloError hierarchy and `isPermanentError` sees a
1003
- // transient transport failure (retry on reconnect, don't roll back).
1004
- // `diagnostics` stays a property — the queue's failure log walks the cause
1005
- // chain for it.
1006
- const err = Object.assign(new AbloConnectionError(`SyncWebSocket not connected — cannot send ${action} (${detail})`, { code: 'ws_not_ready' }), { diagnostics: d });
1007
- return err;
1008
- }
1009
- /** Returns the sync groups this connection is subscribed to. */
1010
- getSyncGroups() {
1011
- return this.options.syncGroups;
1012
- }
1013
- /**
1014
- * Seeds the connection's read interest — the sync groups the next upgrade
1015
- * URL carries. The set is already mutable state (`subscription_ack` writes
1016
- * the acked set back so a reconnect resubscribes to current interest);
1017
- * this setter is the host's way to seed it once identity resolves, before
1018
- * the first `connect()`.
1019
- */
1020
- setSyncGroups(syncGroups) {
1021
- this.options.syncGroups = [...syncGroups];
1022
- }
1023
- }