@myko/core 4.4.0 → 4.4.2

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 (360) hide show
  1. package/dist/client.d.ts +252 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +1316 -0
  4. package/dist/generated/CancelSubscription.d.ts +7 -0
  5. package/dist/generated/CancelSubscription.d.ts.map +1 -0
  6. package/{src/generated/ServerId.ts → dist/generated/CancelSubscription.js} +1 -2
  7. package/dist/generated/ChildEntities.d.ts +8 -0
  8. package/dist/generated/ChildEntities.d.ts.map +1 -0
  9. package/{src/generated/ClientId.ts → dist/generated/ChildEntities.js} +1 -2
  10. package/dist/generated/ChildEntitiesAllTime.d.ts +8 -0
  11. package/dist/generated/ChildEntitiesAllTime.d.ts.map +1 -0
  12. package/{src/generated/MEventType.ts → dist/generated/ChildEntitiesAllTime.js} +1 -2
  13. package/{src/generated/ClearClientWindbackTime.ts → dist/generated/ClearClientWindbackTime.d.ts} +1 -2
  14. package/dist/generated/ClearClientWindbackTime.d.ts.map +1 -0
  15. package/{src/generated/ClientCount.ts → dist/generated/ClearClientWindbackTime.js} +1 -2
  16. package/dist/generated/ClearClientWindbackTimeArgs.d.ts +2 -0
  17. package/dist/generated/ClearClientWindbackTimeArgs.d.ts.map +1 -0
  18. package/dist/generated/ClearClientWindbackTimeArgs.js +2 -0
  19. package/dist/generated/Client.d.ts +12 -0
  20. package/dist/generated/Client.d.ts.map +1 -0
  21. package/dist/generated/Client.js +1 -0
  22. package/dist/generated/ClientCount.d.ts +4 -0
  23. package/dist/generated/ClientCount.d.ts.map +1 -0
  24. package/dist/generated/ClientCount.js +2 -0
  25. package/dist/generated/ClientId.d.ts +2 -0
  26. package/dist/generated/ClientId.d.ts.map +1 -0
  27. package/dist/generated/ClientId.js +2 -0
  28. package/dist/generated/ClientStatus.d.ts +8 -0
  29. package/dist/generated/ClientStatus.d.ts.map +1 -0
  30. package/dist/generated/ClientStatus.js +1 -0
  31. package/dist/generated/ClientStatusOutput.d.ts +4 -0
  32. package/dist/generated/ClientStatusOutput.d.ts.map +1 -0
  33. package/dist/generated/ClientStatusOutput.js +2 -0
  34. package/dist/generated/CommandError.d.ts +6 -0
  35. package/dist/generated/CommandError.d.ts.map +1 -0
  36. package/dist/generated/CommandError.js +2 -0
  37. package/dist/generated/CommandResponse.d.ts +6 -0
  38. package/dist/generated/CommandResponse.d.ts.map +1 -0
  39. package/dist/generated/CommandResponse.js +1 -0
  40. package/dist/generated/CountAllClients.d.ts +2 -0
  41. package/dist/generated/CountAllClients.d.ts.map +1 -0
  42. package/dist/generated/CountAllClients.js +2 -0
  43. package/dist/generated/CountAllServers.d.ts +2 -0
  44. package/dist/generated/CountAllServers.d.ts.map +1 -0
  45. package/dist/generated/CountAllServers.js +2 -0
  46. package/dist/generated/CountClients.d.ts +3 -0
  47. package/dist/generated/CountClients.d.ts.map +1 -0
  48. package/dist/generated/CountClients.js +1 -0
  49. package/dist/generated/CountServers.d.ts +3 -0
  50. package/dist/generated/CountServers.d.ts.map +1 -0
  51. package/dist/generated/CountServers.js +1 -0
  52. package/dist/generated/DeleteClient.d.ts +8 -0
  53. package/dist/generated/DeleteClient.d.ts.map +1 -0
  54. package/dist/generated/DeleteClient.js +1 -0
  55. package/dist/generated/DeleteClientArgs.d.ts +5 -0
  56. package/dist/generated/DeleteClientArgs.d.ts.map +1 -0
  57. package/dist/generated/DeleteClientArgs.js +1 -0
  58. package/dist/generated/DeleteClientResult.d.ts +7 -0
  59. package/dist/generated/DeleteClientResult.d.ts.map +1 -0
  60. package/dist/generated/DeleteClientResult.js +2 -0
  61. package/dist/generated/DeleteClients.d.ts +8 -0
  62. package/dist/generated/DeleteClients.d.ts.map +1 -0
  63. package/dist/generated/DeleteClients.js +1 -0
  64. package/dist/generated/DeleteClientsArgs.d.ts +5 -0
  65. package/dist/generated/DeleteClientsArgs.d.ts.map +1 -0
  66. package/dist/generated/DeleteClientsArgs.js +1 -0
  67. package/dist/generated/DeleteClientsResult.d.ts +7 -0
  68. package/dist/generated/DeleteClientsResult.d.ts.map +1 -0
  69. package/dist/generated/DeleteClientsResult.js +2 -0
  70. package/dist/generated/DeleteServer.d.ts +8 -0
  71. package/dist/generated/DeleteServer.d.ts.map +1 -0
  72. package/dist/generated/DeleteServer.js +1 -0
  73. package/dist/generated/DeleteServerArgs.d.ts +5 -0
  74. package/dist/generated/DeleteServerArgs.d.ts.map +1 -0
  75. package/dist/generated/DeleteServerArgs.js +1 -0
  76. package/dist/generated/DeleteServerResult.d.ts +7 -0
  77. package/dist/generated/DeleteServerResult.d.ts.map +1 -0
  78. package/dist/generated/DeleteServerResult.js +2 -0
  79. package/dist/generated/DeleteServers.d.ts +8 -0
  80. package/dist/generated/DeleteServers.d.ts.map +1 -0
  81. package/dist/generated/DeleteServers.js +1 -0
  82. package/dist/generated/DeleteServersArgs.d.ts +5 -0
  83. package/dist/generated/DeleteServersArgs.d.ts.map +1 -0
  84. package/dist/generated/DeleteServersArgs.js +1 -0
  85. package/dist/generated/DeleteServersResult.d.ts +7 -0
  86. package/dist/generated/DeleteServersResult.d.ts.map +1 -0
  87. package/dist/generated/DeleteServersResult.js +2 -0
  88. package/dist/generated/EntitySearch.d.ts +21 -0
  89. package/dist/generated/EntitySearch.d.ts.map +1 -0
  90. package/dist/generated/EntitySearch.js +2 -0
  91. package/dist/generated/EntitySearchResult.d.ts +10 -0
  92. package/dist/generated/EntitySearchResult.d.ts.map +1 -0
  93. package/dist/generated/EntitySearchResult.js +2 -0
  94. package/dist/generated/EntitySnapshotDifference.d.ts +8 -0
  95. package/dist/generated/EntitySnapshotDifference.d.ts.map +1 -0
  96. package/dist/generated/EntitySnapshotDifference.js +2 -0
  97. package/dist/generated/EntitySnapshotDifferenceData.d.ts +10 -0
  98. package/dist/generated/EntitySnapshotDifferenceData.d.ts.map +1 -0
  99. package/dist/generated/EntitySnapshotDifferenceData.js +1 -0
  100. package/dist/generated/EntityTreeExport.d.ts +27 -0
  101. package/dist/generated/EntityTreeExport.d.ts.map +1 -0
  102. package/dist/generated/EntityTreeExport.js +1 -0
  103. package/dist/generated/EventContainer.d.ts +9 -0
  104. package/dist/generated/EventContainer.d.ts.map +1 -0
  105. package/dist/generated/EventContainer.js +1 -0
  106. package/dist/generated/EventOptions.d.ts +21 -0
  107. package/dist/generated/EventOptions.d.ts.map +1 -0
  108. package/dist/generated/EventOptions.js +2 -0
  109. package/dist/generated/EventsForTransaction.d.ts +7 -0
  110. package/dist/generated/EventsForTransaction.d.ts.map +1 -0
  111. package/dist/generated/EventsForTransaction.js +2 -0
  112. package/dist/generated/ExportEntityTree.d.ts +24 -0
  113. package/dist/generated/ExportEntityTree.d.ts.map +1 -0
  114. package/dist/generated/ExportEntityTree.js +2 -0
  115. package/dist/generated/ExportedEntity.d.ts +15 -0
  116. package/dist/generated/ExportedEntity.d.ts.map +1 -0
  117. package/dist/generated/ExportedEntity.js +1 -0
  118. package/dist/generated/FullChildEntities.d.ts +8 -0
  119. package/dist/generated/FullChildEntities.d.ts.map +1 -0
  120. package/dist/generated/FullChildEntities.js +2 -0
  121. package/dist/generated/GetAllClients.d.ts +2 -0
  122. package/dist/generated/GetAllClients.d.ts.map +1 -0
  123. package/dist/generated/GetAllClients.js +2 -0
  124. package/dist/generated/GetAllServers.d.ts +2 -0
  125. package/dist/generated/GetAllServers.d.ts.map +1 -0
  126. package/dist/generated/GetAllServers.js +2 -0
  127. package/dist/generated/GetClientById.d.ts +5 -0
  128. package/dist/generated/GetClientById.d.ts.map +1 -0
  129. package/dist/generated/GetClientById.js +1 -0
  130. package/dist/generated/GetClientsByIds.d.ts +5 -0
  131. package/dist/generated/GetClientsByIds.d.ts.map +1 -0
  132. package/dist/generated/GetClientsByIds.js +1 -0
  133. package/dist/generated/GetClientsByQuery.d.ts +3 -0
  134. package/dist/generated/GetClientsByQuery.d.ts.map +1 -0
  135. package/dist/generated/GetClientsByQuery.js +1 -0
  136. package/dist/generated/GetConnectedServer.d.ts +2 -0
  137. package/dist/generated/GetConnectedServer.d.ts.map +1 -0
  138. package/dist/generated/GetConnectedServer.js +2 -0
  139. package/dist/generated/GetItemsByTypeAndIds.d.ts +14 -0
  140. package/dist/generated/GetItemsByTypeAndIds.d.ts.map +1 -0
  141. package/dist/generated/GetItemsByTypeAndIds.js +2 -0
  142. package/dist/generated/GetPeerServers.d.ts +2 -0
  143. package/dist/generated/GetPeerServers.d.ts.map +1 -0
  144. package/dist/generated/GetPeerServers.js +2 -0
  145. package/{src/generated/GetPersistHealth.ts → dist/generated/GetPersistHealth.d.ts} +1 -2
  146. package/dist/generated/GetPersistHealth.d.ts.map +1 -0
  147. package/dist/generated/GetPersistHealth.js +2 -0
  148. package/dist/generated/GetServerById.d.ts +5 -0
  149. package/dist/generated/GetServerById.d.ts.map +1 -0
  150. package/dist/generated/GetServerById.js +1 -0
  151. package/dist/generated/GetServersByIds.d.ts +5 -0
  152. package/dist/generated/GetServersByIds.d.ts.map +1 -0
  153. package/dist/generated/GetServersByIds.js +1 -0
  154. package/dist/generated/GetServersByQuery.d.ts +3 -0
  155. package/dist/generated/GetServersByQuery.d.ts.map +1 -0
  156. package/dist/generated/GetServersByQuery.js +1 -0
  157. package/dist/generated/ImportItems.d.ts +19 -0
  158. package/dist/generated/ImportItems.d.ts.map +1 -0
  159. package/dist/generated/ImportItems.js +1 -0
  160. package/dist/generated/ImportItemsArgs.d.ts +13 -0
  161. package/dist/generated/ImportItemsArgs.d.ts.map +1 -0
  162. package/dist/generated/ImportItemsArgs.js +1 -0
  163. package/dist/generated/ItemStub.d.ts +10 -0
  164. package/dist/generated/ItemStub.d.ts.map +1 -0
  165. package/dist/generated/ItemStub.js +2 -0
  166. package/{src/generated/LogLevel.ts → dist/generated/LogLevel.d.ts} +1 -2
  167. package/dist/generated/LogLevel.d.ts.map +1 -0
  168. package/dist/generated/LogLevel.js +2 -0
  169. package/{src/generated/Loggers.ts → dist/generated/Loggers.d.ts} +1 -2
  170. package/dist/generated/Loggers.d.ts.map +1 -0
  171. package/dist/generated/Loggers.js +2 -0
  172. package/dist/generated/MEvent.d.ts +16 -0
  173. package/dist/generated/MEvent.d.ts.map +1 -0
  174. package/dist/generated/MEvent.js +1 -0
  175. package/dist/generated/MEventType.d.ts +2 -0
  176. package/dist/generated/MEventType.d.ts.map +1 -0
  177. package/dist/generated/MEventType.js +2 -0
  178. package/dist/generated/MykoMessage.d.ts +86 -0
  179. package/dist/generated/MykoMessage.d.ts.map +1 -0
  180. package/dist/generated/MykoMessage.js +1 -0
  181. package/dist/generated/PartialClient.d.ts +12 -0
  182. package/dist/generated/PartialClient.d.ts.map +1 -0
  183. package/dist/generated/PartialClient.js +1 -0
  184. package/dist/generated/PartialServer.d.ts +9 -0
  185. package/dist/generated/PartialServer.d.ts.map +1 -0
  186. package/dist/generated/PartialServer.js +1 -0
  187. package/dist/generated/PeerAlive.d.ts +8 -0
  188. package/dist/generated/PeerAlive.d.ts.map +1 -0
  189. package/dist/generated/PeerAlive.js +2 -0
  190. package/dist/generated/PersistHealthStatus.d.ts +34 -0
  191. package/dist/generated/PersistHealthStatus.d.ts.map +1 -0
  192. package/dist/generated/PersistHealthStatus.js +2 -0
  193. package/dist/generated/PingData.d.ts +14 -0
  194. package/dist/generated/PingData.d.ts.map +1 -0
  195. package/dist/generated/PingData.js +2 -0
  196. package/dist/generated/QueryError.d.ts +6 -0
  197. package/dist/generated/QueryError.d.ts.map +1 -0
  198. package/dist/generated/QueryError.js +2 -0
  199. package/dist/generated/QueryWindow.d.ts +5 -0
  200. package/dist/generated/QueryWindow.d.ts.map +1 -0
  201. package/dist/generated/QueryWindow.js +2 -0
  202. package/dist/generated/QueryWindowUpdate.d.ts +6 -0
  203. package/dist/generated/QueryWindowUpdate.d.ts.map +1 -0
  204. package/dist/generated/QueryWindowUpdate.js +1 -0
  205. package/dist/generated/ReportError.d.ts +6 -0
  206. package/dist/generated/ReportError.d.ts.map +1 -0
  207. package/dist/generated/ReportError.js +2 -0
  208. package/dist/generated/ReportResponse.d.ts +6 -0
  209. package/dist/generated/ReportResponse.d.ts.map +1 -0
  210. package/dist/generated/ReportResponse.js +1 -0
  211. package/dist/generated/Server.d.ts +9 -0
  212. package/dist/generated/Server.d.ts.map +1 -0
  213. package/dist/generated/Server.js +1 -0
  214. package/dist/generated/ServerCount.d.ts +4 -0
  215. package/dist/generated/ServerCount.d.ts.map +1 -0
  216. package/dist/generated/ServerCount.js +2 -0
  217. package/dist/generated/ServerId.d.ts +2 -0
  218. package/dist/generated/ServerId.d.ts.map +1 -0
  219. package/dist/generated/ServerId.js +2 -0
  220. package/dist/generated/ServerLogLevel.d.ts +7 -0
  221. package/dist/generated/ServerLogLevel.d.ts.map +1 -0
  222. package/dist/generated/ServerLogLevel.js +2 -0
  223. package/{src/generated/ServerStats.ts → dist/generated/ServerStats.d.ts} +1 -2
  224. package/dist/generated/ServerStats.d.ts.map +1 -0
  225. package/dist/generated/ServerStats.js +2 -0
  226. package/dist/generated/ServerStatsOutput.d.ts +20 -0
  227. package/dist/generated/ServerStatsOutput.d.ts.map +1 -0
  228. package/dist/generated/ServerStatsOutput.js +1 -0
  229. package/dist/generated/SetClientWindbackTime.d.ts +11 -0
  230. package/dist/generated/SetClientWindbackTime.d.ts.map +1 -0
  231. package/dist/generated/SetClientWindbackTime.js +2 -0
  232. package/dist/generated/SetClientWindbackTimeArgs.d.ts +7 -0
  233. package/dist/generated/SetClientWindbackTimeArgs.d.ts.map +1 -0
  234. package/dist/generated/SetClientWindbackTimeArgs.js +2 -0
  235. package/dist/generated/SetLogLevel.d.ts +9 -0
  236. package/dist/generated/SetLogLevel.d.ts.map +1 -0
  237. package/dist/generated/SetLogLevel.js +1 -0
  238. package/dist/generated/SetLogLevelArgs.d.ts +6 -0
  239. package/dist/generated/SetLogLevelArgs.d.ts.map +1 -0
  240. package/dist/generated/SetLogLevelArgs.js +1 -0
  241. package/dist/generated/ViewError.d.ts +6 -0
  242. package/dist/generated/ViewError.d.ts.map +1 -0
  243. package/dist/generated/ViewError.js +2 -0
  244. package/dist/generated/ViewWindowUpdate.d.ts +6 -0
  245. package/dist/generated/ViewWindowUpdate.d.ts.map +1 -0
  246. package/dist/generated/ViewWindowUpdate.js +1 -0
  247. package/dist/generated/WindbackStatus.d.ts +2 -0
  248. package/dist/generated/WindbackStatus.d.ts.map +1 -0
  249. package/dist/generated/WindbackStatus.js +2 -0
  250. package/dist/generated/WindbackStatusOutput.d.ts +11 -0
  251. package/dist/generated/WindbackStatusOutput.d.ts.map +1 -0
  252. package/dist/generated/WindbackStatusOutput.js +2 -0
  253. package/dist/generated/WrappedCommand.d.ts +6 -0
  254. package/dist/generated/WrappedCommand.d.ts.map +1 -0
  255. package/dist/generated/WrappedCommand.js +1 -0
  256. package/dist/generated/WrappedItem.d.ts +9 -0
  257. package/dist/generated/WrappedItem.d.ts.map +1 -0
  258. package/dist/generated/WrappedItem.js +1 -0
  259. package/dist/generated/WrappedQuery.d.ts +9 -0
  260. package/dist/generated/WrappedQuery.d.ts.map +1 -0
  261. package/dist/generated/WrappedQuery.js +1 -0
  262. package/dist/generated/WrappedReport.d.ts +6 -0
  263. package/dist/generated/WrappedReport.d.ts.map +1 -0
  264. package/dist/generated/WrappedReport.js +1 -0
  265. package/dist/generated/WrappedView.d.ts +9 -0
  266. package/dist/generated/WrappedView.d.ts.map +1 -0
  267. package/dist/generated/WrappedView.js +1 -0
  268. package/dist/generated/index.d.ts +421 -0
  269. package/dist/generated/index.d.ts.map +1 -0
  270. package/dist/generated/index.js +349 -0
  271. package/dist/generated/serde_json/JsonValue.d.ts +4 -0
  272. package/dist/generated/serde_json/JsonValue.d.ts.map +1 -0
  273. package/dist/generated/serde_json/JsonValue.js +2 -0
  274. package/dist/index.d.ts +76 -0
  275. package/dist/index.d.ts.map +1 -0
  276. package/dist/index.js +68 -0
  277. package/package.json +8 -4
  278. package/src/client.ts +0 -1851
  279. package/src/generated/CancelSubscription.ts +0 -6
  280. package/src/generated/ChildEntities.ts +0 -6
  281. package/src/generated/ChildEntitiesAllTime.ts +0 -6
  282. package/src/generated/ClearClientWindbackTimeArgs.ts +0 -3
  283. package/src/generated/Client.ts +0 -10
  284. package/src/generated/ClientStatus.ts +0 -7
  285. package/src/generated/ClientStatusOutput.ts +0 -3
  286. package/src/generated/CommandError.ts +0 -3
  287. package/src/generated/CommandResponse.ts +0 -4
  288. package/src/generated/CountAllClients.ts +0 -3
  289. package/src/generated/CountAllServers.ts +0 -3
  290. package/src/generated/CountClients.ts +0 -4
  291. package/src/generated/CountServers.ts +0 -4
  292. package/src/generated/DeleteClient.ts +0 -7
  293. package/src/generated/DeleteClientArgs.ts +0 -4
  294. package/src/generated/DeleteClientResult.ts +0 -6
  295. package/src/generated/DeleteClients.ts +0 -7
  296. package/src/generated/DeleteClientsArgs.ts +0 -4
  297. package/src/generated/DeleteClientsResult.ts +0 -6
  298. package/src/generated/DeleteServer.ts +0 -7
  299. package/src/generated/DeleteServerArgs.ts +0 -4
  300. package/src/generated/DeleteServerResult.ts +0 -6
  301. package/src/generated/DeleteServers.ts +0 -7
  302. package/src/generated/DeleteServersArgs.ts +0 -4
  303. package/src/generated/DeleteServersResult.ts +0 -6
  304. package/src/generated/EntitySearch.ts +0 -21
  305. package/src/generated/EntitySearchResult.ts +0 -10
  306. package/src/generated/EntitySnapshotDifference.ts +0 -6
  307. package/src/generated/EntitySnapshotDifferenceData.ts +0 -7
  308. package/src/generated/EntityTreeExport.ts +0 -27
  309. package/src/generated/EventContainer.ts +0 -7
  310. package/src/generated/EventOptions.ts +0 -21
  311. package/src/generated/EventsForTransaction.ts +0 -6
  312. package/src/generated/ExportEntityTree.ts +0 -24
  313. package/src/generated/ExportedEntity.ts +0 -15
  314. package/src/generated/FullChildEntities.ts +0 -6
  315. package/src/generated/GetAllClients.ts +0 -3
  316. package/src/generated/GetAllServers.ts +0 -3
  317. package/src/generated/GetClientById.ts +0 -4
  318. package/src/generated/GetClientsByIds.ts +0 -4
  319. package/src/generated/GetClientsByQuery.ts +0 -4
  320. package/src/generated/GetConnectedServer.ts +0 -3
  321. package/src/generated/GetItemsByTypeAndIds.ts +0 -14
  322. package/src/generated/GetPeerServers.ts +0 -3
  323. package/src/generated/GetServerById.ts +0 -4
  324. package/src/generated/GetServersByIds.ts +0 -4
  325. package/src/generated/GetServersByQuery.ts +0 -4
  326. package/src/generated/ImportItems.ts +0 -19
  327. package/src/generated/ImportItemsArgs.ts +0 -13
  328. package/src/generated/ItemStub.ts +0 -7
  329. package/src/generated/MEvent.ts +0 -10
  330. package/src/generated/MykoMessage.ts +0 -19
  331. package/src/generated/PartialClient.ts +0 -10
  332. package/src/generated/PartialServer.ts +0 -4
  333. package/src/generated/PeerAlive.ts +0 -7
  334. package/src/generated/PersistHealthStatus.ts +0 -34
  335. package/src/generated/PingData.ts +0 -14
  336. package/src/generated/QueryError.ts +0 -3
  337. package/src/generated/QueryWindow.ts +0 -3
  338. package/src/generated/QueryWindowUpdate.ts +0 -4
  339. package/src/generated/ReportError.ts +0 -3
  340. package/src/generated/ReportResponse.ts +0 -4
  341. package/src/generated/Server.ts +0 -4
  342. package/src/generated/ServerCount.ts +0 -3
  343. package/src/generated/ServerLogLevel.ts +0 -6
  344. package/src/generated/ServerStatsOutput.ts +0 -20
  345. package/src/generated/SetClientWindbackTime.ts +0 -11
  346. package/src/generated/SetClientWindbackTimeArgs.ts +0 -7
  347. package/src/generated/SetLogLevel.ts +0 -7
  348. package/src/generated/SetLogLevelArgs.ts +0 -4
  349. package/src/generated/ViewError.ts +0 -3
  350. package/src/generated/ViewWindowUpdate.ts +0 -4
  351. package/src/generated/WindbackStatus.ts +0 -3
  352. package/src/generated/WindbackStatusOutput.ts +0 -11
  353. package/src/generated/WrappedCommand.ts +0 -4
  354. package/src/generated/WrappedItem.ts +0 -7
  355. package/src/generated/WrappedQuery.ts +0 -5
  356. package/src/generated/WrappedReport.ts +0 -4
  357. package/src/generated/WrappedView.ts +0 -5
  358. package/src/generated/index.ts +0 -580
  359. package/src/generated/serde_json/JsonValue.ts +0 -3
  360. package/src/index.ts +0 -128
package/src/client.ts DELETED
@@ -1,1851 +0,0 @@
1
- /**
2
- * Pure TypeScript WebSocket client for Myko servers
3
- *
4
- * Maintains connections to all known servers for instant failover.
5
- * When the current server disconnects, instantly switches to another open connection.
6
- */
7
-
8
- import {
9
- GetPeerServers,
10
- MykoEvent,
11
- type JsonValue,
12
- type MEvent,
13
- type MykoMessage,
14
- type PingData,
15
- type Server,
16
- type WrappedItem,
17
- type WrappedQuery,
18
- type WrappedReport,
19
- type WrappedView,
20
- } from './generated'
21
- import { Packr, Unpackr } from 'msgpackr'
22
- import {
23
- bufferCount,
24
- bufferTime,
25
- catchError,
26
- combineLatest,
27
- filter,
28
- finalize,
29
- firstValueFrom,
30
- interval,
31
- map,
32
- merge,
33
- Observable,
34
- of,
35
- ReplaySubject,
36
- scan,
37
- shareReplay,
38
- Subject,
39
- Subscription,
40
- switchMap,
41
- } from 'rxjs'
42
- import { v4 as uuid } from 'uuid'
43
-
44
- // msgpackr defaults can emit extension types for values that don't exist in JSON (notably
45
- // `undefined`). Our Rust server deserializes msgpack into `serde_json::Value`, so ensure we
46
- // encode `undefined` as nil/null instead of an extension.
47
- const packr = new Packr({ encodeUndefinedAsNil: true })
48
- const unpackr = new Unpackr({})
49
- const EVENT_BATCH = 'ws:m:event-batch'
50
-
51
- function stableStringify(value: unknown): string | null {
52
- const seen = new WeakSet<object>()
53
- try {
54
- return JSON.stringify(value, (_key, raw) => {
55
- if (typeof raw === 'bigint') return `__bigint:${raw.toString()}`
56
- if (!raw || typeof raw !== 'object') return raw
57
- if (seen.has(raw)) return '__circular__'
58
- seen.add(raw)
59
- if (Array.isArray(raw)) return raw
60
- const sorted: Record<string, unknown> = {}
61
- for (const key of Object.keys(raw as Record<string, unknown>).sort()) {
62
- sorted[key] = (raw as Record<string, unknown>)[key]
63
- }
64
- return sorted
65
- })
66
- } catch {
67
- return null
68
- }
69
- }
70
-
71
- /** Union type for error event names */
72
- export type MykoErrorEvent =
73
- | typeof MykoEvent.QueryError
74
- | typeof MykoEvent.ViewError
75
- | typeof MykoEvent.CommandError
76
- | typeof MykoEvent.ReportError
77
-
78
- /** Error types from server */
79
- export type MykoError = {
80
- event: MykoErrorEvent
81
- tx: string
82
- message: string
83
- }
84
-
85
- /** Client statistics */
86
- export type ClientStats = {
87
- ping: number
88
- mpsDown: number
89
- mpsUp: number
90
- }
91
-
92
- /** Connection status */
93
- export enum ConnectionStatus {
94
- Connected = 'Connected',
95
- Disconnected = 'Disconnected',
96
- Connecting = 'Connecting',
97
- }
98
-
99
- /** Wire protocol for encoding messages */
100
- export enum MykoProtocol {
101
- JSON = 'JSON',
102
- MSGPACK = 'MSGPACK',
103
- }
104
-
105
- type ConnectionLogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'verbose'
106
-
107
- /** Query class interface */
108
- export interface Query<T> {
109
- readonly queryId: string
110
- readonly queryItemType: string
111
- readonly query: Record<string, unknown>
112
- readonly $res?: () => T[]
113
- }
114
-
115
- /** View class interface */
116
- export interface View<T> {
117
- readonly viewId: string
118
- readonly viewItemType: string
119
- readonly view: Record<string, unknown>
120
- readonly $res?: () => T[]
121
- }
122
-
123
- /** Report class interface */
124
- export interface Report<T> {
125
- readonly reportId: string
126
- readonly report: Record<string, unknown>
127
- readonly $res?: () => T
128
- }
129
-
130
- /** Command class interface */
131
- export interface Command<T> {
132
- readonly commandId: string
133
- readonly command: Record<string, unknown>
134
- readonly $res?: () => T
135
- }
136
-
137
- /** Extract result type from a query */
138
- export type QueryResult<Q> = Q extends Query<infer R> ? R[] : unknown[]
139
-
140
- /** Extract item type from a query */
141
- export type QueryItem<Q> = Q extends Query<infer R> ? R : unknown
142
-
143
- /** Extract item type from a view */
144
- export type ViewItem<V> = V extends View<infer R> ? R : unknown
145
-
146
- /** Extract result type from a view */
147
- export type ViewResult<V> = V extends View<infer R> ? R[] : unknown[]
148
-
149
- /** Extract result type from a report */
150
- export type ReportResult<R> = R extends Report<infer T> ? T : unknown
151
-
152
- /** Extract result type from a command */
153
- export type CommandResult<C> = C extends Command<infer T> ? T : unknown
154
-
155
- /** Diff event for incremental query updates */
156
- export type QueryDiff<T> = {
157
- sequence: bigint
158
- deletes: string[]
159
- upserts: T[]
160
- }
161
-
162
- export type QueryWindow = {
163
- offset: number
164
- limit: number
165
- }
166
-
167
- export type QueryWatchOptions = {
168
- window?: QueryWindow | null
169
- }
170
-
171
- export type QueryWindowInfo = {
172
- totalCount: number | null
173
- window: QueryWindow | null
174
- }
175
-
176
- function queryCacheKey(
177
- query: Pick<Query<unknown>, 'queryId' | 'query'>,
178
- options?: QueryWatchOptions,
179
- ): string {
180
- const queryPayload = stableStringify(query.query) ?? '__unstable_query__'
181
- const windowPayload = stableStringify(options?.window ?? null) ?? '__unstable_window__'
182
- return `query:${query.queryId}:${queryPayload}:${windowPayload}`
183
- }
184
-
185
- function viewCacheKey(
186
- view: Pick<View<unknown>, 'viewId' | 'view'>,
187
- options?: QueryWatchOptions,
188
- ): string {
189
- const viewPayload = stableStringify(view.view) ?? '__unstable_view__'
190
- const windowPayload = stableStringify(options?.window ?? null) ?? '__unstable_window__'
191
- return `view:${view.viewId}:${viewPayload}:${windowPayload}`
192
- }
193
-
194
- function reportCacheKey(report: Pick<Report<unknown>, 'reportId' | 'report'>): string {
195
- const payload = stableStringify(report.report) ?? '__unstable_report__'
196
- return `report:${report.reportId}:${payload}`
197
- }
198
-
199
- // Message type aliases
200
- type QueryResponseMessage = Extract<
201
- MykoMessage,
202
- { event: typeof MykoEvent.QueryResponse }
203
- >
204
- type ReportResponseMessage = Extract<
205
- MykoMessage,
206
- { event: typeof MykoEvent.ReportResponse }
207
- >
208
- type CommandResponseMessage = Extract<
209
- MykoMessage,
210
- { event: typeof MykoEvent.CommandResponse }
211
- >
212
- type CommandErrorMessage = Extract<
213
- MykoMessage,
214
- { event: typeof MykoEvent.CommandError }
215
- >
216
- type QueryErrorMessage = Extract<
217
- MykoMessage,
218
- { event: typeof MykoEvent.QueryError }
219
- >
220
- type ReportErrorMessage = Extract<
221
- MykoMessage,
222
- { event: typeof MykoEvent.ReportError }
223
- >
224
- type PingMessage = Extract<MykoMessage, { event: typeof MykoEvent.Ping }>
225
- type CommandIncomingMessage = Extract<
226
- MykoMessage,
227
- { event: typeof MykoEvent.Command }
228
- >
229
-
230
- interface ManagedSocket {
231
- ws: WebSocket
232
- address: string
233
- endpointKey: string
234
- reconnectOnClose: boolean
235
- }
236
-
237
- /**
238
- * Reactive WebSocket client for Myko servers with automatic failover.
239
- */
240
- export class MykoClient {
241
- // Socket management
242
- private sockets = new Map<string, ManagedSocket>()
243
- private reconnectTimers = new Map<string, ReturnType<typeof setTimeout>>()
244
- private endpointSockets = new Map<string, string>()
245
- // Main server: the socket used for outbound sends.
246
- // Other open sockets are warm standbys for failover.
247
- private currentServer: string | null = null
248
- private shouldReconnect = true
249
-
250
- // Message routing
251
- private queryResponses = new Subject<QueryResponseMessage>()
252
- private reportResponses = new Subject<ReportResponseMessage>()
253
- private commandResponses = new Subject<CommandResponseMessage>()
254
- private commandErrors = new Subject<CommandErrorMessage>()
255
- private queryErrors = new Subject<QueryErrorMessage>()
256
- private reportErrors = new Subject<ReportErrorMessage>()
257
- private pingResponses = new Subject<PingMessage>()
258
- private commandIncoming = new Subject<CommandIncomingMessage>()
259
-
260
- // State observables
261
- private connectionStatusSubject = new ReplaySubject<ConnectionStatus>(1)
262
- private currentServerSubject = new ReplaySubject<string | null>(1)
263
-
264
- // Subscription tracking
265
- private activeQueries = new Map<string, WrappedQuery>()
266
- private activeViews = new Map<string, WrappedView>()
267
- private activeReports = new Map<string, WrappedReport>()
268
- private activeQueryNames = new Map<string, string>()
269
- private activeViewNames = new Map<string, string>()
270
- private activeReportNames = new Map<string, string>()
271
- private sharedQueries = new Map<string, Observable<unknown>>()
272
- private sharedViews = new Map<string, Observable<unknown>>()
273
- private sharedQueryDiffs = new Map<string, Observable<unknown>>()
274
- private sharedViewDiffs = new Map<string, Observable<unknown>>()
275
- private sharedReports = new Map<string, Observable<unknown>>()
276
- private subscriptionStartMs = new Map<string, number>()
277
- private firstResponseLogged = new Set<string>()
278
- private messageQueue: MykoMessage[] = []
279
- private pendingEventBatch: MEvent[] = []
280
- private eventBatchFlushScheduled = false
281
- private readonly eventBatchMaxSize = 256
282
- private connectionLogLevelThreshold: ConnectionLogLevel =
283
- MykoClient.resolveDefaultConnectionLogLevel()
284
-
285
- // Stats
286
- private downMsgCounter = new Subject<void>()
287
- private upMsgCounter = new Subject<void>()
288
-
289
- // Auth & peer discovery
290
- private userToken: string | null = null
291
- private peerDiscoveryEnabled = true
292
- private peerDiscoverySubscription: Subscription | null = null
293
- private useSecureWebSocket = false
294
-
295
- // Protocol defaults to JSON for maximum compatibility (no msgpack extensions, bigint issues, etc).
296
- private protocol: MykoProtocol = MykoProtocol.JSON
297
-
298
- constructor() {
299
- this.setConnectionStatus(ConnectionStatus.Disconnected, 'init')
300
- this.setCurrentServer(null, 'init')
301
- }
302
-
303
- /** Set connection log verbosity at runtime. */
304
- setConnectionLogLevel(level: ConnectionLogLevel): void {
305
- this.connectionLogLevelThreshold = level
306
- }
307
-
308
- /** Set the wire protocol (JSON or MSGPACK). Default is MSGPACK. */
309
- setProtocol(protocol: MykoProtocol): void {
310
- this.protocol = protocol
311
- }
312
-
313
- // ─────────────────────────────────────────────────────────────────────────────
314
- // Connection Management
315
- // ─────────────────────────────────────────────────────────────────────────────
316
-
317
- /** Set a single server address, clearing any existing connections */
318
- setAddress(address: string | null): void {
319
- this.shouldReconnect = true // Re-enable autoreconnect when setting new address
320
- this.closeAllSockets()
321
- if (address) {
322
- this.useSecureWebSocket = address.startsWith('wss://')
323
- this.createSocket(address, true)
324
- }
325
- }
326
-
327
- /** Set multiple server addresses, clearing any existing connections */
328
- setAddresses(addresses: string[]): void {
329
- this.shouldReconnect = true // Re-enable autoreconnect when setting new addresses
330
- this.closeAllSockets()
331
- for (const addr of addresses) {
332
- this.createSocket(addr, true)
333
- }
334
- }
335
-
336
- /** Add additional servers (connects immediately) */
337
- addServers(addresses: string[], reconnectOnClose = true): void {
338
- this.shouldReconnect = true // Re-enable autoreconnect when adding servers
339
- for (const addr of addresses) {
340
- if (!this.hasConnectionTo(addr)) {
341
- this.createSocket(addr, reconnectOnClose)
342
- }
343
- }
344
- }
345
-
346
- /** Disconnect from all servers */
347
- disconnect(): void {
348
- this.shouldReconnect = false
349
- this.stopPeerDiscovery()
350
- this.closeAllSockets()
351
- }
352
-
353
- /** Get the currently active server address */
354
- getCurrentServer(): string | null {
355
- return this.currentServer
356
- }
357
-
358
- /** Get the current main server address used for outbound sends */
359
- getMainServer(): string | null {
360
- return this.currentServer
361
- }
362
-
363
- /** Get all server addresses */
364
- getServers(): string[] {
365
- return Array.from(this.sockets.values()).map((m) => m.address)
366
- }
367
-
368
- /** Get addresses of all open connections */
369
- getOpenServers(): string[] {
370
- return Array.from(this.sockets.values())
371
- .filter((m) => m.ws.readyState === WebSocket.OPEN)
372
- .map((m) => m.address)
373
- }
374
-
375
- /** Observable of current server changes */
376
- get currentServer$(): Observable<string | null> {
377
- return this.currentServerSubject.asObservable()
378
- }
379
-
380
- /** Observable of main server changes */
381
- get mainServer$(): Observable<string | null> {
382
- return this.currentServer$
383
- }
384
-
385
- /** Get current connection status */
386
- getConnectionStatus(): ConnectionStatus {
387
- if (this.currentServer) return ConnectionStatus.Connected
388
- for (const m of this.sockets.values()) {
389
- if (m.ws.readyState === WebSocket.CONNECTING)
390
- return ConnectionStatus.Connecting
391
- }
392
- return ConnectionStatus.Disconnected
393
- }
394
-
395
- /** Observable of connection status changes */
396
- get connectionStatus$(): Observable<ConnectionStatus> {
397
- return this.connectionStatusSubject.asObservable()
398
- }
399
-
400
- /** Observable of incoming command messages (ws:m:command) from the server */
401
- get commandIncoming$(): Observable<CommandIncomingMessage> {
402
- return this.commandIncoming.asObservable()
403
- }
404
-
405
- // ─────────────────────────────────────────────────────────────────────────────
406
- // Peer Discovery
407
- // ─────────────────────────────────────────────────────────────────────────────
408
-
409
- /** Enable automatic peer discovery via GetPeerServers query */
410
- enablePeerDiscovery(enabled: boolean, secure = false): void {
411
- this.peerDiscoveryEnabled = enabled
412
- this.useSecureWebSocket = secure
413
-
414
- if (!enabled && this.peerDiscoverySubscription) {
415
- this.peerDiscoverySubscription.unsubscribe()
416
- this.peerDiscoverySubscription = null
417
- } else if (enabled && this.hasOpenConnection()) {
418
- this.startPeerDiscovery()
419
- }
420
- }
421
-
422
- private startPeerDiscovery(): void {
423
- this.peerDiscoverySubscription?.unsubscribe()
424
- this.logConnection('peer_discovery_started', {
425
- secure: this.useSecureWebSocket,
426
- via: 'query:GetPeerServers',
427
- })
428
-
429
- this.peerDiscoverySubscription = this.watchQuery(
430
- new GetPeerServers({}),
431
- ).subscribe((servers: Server[]) => {
432
- const addresses = servers.map((s) =>
433
- this.useSecureWebSocket
434
- ? `wss://${s.address}/myko`
435
- : `ws://${s.address}:${s.port}/myko`,
436
- )
437
- this.logConnection('peer_discovery_update', {
438
- peers: servers.length,
439
- addresses,
440
- })
441
- // Discovered peers are ephemeral: if they disconnect, wait for discovery
442
- // to advertise them again rather than actively redialing.
443
- if (addresses.length > 0) this.addServers(addresses, false)
444
- })
445
- }
446
-
447
- private stopPeerDiscovery(): void {
448
- this.peerDiscoverySubscription?.unsubscribe()
449
- this.peerDiscoverySubscription = null
450
- }
451
-
452
- // ─────────────────────────────────────────────────────────────────────────────
453
- // Auth & Stats
454
- // ─────────────────────────────────────────────────────────────────────────────
455
-
456
- /** Set authentication token for commands */
457
- setToken(token: string | null): void {
458
- this.userToken = token
459
- }
460
-
461
- /** Observable of all errors */
462
- get errors$(): Observable<MykoError> {
463
- const toError = <
464
- T extends {
465
- event: MykoErrorEvent
466
- data: { tx: string; message: string }
467
- },
468
- >(
469
- e: T,
470
- ): MykoError => ({ event: e.event, tx: e.data.tx, message: e.data.message })
471
-
472
- return merge(
473
- this.queryErrors.pipe(map(toError)),
474
- this.commandErrors.pipe(map(toError)),
475
- this.reportErrors.pipe(map(toError)),
476
- )
477
- }
478
-
479
- /** Observable of successful command completions (tx id) */
480
- get successes$(): Observable<string> {
481
- return this.commandResponses.pipe(map((r) => r.data.tx))
482
- }
483
-
484
- /** Measure round-trip latency */
485
- async ping(): Promise<number> {
486
- const id = uuid()
487
- const nowMs = Date.now()
488
- // IMPORTANT: the Rust server expects `timestamp: i64`.
489
- // - JSON cannot encode bigint, so use number in JSON mode.
490
- // - msgpack can encode bigint as int64, so use bigint in MSGPACK mode.
491
- const timestamp =
492
- this.protocol === MykoProtocol.MSGPACK ? BigInt(nowMs) : nowMs
493
-
494
- this.send({
495
- event: MykoEvent.Ping,
496
- data: { id, timestamp } as unknown as PingData,
497
- } as MykoMessage)
498
-
499
- return firstValueFrom(
500
- this.pingResponses.pipe(
501
- filter((p) => p.data.id === id),
502
- map((p) => Date.now() - Number(p.data.timestamp)),
503
- ),
504
- )
505
- }
506
-
507
- /** Get real-time client statistics (emits every second) */
508
- stats(): Observable<ClientStats> {
509
- const pingLatency = interval(1000).pipe(
510
- switchMap(() => this.ping()),
511
- catchError(() => of(0)),
512
- )
513
- const mpsDown = this.downMsgCounter.pipe(
514
- bufferTime(100),
515
- bufferCount(10),
516
- map((b) => b.flat().length),
517
- )
518
- const mpsUp = this.upMsgCounter.pipe(
519
- bufferTime(100),
520
- bufferCount(10),
521
- map((b) => b.flat().length),
522
- )
523
- return combineLatest([pingLatency, mpsDown, mpsUp]).pipe(
524
- map(([ping, down, up]) => ({ ping, mpsDown: down, mpsUp: up })),
525
- )
526
- }
527
-
528
- // ─────────────────────────────────────────────────────────────────────────────
529
- // Queries & Reports
530
- // ─────────────────────────────────────────────────────────────────────────────
531
-
532
- /** Start a query subscription, returns [tx, responses$] */
533
- private startQuery<Q extends Query<unknown>>(
534
- query: Q,
535
- options?: QueryWatchOptions,
536
- ): [string, Observable<QueryResponseMessage>] {
537
- if (!query.queryId || !query.queryItemType || !(query as { query?: unknown }).query) {
538
- const details = {
539
- ctor: (query as { constructor?: { name?: string } }).constructor?.name ?? 'unknown',
540
- keys: Object.keys((query as Record<string, unknown>) ?? {}),
541
- queryId: (query as { queryId?: unknown }).queryId,
542
- queryItemType: (query as { queryItemType?: unknown }).queryItemType,
543
- }
544
- this.logConnection('query_shape_invalid', details)
545
- throw new Error(
546
- `Invalid query shape for ${details.ctor}: expected { queryId, queryItemType, query }`,
547
- )
548
- }
549
-
550
- const tx = uuid()
551
- const window = options?.window ?? undefined
552
- const queryName =
553
- (query as { constructor?: { name?: string } }).constructor?.name ?? query.queryId
554
- const wrappedQuery = {
555
- query: { ...query.query, tx, createdAt: new Date().toISOString() },
556
- queryId: query.queryId,
557
- queryItemType: query.queryItemType,
558
- ...(window ? { window } : {}),
559
- } as WrappedQuery
560
-
561
- this.activeQueries.set(tx, wrappedQuery)
562
- this.activeQueryNames.set(tx, queryName)
563
- this.subscriptionStartMs.set(tx, this.nowMs())
564
- this.firstResponseLogged.delete(tx)
565
- this.logConnection('query_subscribe', {
566
- tx,
567
- queryId: wrappedQuery.queryId,
568
- queryItemType: wrappedQuery.queryItemType,
569
- queryName,
570
- window: window ?? null,
571
- activeQueries: this.activeQueries.size,
572
- })
573
- this.send({ event: MykoEvent.Query, data: wrappedQuery })
574
-
575
- const responses$ = new Observable<QueryResponseMessage>((subscriber) => {
576
- const responseSub = this.queryResponses
577
- .pipe(filter((r) => r.data.tx === tx))
578
- .subscribe({
579
- next: (response) => subscriber.next(response),
580
- error: (error) => subscriber.error(error),
581
- })
582
-
583
- const errorSub = this.queryErrors
584
- .pipe(filter((error) => error.data.tx === tx))
585
- .subscribe((error) => {
586
- subscriber.error(new Error(error.data.message))
587
- })
588
-
589
- return () => {
590
- responseSub.unsubscribe()
591
- errorSub.unsubscribe()
592
- }
593
- }).pipe(
594
- finalize(() => {
595
- this.logConnection('query_cancel', {
596
- tx,
597
- queryId: wrappedQuery.queryId,
598
- queryItemType: wrappedQuery.queryItemType,
599
- queryName,
600
- activeQueriesBefore: this.activeQueries.size,
601
- })
602
- this.activeQueries.delete(tx)
603
- this.activeQueryNames.delete(tx)
604
- this.subscriptionStartMs.delete(tx)
605
- this.firstResponseLogged.delete(tx)
606
- this.send({ event: MykoEvent.QueryCancel, data: { tx } })
607
- }),
608
- )
609
-
610
- return [tx, responses$]
611
- }
612
-
613
- /** Update server-side window for an active query subscription */
614
- setQueryWindow(tx: string, window: QueryWindow | null): void {
615
- const active = this.activeQueries.get(tx)
616
- if (!active) return
617
-
618
- const updated = {
619
- ...active,
620
- ...(window ? { window } : {}),
621
- } as WrappedQuery
622
- if (!window) {
623
- delete (updated as { window?: QueryWindow }).window
624
- }
625
- this.activeQueries.set(tx, updated)
626
- this.logConnection('query_window_set', {
627
- tx,
628
- queryId: active.queryId,
629
- queryItemType: active.queryItemType,
630
- queryName: this.activeQueryNames.get(tx) ?? active.queryId,
631
- window,
632
- })
633
-
634
- this.send({ event: MykoEvent.QueryWindow, data: { tx, window } as unknown } as MykoMessage)
635
- }
636
-
637
- /** Start a view subscription, returns [tx, responses$] */
638
- private startView<V extends View<unknown>>(
639
- view: V,
640
- options?: QueryWatchOptions,
641
- ): [string, Observable<QueryResponseMessage>] {
642
- if (!view.viewId || !view.viewItemType || !(view as { view?: unknown }).view) {
643
- const details = {
644
- ctor: (view as { constructor?: { name?: string } }).constructor?.name ?? 'unknown',
645
- keys: Object.keys((view as Record<string, unknown>) ?? {}),
646
- viewId: (view as { viewId?: unknown }).viewId,
647
- viewItemType: (view as { viewItemType?: unknown }).viewItemType,
648
- }
649
- this.logConnection('view_shape_invalid', details)
650
- throw new Error(
651
- `Invalid view shape for ${details.ctor}: expected { viewId, viewItemType, view }`,
652
- )
653
- }
654
-
655
- const tx = uuid()
656
- const window = options?.window ?? undefined
657
- const viewName =
658
- (view as { constructor?: { name?: string } }).constructor?.name ?? view.viewId
659
- const wrappedView = {
660
- view: { ...view.view, tx, createdAt: new Date().toISOString() },
661
- viewId: view.viewId,
662
- viewItemType: view.viewItemType,
663
- ...(window ? { window } : {}),
664
- } as WrappedView
665
-
666
- this.activeViews.set(tx, wrappedView)
667
- this.activeViewNames.set(tx, viewName)
668
- this.subscriptionStartMs.set(tx, this.nowMs())
669
- this.firstResponseLogged.delete(tx)
670
- this.logConnection('view_subscribe', {
671
- tx,
672
- viewId: wrappedView.viewId,
673
- viewItemType: wrappedView.viewItemType,
674
- viewName,
675
- window: wrappedView.window ?? null,
676
- activeViews: this.activeViews.size,
677
- })
678
- this.send({ event: MykoEvent.View, data: wrappedView })
679
-
680
- const responses$ = new Observable<QueryResponseMessage>((subscriber) => {
681
- const responseSub = this.queryResponses
682
- .pipe(filter((r) => r.data.tx === tx))
683
- .subscribe({
684
- next: (response) => subscriber.next(response),
685
- error: (error) => subscriber.error(error),
686
- })
687
-
688
- const errorSub = this.queryErrors
689
- .pipe(filter((error) => error.data.tx === tx))
690
- .subscribe((error) => {
691
- subscriber.error(new Error(error.data.message))
692
- })
693
-
694
- return () => {
695
- responseSub.unsubscribe()
696
- errorSub.unsubscribe()
697
- }
698
- }).pipe(
699
- finalize(() => {
700
- this.logConnection('view_cancel', {
701
- tx,
702
- viewId: wrappedView.viewId,
703
- viewName,
704
- activeViewsBefore: this.activeViews.size,
705
- })
706
- this.activeViews.delete(tx)
707
- this.activeViewNames.delete(tx)
708
- this.subscriptionStartMs.delete(tx)
709
- this.firstResponseLogged.delete(tx)
710
- this.send({ event: MykoEvent.ViewCancel, data: { tx } })
711
- }),
712
- )
713
-
714
- return [tx, responses$]
715
- }
716
-
717
- /** Update server-side window for an active view subscription */
718
- setViewWindow(tx: string, window: QueryWindow | null): void {
719
- const active = this.activeViews.get(tx)
720
- if (!active) return
721
-
722
- const updated = {
723
- ...active,
724
- ...(window ? { window } : {}),
725
- } as WrappedView
726
- if (!window) {
727
- delete (updated as { window?: QueryWindow }).window
728
- }
729
- this.activeViews.set(tx, updated)
730
- this.logConnection('view_window_set', {
731
- tx,
732
- viewId: active.viewId,
733
- viewName: this.activeViewNames.get(tx) ?? active.viewId,
734
- window,
735
- })
736
-
737
- this.send({ event: MykoEvent.ViewWindow, data: { tx, window } as unknown } as MykoMessage)
738
- }
739
-
740
- /** Watch a query and receive live updates with automatic deduplication */
741
- watchQuery<Q extends Query<unknown>>(
742
- query: Q,
743
- options?: QueryWatchOptions,
744
- ): Observable<QueryResult<Q>> {
745
- const cacheKey = queryCacheKey(query, options)
746
-
747
- const existing = this.sharedQueries.get(cacheKey)
748
- if (existing) return existing as Observable<QueryResult<Q>>
749
-
750
- const [, responses$] = this.startQuery(query, options)
751
-
752
- const shared$ = responses$.pipe(
753
- scan((acc, update) => {
754
- if (BigInt(update.data.sequence) === 0n) acc.clear()
755
- for (const id of update.data.deletes) acc.delete(id)
756
- for (const wrapped of update.data.upserts) {
757
- const item = wrapped.item as { id: string }
758
- if (item?.id) acc.set(item.id, wrapped)
759
- }
760
- return acc
761
- }, new Map<string, WrappedItem>()),
762
- map((items) => [...items.values()].map((w) => w.item) as QueryResult<Q>),
763
- finalize(() => {
764
- this.sharedQueries.delete(cacheKey)
765
- }),
766
- shareReplay({ bufferSize: 1, refCount: true }),
767
- // Defensive copy: shareReplay replays the same array reference to all
768
- // subscribers, so a mutation (e.g. .shift()) by one subscriber would
769
- // corrupt the shared value for others. Cloning per-subscriber prevents this.
770
- map((x) => (Array.isArray(x) ? (x.slice() as QueryResult<Q>) : x)),
771
- )
772
-
773
- this.sharedQueries.set(cacheKey, shared$)
774
- return shared$
775
- }
776
-
777
- /** Watch a view and receive live updates with automatic deduplication */
778
- watchView<V extends View<unknown>>(
779
- view: V,
780
- options?: QueryWatchOptions,
781
- ): Observable<ViewResult<V>> {
782
- const cacheKey = viewCacheKey(view, options)
783
-
784
- const existing = this.sharedViews.get(cacheKey)
785
- if (existing) return existing as Observable<ViewResult<V>>
786
-
787
- const [, responses$] = this.startView(view, options)
788
-
789
- const shared$ = responses$.pipe(
790
- scan((acc, update) => {
791
- if (BigInt(update.data.sequence) === 0n) acc.clear()
792
- for (const id of update.data.deletes) acc.delete(id)
793
- for (const wrapped of update.data.upserts) {
794
- const item = wrapped.item as { id: string }
795
- if (item?.id) acc.set(item.id, wrapped)
796
- }
797
- return acc
798
- }, new Map<string, WrappedItem>()),
799
- map((items) => [...items.values()].map((w) => w.item) as ViewResult<V>),
800
- finalize(() => {
801
- this.sharedViews.delete(cacheKey)
802
- }),
803
- shareReplay({ bufferSize: 1, refCount: true }),
804
- map((x) => (Array.isArray(x) ? (x.slice() as ViewResult<V>) : x)),
805
- )
806
-
807
- this.sharedViews.set(cacheKey, shared$)
808
- return shared$
809
- }
810
-
811
- /** Watch a query and receive raw diff events */
812
- watchQueryDiff<Q extends Query<unknown>>(
813
- query: Q,
814
- options?: QueryWatchOptions,
815
- ): Observable<QueryDiff<QueryItem<Q>>> {
816
- const cacheKey = queryCacheKey(query, options)
817
- const existing = this.sharedQueryDiffs.get(cacheKey)
818
- if (existing) return existing as Observable<QueryDiff<QueryItem<Q>>>
819
-
820
- const [, responses$] = this.startQuery(query, options)
821
- const shared$ = responses$.pipe(
822
- map((r) => ({
823
- sequence: BigInt(r.data.sequence),
824
- deletes: r.data.deletes.slice(),
825
- upserts: r.data.upserts.map(
826
- (w: WrappedItem) => w.item,
827
- ) as QueryItem<Q>[],
828
- })),
829
- finalize(() => {
830
- this.sharedQueryDiffs.delete(cacheKey)
831
- }),
832
- shareReplay({ bufferSize: 1, refCount: true }),
833
- map((diff) => ({
834
- sequence: diff.sequence,
835
- deletes: diff.deletes.slice(),
836
- upserts: diff.upserts.slice(),
837
- })),
838
- )
839
- this.sharedQueryDiffs.set(cacheKey, shared$)
840
- return shared$
841
- }
842
-
843
- /** Watch a view and receive raw diff events */
844
- watchViewDiff<V extends View<unknown>>(
845
- view: V,
846
- options?: QueryWatchOptions,
847
- ): Observable<QueryDiff<ViewItem<V>>> {
848
- const cacheKey = viewCacheKey(view, options)
849
- const existing = this.sharedViewDiffs.get(cacheKey)
850
- if (existing) return existing as Observable<QueryDiff<ViewItem<V>>>
851
-
852
- const [, responses$] = this.startView(view, options)
853
- const shared$ = responses$.pipe(
854
- map((r) => ({
855
- sequence: BigInt(r.data.sequence),
856
- deletes: r.data.deletes.slice(),
857
- upserts: r.data.upserts.map(
858
- (w: WrappedItem) => w.item,
859
- ) as ViewItem<V>[],
860
- })),
861
- finalize(() => {
862
- this.sharedViewDiffs.delete(cacheKey)
863
- }),
864
- shareReplay({ bufferSize: 1, refCount: true }),
865
- map((diff) => ({
866
- sequence: diff.sequence,
867
- deletes: diff.deletes.slice(),
868
- upserts: diff.upserts.slice(),
869
- })),
870
- )
871
- this.sharedViewDiffs.set(cacheKey, shared$)
872
- return shared$
873
- }
874
-
875
- /**
876
- * Start a live query with a mutable server-side window.
877
- * Use `setWindow` to scroll without re-subscribing.
878
- */
879
- watchQueryWindowed<Q extends Query<unknown>>(
880
- query: Q,
881
- options?: QueryWatchOptions,
882
- ): {
883
- tx: string
884
- results$: Observable<QueryResult<Q>>
885
- windowInfo$: Observable<QueryWindowInfo>
886
- setWindow: (window: QueryWindow | null) => void
887
- } {
888
- const [tx, responses$] = this.startQuery(query, options)
889
- const sharedResponses$ = responses$.pipe(
890
- shareReplay({ bufferSize: 1, refCount: true }),
891
- )
892
- const results$ = sharedResponses$.pipe(
893
- scan((state, update) => {
894
- const data = update.data as QueryResponseMessage['data'] & {
895
- total_count?: number
896
- totalCount?: number
897
- window?: QueryWindow | null
898
- changes?: Array<
899
- | { kind: 'upsert'; item: WrappedItem }
900
- | { kind: 'delete'; id: string }
901
- | {
902
- kind: 'windowOrder'
903
- ids: string[]
904
- totalCount?: number
905
- total_count?: number
906
- window?: QueryWindow | null
907
- }
908
- >
909
- }
910
-
911
- if (BigInt(update.data.sequence) === 0n) {
912
- state.cache.clear()
913
- state.visibleIds = []
914
- }
915
-
916
- for (const id of update.data.deletes) state.cache.delete(id)
917
- for (const wrapped of update.data.upserts) {
918
- const item = wrapped.item as { id?: string }
919
- if (item?.id) state.cache.set(item.id, wrapped)
920
- }
921
-
922
- const order = data.changes?.find(
923
- (change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
924
- change.kind === 'windowOrder',
925
- )
926
- if (order) {
927
- state.visibleIds = order.ids.slice()
928
- } else {
929
- // Fallback when window-order diffs are unavailable: derive visible ids
930
- // from current cache contents (in insertion order).
931
- state.visibleIds = [...state.cache.keys()]
932
- }
933
-
934
- return state
935
- }, {
936
- cache: new Map<string, WrappedItem>(),
937
- visibleIds: [] as string[],
938
- }),
939
- map((state) =>
940
- state.visibleIds
941
- .map((id) => state.cache.get(id)?.item)
942
- .filter((item): item is JsonValue => item !== undefined) as QueryResult<Q>,
943
- ),
944
- map((x) => x.slice() as QueryResult<Q>),
945
- )
946
- const windowInfo$ = sharedResponses$.pipe(
947
- map((update) => {
948
- const data = update.data as QueryResponseMessage['data'] & {
949
- total_count?: number
950
- totalCount?: number
951
- window?: QueryWindow | null
952
- changes?: Array<
953
- | { kind: 'upsert'; item: WrappedItem }
954
- | { kind: 'delete'; id: string }
955
- | {
956
- kind: 'windowOrder'
957
- ids: string[]
958
- totalCount?: number
959
- total_count?: number
960
- window?: QueryWindow | null
961
- }
962
- >
963
- }
964
- const order = data.changes?.find(
965
- (change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
966
- change.kind === 'windowOrder',
967
- )
968
- const orderTotalCount = order?.totalCount ?? order?.total_count
969
- return {
970
- totalCount:
971
- typeof data.totalCount === 'number'
972
- ? data.totalCount
973
- : typeof data.total_count === 'number'
974
- ? data.total_count
975
- : typeof orderTotalCount === 'number'
976
- ? orderTotalCount
977
- : null,
978
- window: data.window ?? order?.window ?? null,
979
- } satisfies QueryWindowInfo
980
- }),
981
- shareReplay({ bufferSize: 1, refCount: true }),
982
- )
983
-
984
- return {
985
- tx,
986
- results$,
987
- windowInfo$,
988
- setWindow: (window) => this.setQueryWindow(tx, window),
989
- }
990
- }
991
-
992
- /** Start a live view with a mutable server-side window. */
993
- watchViewWindowed<V extends View<unknown>>(
994
- view: V,
995
- options?: QueryWatchOptions,
996
- ): {
997
- tx: string
998
- results$: Observable<ViewResult<V>>
999
- windowInfo$: Observable<QueryWindowInfo>
1000
- setWindow: (window: QueryWindow | null) => void
1001
- } {
1002
- const [tx, responses$] = this.startView(view, options)
1003
- const sharedResponses$ = responses$.pipe(
1004
- shareReplay({ bufferSize: 1, refCount: true }),
1005
- )
1006
- const results$ = sharedResponses$.pipe(
1007
- scan((state, update) => {
1008
- const data = update.data as QueryResponseMessage['data'] & {
1009
- total_count?: number
1010
- totalCount?: number
1011
- window?: QueryWindow | null
1012
- changes?: Array<
1013
- | { kind: 'upsert'; item: WrappedItem }
1014
- | { kind: 'delete'; id: string }
1015
- | {
1016
- kind: 'windowOrder'
1017
- ids: string[]
1018
- totalCount?: number
1019
- total_count?: number
1020
- window?: QueryWindow | null
1021
- }
1022
- >
1023
- }
1024
-
1025
- if (BigInt(update.data.sequence) === 0n) {
1026
- state.cache.clear()
1027
- state.visibleIds = []
1028
- }
1029
-
1030
- for (const id of update.data.deletes) state.cache.delete(id)
1031
- for (const wrapped of update.data.upserts) {
1032
- const item = wrapped.item as { id?: string }
1033
- if (item?.id) state.cache.set(item.id, wrapped)
1034
- }
1035
-
1036
- const order = data.changes?.find(
1037
- (change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
1038
- change.kind === 'windowOrder',
1039
- )
1040
- if (order) {
1041
- state.visibleIds = order.ids.slice()
1042
- } else {
1043
- state.visibleIds = [...state.cache.keys()]
1044
- }
1045
-
1046
- return state
1047
- }, {
1048
- cache: new Map<string, WrappedItem>(),
1049
- visibleIds: [] as string[],
1050
- }),
1051
- map((state) =>
1052
- state.visibleIds
1053
- .map((id) => state.cache.get(id)?.item)
1054
- .filter((item): item is JsonValue => item !== undefined) as ViewResult<V>,
1055
- ),
1056
- map((x) => x.slice() as ViewResult<V>),
1057
- )
1058
- const windowInfo$ = sharedResponses$.pipe(
1059
- map((update) => {
1060
- const data = update.data as QueryResponseMessage['data'] & {
1061
- total_count?: number
1062
- totalCount?: number
1063
- window?: QueryWindow | null
1064
- changes?: Array<
1065
- | { kind: 'upsert'; item: WrappedItem }
1066
- | { kind: 'delete'; id: string }
1067
- | {
1068
- kind: 'windowOrder'
1069
- ids: string[]
1070
- totalCount?: number
1071
- total_count?: number
1072
- window?: QueryWindow | null
1073
- }
1074
- >
1075
- }
1076
- const order = data.changes?.find(
1077
- (change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
1078
- change.kind === 'windowOrder',
1079
- )
1080
- const orderTotalCount = order?.totalCount ?? order?.total_count
1081
- return {
1082
- totalCount:
1083
- typeof data.totalCount === 'number'
1084
- ? data.totalCount
1085
- : typeof data.total_count === 'number'
1086
- ? data.total_count
1087
- : typeof orderTotalCount === 'number'
1088
- ? orderTotalCount
1089
- : null,
1090
- window: data.window ?? order?.window ?? null,
1091
- } satisfies QueryWindowInfo
1092
- }),
1093
- shareReplay({ bufferSize: 1, refCount: true }),
1094
- )
1095
- return {
1096
- tx,
1097
- results$,
1098
- windowInfo$,
1099
- setWindow: (window) => this.setViewWindow(tx, window),
1100
- }
1101
- }
1102
-
1103
- /** Watch a report with automatic deduplication */
1104
- watchReport<R extends Report<unknown>>(
1105
- report: R,
1106
- ): Observable<ReportResult<R>> {
1107
- const cacheKey = reportCacheKey(report)
1108
-
1109
- const existing = this.sharedReports.get(cacheKey)
1110
- if (existing) return existing as Observable<ReportResult<R>>
1111
-
1112
- const tx = uuid()
1113
- const reportName =
1114
- (report as { constructor?: { name?: string } }).constructor?.name ??
1115
- report.reportId
1116
- const wrappedReport: WrappedReport = {
1117
- report: { ...report.report, tx },
1118
- reportId: report.reportId,
1119
- }
1120
-
1121
- this.activeReports.set(tx, wrappedReport)
1122
- this.activeReportNames.set(tx, reportName)
1123
- this.logConnection('report_subscribe', {
1124
- tx,
1125
- reportId: wrappedReport.reportId,
1126
- reportName,
1127
- report: report.report,
1128
- activeReports: this.activeReports.size,
1129
- })
1130
- this.send({ event: MykoEvent.Report, data: wrappedReport })
1131
-
1132
- const shared$ = this.reportResponses.pipe(
1133
- filter((r) => r.data.tx === tx),
1134
- map((r) => {
1135
- this.logConnection('report_response', {
1136
- tx,
1137
- reportId: wrappedReport.reportId,
1138
- reportName,
1139
- })
1140
- return r
1141
- }),
1142
- map((r) => r.data.response as ReportResult<R>),
1143
- finalize(() => {
1144
- this.logConnection('report_cancel', {
1145
- tx,
1146
- reportId: wrappedReport.reportId,
1147
- reportName,
1148
- activeReportsBefore: this.activeReports.size,
1149
- })
1150
- this.sharedReports.delete(cacheKey)
1151
- this.activeReports.delete(tx)
1152
- this.activeReportNames.delete(tx)
1153
- this.send({ event: MykoEvent.ReportCancel, data: { tx } })
1154
- }),
1155
- shareReplay({ bufferSize: 1, refCount: true }),
1156
- // Defensive copy: prevent one subscriber's mutations from affecting others
1157
- map((x) => {
1158
- if (Array.isArray(x)) return x.slice() as ReportResult<R>
1159
- if (x && typeof x === 'object') {
1160
- return { ...(x as Record<string, unknown>) } as ReportResult<R>
1161
- }
1162
- return x
1163
- }),
1164
- )
1165
-
1166
- this.sharedReports.set(cacheKey, shared$)
1167
- return shared$
1168
- }
1169
-
1170
- // ─────────────────────────────────────────────────────────────────────────────
1171
- // Commands & Events
1172
- // ─────────────────────────────────────────────────────────────────────────────
1173
-
1174
- /** Send an event to the server */
1175
- sendEvent(event: MEvent): void {
1176
- // Pulses are latency-sensitive; bypass batching and send immediately.
1177
- if (event.itemType === 'Pulse') {
1178
- this.flushPendingEventBatch()
1179
- this.sendNow({ event: MykoEvent.Event, data: event })
1180
- return
1181
- }
1182
- this.sendEventBatch([event])
1183
- }
1184
-
1185
- /** Send a batch of events to the server */
1186
- sendEventBatch(events: MEvent[]): void {
1187
- if (events.length === 0) return
1188
-
1189
- const buffered: MEvent[] = []
1190
- const immediatePulses: MEvent[] = []
1191
- for (const event of events) {
1192
- if (event.itemType === 'Pulse') {
1193
- immediatePulses.push(event)
1194
- } else {
1195
- buffered.push(event)
1196
- }
1197
- }
1198
-
1199
- if (buffered.length > 0) {
1200
- this.pendingEventBatch.push(...buffered)
1201
- }
1202
-
1203
- if (immediatePulses.length > 0) {
1204
- this.flushPendingEventBatch()
1205
- for (const pulse of immediatePulses) {
1206
- this.sendNow({ event: MykoEvent.Event, data: pulse })
1207
- }
1208
- return
1209
- }
1210
-
1211
- if (this.pendingEventBatch.length >= this.eventBatchMaxSize) {
1212
- this.flushPendingEventBatch()
1213
- return
1214
- }
1215
- this.scheduleEventBatchFlush()
1216
- }
1217
-
1218
- /** Send a command and wait for response */
1219
- sendCommand<C extends Command<unknown>>(
1220
- command: C,
1221
- ): Promise<CommandResult<C>> {
1222
- const tx = uuid()
1223
-
1224
- const wrappedCommand = {
1225
- command: {
1226
- ...command.command,
1227
- tx,
1228
- createdAt: new Date().toISOString(),
1229
- ...(this.userToken && { userToken: this.userToken }),
1230
- },
1231
- commandId: command.commandId,
1232
- }
1233
-
1234
- return new Promise<CommandResult<C>>((resolve, reject) => {
1235
- const responseSub = this.commandResponses
1236
- .pipe(filter((r) => r.data.tx === tx))
1237
- .subscribe((r) => {
1238
- cleanup()
1239
- resolve(r.data.response as CommandResult<C>)
1240
- })
1241
-
1242
- const errorSub = this.commandErrors
1243
- .pipe(filter((r) => r.data.tx === tx))
1244
- .subscribe((r) => {
1245
- cleanup()
1246
- reject(new Error(r.data.message))
1247
- })
1248
-
1249
- const cleanup = () => {
1250
- responseSub.unsubscribe()
1251
- errorSub.unsubscribe()
1252
- }
1253
-
1254
- this.send({ event: MykoEvent.Command, data: wrappedCommand })
1255
- })
1256
- }
1257
-
1258
- // ─────────────────────────────────────────────────────────────────────────────
1259
- // Private: Socket Management
1260
- // ─────────────────────────────────────────────────────────────────────────────
1261
-
1262
- private getFirstOpenSocket(): ManagedSocket | null {
1263
- for (const m of this.sockets.values()) {
1264
- if (m.ws.readyState === WebSocket.OPEN) return m
1265
- }
1266
- return null
1267
- }
1268
-
1269
- private hasOpenConnection(): boolean {
1270
- return this.getFirstOpenSocket() !== null
1271
- }
1272
-
1273
- private hasConnectionTo(address: string): boolean {
1274
- return this.endpointSockets.has(this.endpointKey(address))
1275
- }
1276
-
1277
- private parseAddress(address: string): { host: string; port: number } | null {
1278
- try {
1279
- const url = new URL(address)
1280
- const port = url.port
1281
- ? parseInt(url.port, 10)
1282
- : url.protocol === 'wss:'
1283
- ? 443
1284
- : 80
1285
- return { host: url.hostname.toLowerCase(), port }
1286
- } catch {
1287
- return null
1288
- }
1289
- }
1290
-
1291
- private endpointKey(address: string): string {
1292
- const parsed = this.parseAddress(address)
1293
- if (!parsed) return address
1294
- const host = this.hostsEquivalent(parsed.host, 'localhost')
1295
- ? 'localhost'
1296
- : parsed.host
1297
- return `${host}:${parsed.port}`
1298
- }
1299
-
1300
- private hostsEquivalent(a: string, b: string): boolean {
1301
- if (a === b) return true
1302
-
1303
- const loopback = new Set(['localhost', '127.0.0.1', '::1'])
1304
- return loopback.has(a) && loopback.has(b)
1305
- }
1306
-
1307
- private closeAllSockets(): void {
1308
- for (const timer of this.reconnectTimers.values()) clearTimeout(timer)
1309
- this.reconnectTimers.clear()
1310
- this.endpointSockets.clear()
1311
-
1312
- for (const m of this.sockets.values()) {
1313
- m.ws.onclose = null
1314
- m.ws.onerror = null
1315
- m.ws.onopen = null
1316
- m.ws.onmessage = null
1317
- m.ws.close()
1318
- }
1319
- this.sockets.clear()
1320
- this.setCurrentServer(null, 'all sockets closed')
1321
- this.setConnectionStatus(
1322
- ConnectionStatus.Disconnected,
1323
- 'all sockets closed',
1324
- )
1325
- }
1326
-
1327
- private createSocket(address: string, reconnectOnClose = true): void {
1328
- const endpointKey = this.endpointKey(address)
1329
- if (this.endpointSockets.has(endpointKey)) return
1330
- this.endpointSockets.set(endpointKey, address)
1331
- const reconnectTimer = this.reconnectTimers.get(endpointKey)
1332
- if (reconnectTimer) {
1333
- clearTimeout(reconnectTimer)
1334
- this.reconnectTimers.delete(endpointKey)
1335
- }
1336
-
1337
- this.logConnection('socket_connecting', {
1338
- address,
1339
- openServers: this.getOpenServers(),
1340
- knownServers: this.getServers(),
1341
- })
1342
- const ws = new WebSocket(address)
1343
- ws.binaryType = 'arraybuffer' // Receive binary messages as ArrayBuffer for msgpack
1344
- const managed: ManagedSocket = {
1345
- ws,
1346
- address,
1347
- endpointKey,
1348
- reconnectOnClose,
1349
- }
1350
- this.sockets.set(address, managed)
1351
-
1352
- if (this.sockets.size === 1) {
1353
- this.setConnectionStatus(
1354
- ConnectionStatus.Connecting,
1355
- `connecting to ${address}`,
1356
- )
1357
- }
1358
-
1359
- ws.onopen = () => {
1360
- if (this.sockets.get(address) !== managed) {
1361
- ws.close()
1362
- return
1363
- }
1364
-
1365
- if (!this.currentServer) {
1366
- this.setCurrentServer(address, 'socket open')
1367
- this.setConnectionStatus(
1368
- ConnectionStatus.Connected,
1369
- `connected to ${address}`,
1370
- )
1371
- this.flushQueue()
1372
- this.resendSubscriptions()
1373
- if (this.peerDiscoveryEnabled) this.startPeerDiscovery()
1374
- }
1375
- }
1376
-
1377
- ws.onclose = () => {
1378
- if (this.sockets.get(address) !== managed) return
1379
-
1380
- this.sockets.delete(address)
1381
- this.endpointSockets.delete(endpointKey)
1382
-
1383
- if (this.currentServer === address) {
1384
- const next = this.getFirstOpenSocket()
1385
- if (next) {
1386
- this.setCurrentServer(
1387
- next.address,
1388
- `failover from ${address} to ${next.address}`,
1389
- )
1390
- this.setConnectionStatus(
1391
- ConnectionStatus.Connected,
1392
- `main failover to ${next.address}`,
1393
- )
1394
- this.resendSubscriptions()
1395
- return // Peer discovery will re-add this server when it's back
1396
- }
1397
- this.setCurrentServer(null, `disconnected from ${address}`)
1398
- this.setConnectionStatus(
1399
- ConnectionStatus.Disconnected,
1400
- `no open servers after ${address} closed`,
1401
- )
1402
- }
1403
-
1404
- // Only retry explicitly configured sockets when completely disconnected.
1405
- // Discovered peers are expected to reappear via peer discovery.
1406
- if (
1407
- managed.reconnectOnClose &&
1408
- this.shouldReconnect &&
1409
- !this.hasOpenConnection()
1410
- ) {
1411
- this.scheduleReconnect(address, endpointKey)
1412
- }
1413
- }
1414
-
1415
- ws.onerror = () => {}
1416
-
1417
- ws.onmessage = (event) => {
1418
- if (this.sockets.get(address) !== managed) return
1419
- this.onMessage(event.data)
1420
- }
1421
- }
1422
-
1423
- private scheduleReconnect(address: string, endpointKey: string): void {
1424
- if (this.reconnectTimers.has(endpointKey)) return
1425
-
1426
- this.logConnection('reconnect_scheduled', { address, delayMs: 1000 })
1427
- const timer = setTimeout(() => {
1428
- this.reconnectTimers.delete(endpointKey)
1429
- if (
1430
- this.shouldReconnect &&
1431
- !this.hasOpenConnection() &&
1432
- !this.endpointSockets.has(endpointKey)
1433
- ) {
1434
- this.logConnection('reconnect_attempt', { address })
1435
- this.createSocket(address)
1436
- }
1437
- }, 1000)
1438
-
1439
- this.reconnectTimers.set(endpointKey, timer)
1440
- }
1441
-
1442
- // ─────────────────────────────────────────────────────────────────────────────
1443
- // Private: Message Handling
1444
- // ─────────────────────────────────────────────────────────────────────────────
1445
-
1446
- private onMessage(data: string | ArrayBuffer | Blob): void {
1447
- this.downMsgCounter.next()
1448
-
1449
- try {
1450
- let message: MykoMessage
1451
-
1452
- if (typeof data === 'string') {
1453
- // JSON text message
1454
- message = JSON.parse(data) as MykoMessage
1455
- } else if (data instanceof ArrayBuffer) {
1456
- // Binary msgpack message
1457
- message = unpackr.unpack(new Uint8Array(data)) as MykoMessage
1458
- } else if (data instanceof Blob) {
1459
- // Handle Blob asynchronously - convert to ArrayBuffer first
1460
- data.arrayBuffer().then((buffer) => {
1461
- const decoded = unpackr.unpack(new Uint8Array(buffer)) as MykoMessage
1462
- this.routeMessage(decoded)
1463
- })
1464
- return
1465
- } else {
1466
- return
1467
- }
1468
-
1469
- this.routeMessage(message)
1470
- } catch {
1471
- // Ignore parse errors
1472
- }
1473
- }
1474
-
1475
- private routeMessage(message: MykoMessage): void {
1476
- switch (message.event) {
1477
- case MykoEvent.QueryResponse:
1478
- this.maybeLogFirstResponseTiming('query', message.data.tx, {
1479
- queryId: this.activeQueries.get(message.data.tx)?.queryId,
1480
- queryName:
1481
- this.activeQueryNames.get(message.data.tx) ??
1482
- this.activeQueries.get(message.data.tx)?.queryId ??
1483
- 'unknown',
1484
- sequence: message.data.sequence,
1485
- upserts: message.data.upserts.length,
1486
- deletes: message.data.deletes.length,
1487
- })
1488
- this.logConnection('query_response', {
1489
- tx: message.data.tx,
1490
- queryId: this.activeQueries.get(message.data.tx)?.queryId,
1491
- queryItemType: this.activeQueries.get(message.data.tx)?.queryItemType,
1492
- queryName:
1493
- this.activeQueryNames.get(message.data.tx) ??
1494
- this.activeQueries.get(message.data.tx)?.queryId ??
1495
- 'unknown',
1496
- sequence: message.data.sequence,
1497
- upserts: message.data.upserts.length,
1498
- deletes: message.data.deletes.length,
1499
- changes: (message.data as { changes?: unknown[] }).changes?.length ?? 0,
1500
- totalCount:
1501
- (message.data as { totalCount?: number; total_count?: number }).totalCount ??
1502
- (message.data as { totalCount?: number; total_count?: number }).total_count ??
1503
- null,
1504
- window: (message.data as { window?: QueryWindow | null }).window ?? null,
1505
- })
1506
- const queryPublishStarted = this.nowMs()
1507
- this.queryResponses.next(message)
1508
- this.logConnection('query_publish_ms', {
1509
- tx: message.data.tx,
1510
- sequence: message.data.sequence,
1511
- publishMs: Number((this.nowMs() - queryPublishStarted).toFixed(2)),
1512
- })
1513
- break
1514
- case MykoEvent.ViewResponse:
1515
- // ViewResponse and QueryResponse share the same payload shape.
1516
- const viewMessage = message as unknown as QueryResponseMessage
1517
- this.maybeLogFirstResponseTiming(
1518
- 'view',
1519
- viewMessage.data.tx,
1520
- {
1521
- viewId: this.activeViews.get(viewMessage.data.tx)?.viewId,
1522
- viewName:
1523
- this.activeViewNames.get(viewMessage.data.tx) ??
1524
- this.activeViews.get(viewMessage.data.tx)?.viewId ??
1525
- 'unknown',
1526
- sequence: viewMessage.data.sequence,
1527
- upserts: viewMessage.data.upserts.length,
1528
- deletes: viewMessage.data.deletes.length,
1529
- },
1530
- )
1531
- this.logConnection('view_response', {
1532
- tx: viewMessage.data.tx,
1533
- sequence: viewMessage.data.sequence,
1534
- upserts: viewMessage.data.upserts.length,
1535
- deletes: viewMessage.data.deletes.length,
1536
- changes: (viewMessage.data as { changes?: unknown[] }).changes?.length ?? 0,
1537
- totalCount: (viewMessage.data as { totalCount?: number; total_count?: number })
1538
- .totalCount ??
1539
- (viewMessage.data as { totalCount?: number; total_count?: number }).total_count ??
1540
- null,
1541
- window: (viewMessage.data as { window?: QueryWindow | null }).window ?? null,
1542
- })
1543
- const viewPublishStarted = this.nowMs()
1544
- this.queryResponses.next(viewMessage)
1545
- this.logConnection('view_publish_ms', {
1546
- tx: viewMessage.data.tx,
1547
- sequence: viewMessage.data.sequence,
1548
- publishMs: Number((this.nowMs() - viewPublishStarted).toFixed(2)),
1549
- })
1550
- break
1551
- case MykoEvent.ReportResponse:
1552
- this.reportResponses.next(message)
1553
- break
1554
- case MykoEvent.CommandResponse:
1555
- this.commandResponses.next(message as CommandResponseMessage)
1556
- break
1557
- case MykoEvent.CommandError:
1558
- this.commandErrors.next(message as CommandErrorMessage)
1559
- break
1560
- case MykoEvent.QueryError:
1561
- this.queryErrors.next(message as QueryErrorMessage)
1562
- this.logConnection('query_error', {
1563
- tx: message.data.tx,
1564
- message: message.data.message,
1565
- queryId: this.activeQueries.get(message.data.tx)?.queryId,
1566
- queryItemType: this.activeQueries.get(message.data.tx)?.queryItemType,
1567
- queryName:
1568
- this.activeQueryNames.get(message.data.tx) ??
1569
- this.activeQueries.get(message.data.tx)?.queryId ??
1570
- 'unknown',
1571
- })
1572
- break
1573
- case MykoEvent.ViewError:
1574
- this.queryErrors.next(message as unknown as QueryErrorMessage)
1575
- this.logConnection('view_error', {
1576
- tx: message.data.tx,
1577
- message: message.data.message,
1578
- viewId: this.activeViews.get(message.data.tx)?.viewId,
1579
- viewItemType: this.activeViews.get(message.data.tx)?.viewItemType,
1580
- viewName:
1581
- this.activeViewNames.get(message.data.tx) ??
1582
- this.activeViews.get(message.data.tx)?.viewId ??
1583
- 'unknown',
1584
- })
1585
- break
1586
- case MykoEvent.ReportError:
1587
- this.reportErrors.next(message as ReportErrorMessage)
1588
- this.logConnection('report_error', {
1589
- tx: message.data.tx,
1590
- message: message.data.message,
1591
- reportId: this.activeReports.get(message.data.tx)?.reportId,
1592
- reportName:
1593
- this.activeReportNames.get(message.data.tx) ??
1594
- this.activeReports.get(message.data.tx)?.reportId ??
1595
- 'unknown',
1596
- })
1597
- break
1598
- case MykoEvent.Ping:
1599
- this.pingResponses.next(message as PingMessage)
1600
- break
1601
- case MykoEvent.Command:
1602
- this.commandIncoming.next(message as CommandIncomingMessage)
1603
- break
1604
- }
1605
- }
1606
-
1607
- private scheduleEventBatchFlush(): void {
1608
- if (this.eventBatchFlushScheduled) return
1609
- this.eventBatchFlushScheduled = true
1610
- queueMicrotask(() => {
1611
- this.eventBatchFlushScheduled = false
1612
- this.flushPendingEventBatch()
1613
- })
1614
- }
1615
-
1616
- private flushPendingEventBatch(): void {
1617
- if (this.pendingEventBatch.length === 0) return
1618
-
1619
- while (this.pendingEventBatch.length > 0) {
1620
- const batch = this.pendingEventBatch.splice(0, this.eventBatchMaxSize)
1621
- this.sendNow({ event: EVENT_BATCH, data: batch } as unknown as MykoMessage)
1622
- }
1623
- }
1624
-
1625
- private send(message: MykoMessage): void {
1626
- if ((message as { event: string }).event !== EVENT_BATCH) {
1627
- this.flushPendingEventBatch()
1628
- }
1629
- this.sendNow(message)
1630
- }
1631
-
1632
- private messageTx(message: MykoMessage): string | null {
1633
- const data = (message as { data?: unknown }).data as
1634
- | { tx?: string }
1635
- | undefined
1636
- return typeof data?.tx === 'string' ? data.tx : null
1637
- }
1638
-
1639
- private sendNow(message: MykoMessage): void {
1640
- const event = (message as { event: string }).event
1641
- const tx = this.messageTx(message)
1642
- if (this.currentServer) {
1643
- const managed = this.sockets.get(this.currentServer)
1644
- if (managed?.ws.readyState === WebSocket.OPEN) {
1645
- const encoded =
1646
- this.protocol === MykoProtocol.MSGPACK
1647
- ? new Uint8Array(packr.pack(message))
1648
- : JSON.stringify(message)
1649
- managed.ws.send(encoded)
1650
- this.upMsgCounter.next()
1651
- return
1652
- }
1653
- }
1654
- this.messageQueue.push(message)
1655
- this.logConnection('ws_enqueue', {
1656
- event,
1657
- tx,
1658
- queueDepth: this.messageQueue.length,
1659
- currentServer: this.currentServer,
1660
- })
1661
- }
1662
-
1663
- private flushQueue(): void {
1664
- const queue = this.messageQueue
1665
- this.messageQueue = []
1666
- this.logConnection('ws_flush_queue', {
1667
- queuedMessages: queue.length,
1668
- currentServer: this.currentServer,
1669
- })
1670
- for (const msg of queue) this.send(msg)
1671
- }
1672
-
1673
- private withReconnectSequenceReset(query: WrappedQuery): WrappedQuery {
1674
- const queryPayload = query.query
1675
- if (
1676
- queryPayload &&
1677
- typeof queryPayload === 'object' &&
1678
- !Array.isArray(queryPayload)
1679
- ) {
1680
- return {
1681
- ...query,
1682
- query: {
1683
- ...(queryPayload as Record<string, JsonValue>),
1684
- seq: 0 as JsonValue,
1685
- },
1686
- }
1687
- }
1688
- return query
1689
- }
1690
-
1691
- private resendSubscriptions(): void {
1692
- for (const q of this.activeQueries.values()) {
1693
- this.send({
1694
- event: MykoEvent.Query,
1695
- data: this.withReconnectSequenceReset(q),
1696
- })
1697
- }
1698
- for (const v of this.activeViews.values()) {
1699
- this.send({ event: MykoEvent.View, data: v })
1700
- }
1701
- for (const r of this.activeReports.values()) {
1702
- this.send({ event: MykoEvent.Report, data: r })
1703
- }
1704
- }
1705
-
1706
- private setConnectionStatus(status: ConnectionStatus, reason: string): void {
1707
- this.connectionStatusSubject.next(status)
1708
- this.logConnection('status', {
1709
- status,
1710
- reason,
1711
- currentServer: this.currentServer,
1712
- openServers: this.getOpenServers(),
1713
- })
1714
- }
1715
-
1716
- private setCurrentServer(server: string | null, reason: string): void {
1717
- this.currentServer = server
1718
- this.currentServerSubject.next(server)
1719
- this.logConnection('current_server', { server, reason })
1720
- }
1721
-
1722
- private nowMs(): number {
1723
- if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
1724
- return performance.now()
1725
- }
1726
- return Date.now()
1727
- }
1728
-
1729
- private maybeLogFirstResponseTiming(
1730
- kind: 'query' | 'view',
1731
- tx: string,
1732
- details: Record<string, unknown>,
1733
- ): void {
1734
- if (this.firstResponseLogged.has(tx)) return
1735
- const startedAt = this.subscriptionStartMs.get(tx)
1736
- if (startedAt === undefined) return
1737
- const elapsedMs = this.nowMs() - startedAt
1738
- this.firstResponseLogged.add(tx)
1739
- this.logConnection(`${kind}_first_response_timing`, {
1740
- tx,
1741
- subscribeToFirstResponseMs: Number(elapsedMs.toFixed(2)),
1742
- ...details,
1743
- })
1744
- }
1745
-
1746
- private connectionLogLevel(
1747
- event: string,
1748
- ): 'info' | 'debug' | 'verbose' | 'warn' | 'error' {
1749
- // Main lifecycle state transitions remain visible at info.
1750
- const infoEvents = new Set(['status', 'current_server'])
1751
- if (infoEvents.has(event)) return 'info'
1752
-
1753
- // Subscription setup and first-response timing are debug-level diagnostics.
1754
- const debugEvents = new Set([
1755
- 'socket_connecting',
1756
- 'peer_discovery_started',
1757
- 'peer_discovery_update',
1758
- 'query_subscribe',
1759
- 'view_subscribe',
1760
- 'report_subscribe',
1761
- 'query_cancel',
1762
- 'view_cancel',
1763
- 'report_cancel',
1764
- 'query_first_response_timing',
1765
- 'view_first_response_timing',
1766
- 'reconnect_scheduled',
1767
- 'reconnect_attempt',
1768
- 'query_shape_invalid',
1769
- ])
1770
- if (debugEvents.has(event)) return 'debug'
1771
-
1772
- // Follow-up response churn and transport internals are verbose-level.
1773
- const verboseEvents = new Set([
1774
- 'ws_enqueue',
1775
- 'ws_flush_queue',
1776
- 'query_response',
1777
- 'view_response',
1778
- 'report_response',
1779
- 'query_publish_ms',
1780
- 'view_publish_ms',
1781
- ])
1782
- if (verboseEvents.has(event)) return 'verbose'
1783
-
1784
- if (event.endsWith('_error')) return 'error'
1785
-
1786
- return 'debug'
1787
- }
1788
-
1789
- private static resolveDefaultConnectionLogLevel(): ConnectionLogLevel {
1790
- const readGlobal = (): string | undefined => {
1791
- const globalObj = globalThis as Record<string, unknown>
1792
- const value = globalObj.MYKO_CLIENT_LOG_LEVEL
1793
- return typeof value === 'string' ? value : undefined
1794
- }
1795
-
1796
- const readProcessEnv = (): string | undefined => {
1797
- const processLike = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
1798
- return processLike?.env?.MYKO_CLIENT_LOG_LEVEL
1799
- }
1800
-
1801
- const value = (readGlobal() ?? readProcessEnv() ?? '').toLowerCase()
1802
- switch (value) {
1803
- case 'silent':
1804
- case 'error':
1805
- case 'warn':
1806
- case 'info':
1807
- case 'debug':
1808
- case 'verbose':
1809
- return value
1810
- default:
1811
- return 'warn'
1812
- }
1813
- }
1814
-
1815
- private shouldLogConnection(level: ConnectionLogLevel): boolean {
1816
- const priority: Record<ConnectionLogLevel, number> = {
1817
- silent: 0,
1818
- error: 1,
1819
- warn: 2,
1820
- info: 3,
1821
- debug: 4,
1822
- verbose: 5,
1823
- }
1824
- return priority[level] <= priority[this.connectionLogLevelThreshold]
1825
- }
1826
-
1827
- private logConnection(event: string, details: Record<string, unknown>): void {
1828
- const level = this.connectionLogLevel(event)
1829
- if (!this.shouldLogConnection(level)) return
1830
- const maybeVerbose = (
1831
- console as unknown as { verbose?: (...args: unknown[]) => void }
1832
- ).verbose
1833
- const logger: (...args: unknown[]) => void =
1834
- level === 'info'
1835
- ? console.info
1836
- : level === 'warn'
1837
- ? console.warn
1838
- : level === 'error'
1839
- ? console.error
1840
- : level === 'verbose' || level === 'debug'
1841
- ? maybeVerbose ?? console.debug ?? console.log
1842
- : console.log
1843
- const levelPrefix = level === 'verbose' ? '[verbose] ' : ''
1844
-
1845
- logger(`${levelPrefix}[MykoClient] ${event}`, {
1846
- tsIso: new Date().toISOString(),
1847
- tsMs: Date.now(),
1848
- ...details,
1849
- })
1850
- }
1851
- }