@superblocksteam/shared 0.9607.3 → 0.9608.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 (294) hide show
  1. package/dist/database-lifecycle/appDatabaseCompatibility.d.ts +49 -0
  2. package/dist/database-lifecycle/appDatabaseCompatibility.d.ts.map +1 -0
  3. package/dist/database-lifecycle/appDatabaseCompatibility.js +32 -0
  4. package/dist/database-lifecycle/appDatabaseCompatibility.js.map +1 -0
  5. package/dist/database-lifecycle/appDatabaseCompatibility.test.d.ts +2 -0
  6. package/dist/database-lifecycle/appDatabaseCompatibility.test.d.ts.map +1 -0
  7. package/dist/database-lifecycle/appDatabaseCompatibility.test.js +47 -0
  8. package/dist/database-lifecycle/appDatabaseCompatibility.test.js.map +1 -0
  9. package/dist/database-lifecycle/destructiveMigrations.d.ts +12 -0
  10. package/dist/database-lifecycle/destructiveMigrations.d.ts.map +1 -1
  11. package/dist/database-lifecycle/destructiveMigrations.js +114 -0
  12. package/dist/database-lifecycle/destructiveMigrations.js.map +1 -1
  13. package/dist/database-lifecycle/destructiveMigrations.test.js +79 -0
  14. package/dist/database-lifecycle/destructiveMigrations.test.js.map +1 -1
  15. package/dist/database-lifecycle/index.d.ts +46 -2
  16. package/dist/database-lifecycle/index.d.ts.map +1 -1
  17. package/dist/database-lifecycle/index.js +127 -1
  18. package/dist/database-lifecycle/index.js.map +1 -1
  19. package/dist/database-lifecycle/providerDiagnostics.test.d.ts +2 -0
  20. package/dist/database-lifecycle/providerDiagnostics.test.d.ts.map +1 -0
  21. package/dist/database-lifecycle/providerDiagnostics.test.js +119 -0
  22. package/dist/database-lifecycle/providerDiagnostics.test.js.map +1 -0
  23. package/dist/environment-compatibility/appEnvironmentCompatibility.d.ts +65 -0
  24. package/dist/environment-compatibility/appEnvironmentCompatibility.d.ts.map +1 -0
  25. package/dist/environment-compatibility/appEnvironmentCompatibility.js +87 -0
  26. package/dist/environment-compatibility/appEnvironmentCompatibility.js.map +1 -0
  27. package/dist/environment-compatibility/appEnvironmentCompatibility.test.d.ts +2 -0
  28. package/dist/environment-compatibility/appEnvironmentCompatibility.test.d.ts.map +1 -0
  29. package/dist/environment-compatibility/appEnvironmentCompatibility.test.js +285 -0
  30. package/dist/environment-compatibility/appEnvironmentCompatibility.test.js.map +1 -0
  31. package/dist/errors/http.d.ts +15 -4
  32. package/dist/errors/http.d.ts.map +1 -1
  33. package/dist/errors/http.js +25 -7
  34. package/dist/errors/http.js.map +1 -1
  35. package/dist/errors/http.test.d.ts +2 -0
  36. package/dist/errors/http.test.d.ts.map +1 -0
  37. package/dist/errors/http.test.js +28 -0
  38. package/dist/errors/http.test.js.map +1 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/plugins/templates/restApiIntegration.d.ts.map +1 -1
  44. package/dist/plugins/templates/restApiIntegration.js +2 -0
  45. package/dist/plugins/templates/restApiIntegration.js.map +1 -1
  46. package/dist/plugins/templates/shared/auth.d.ts +3 -1
  47. package/dist/plugins/templates/shared/auth.d.ts.map +1 -1
  48. package/dist/plugins/templates/shared/auth.js +129 -17
  49. package/dist/plugins/templates/shared/auth.js.map +1 -1
  50. package/dist/plugins/templates/shared/auth.test.d.ts +2 -0
  51. package/dist/plugins/templates/shared/auth.test.d.ts.map +1 -0
  52. package/dist/plugins/templates/shared/auth.test.js +116 -0
  53. package/dist/plugins/templates/shared/auth.test.js.map +1 -0
  54. package/dist/plugins/templates/shared/restapiintegration.d.ts +2 -0
  55. package/dist/plugins/templates/shared/restapiintegration.d.ts.map +1 -1
  56. package/dist/plugins/templates/shared/restapiintegration.js +3 -1
  57. package/dist/plugins/templates/shared/restapiintegration.js.map +1 -1
  58. package/dist/socket/dist-entry.test.d.ts +2 -0
  59. package/dist/socket/dist-entry.test.d.ts.map +1 -0
  60. package/dist/socket/dist-entry.test.js +34 -0
  61. package/dist/socket/dist-entry.test.js.map +1 -0
  62. package/dist/socket/heartbeat.d.ts +258 -0
  63. package/dist/socket/heartbeat.d.ts.map +1 -0
  64. package/dist/socket/heartbeat.js +307 -0
  65. package/dist/socket/heartbeat.js.map +1 -0
  66. package/dist/socket/heartbeat.test.d.ts +2 -0
  67. package/dist/socket/heartbeat.test.d.ts.map +1 -0
  68. package/dist/socket/heartbeat.test.js +431 -0
  69. package/dist/socket/heartbeat.test.js.map +1 -0
  70. package/dist/socket/index.d.ts +1 -0
  71. package/dist/socket/index.d.ts.map +1 -1
  72. package/dist/socket/index.js +1 -0
  73. package/dist/socket/index.js.map +1 -1
  74. package/dist/socket/util.d.ts.map +1 -1
  75. package/dist/socket/util.js +15 -1
  76. package/dist/socket/util.js.map +1 -1
  77. package/dist/socket/util.test.js +12 -2
  78. package/dist/socket/util.test.js.map +1 -1
  79. package/dist/types/ai/index.d.ts +3 -0
  80. package/dist/types/ai/index.d.ts.map +1 -1
  81. package/dist/types/ai/index.js +3 -0
  82. package/dist/types/ai/index.js.map +1 -1
  83. package/dist/types/application/index.d.ts +12 -0
  84. package/dist/types/application/index.d.ts.map +1 -1
  85. package/dist/types/application/index.js.map +1 -1
  86. package/dist/types/codeSecurity/index.d.ts +55 -1
  87. package/dist/types/codeSecurity/index.d.ts.map +1 -1
  88. package/dist/types/codeSecurity/index.js +21 -1
  89. package/dist/types/codeSecurity/index.js.map +1 -1
  90. package/dist/types/codeSecurity/index.test.d.ts +2 -0
  91. package/dist/types/codeSecurity/index.test.d.ts.map +1 -0
  92. package/dist/types/codeSecurity/index.test.js +19 -0
  93. package/dist/types/codeSecurity/index.test.js.map +1 -0
  94. package/dist/types/datasource/auth.d.ts +12 -1
  95. package/dist/types/datasource/auth.d.ts.map +1 -1
  96. package/dist/types/datasource/auth.js +20 -2
  97. package/dist/types/datasource/auth.js.map +1 -1
  98. package/dist/types/datasource/auth.test.js +3 -1
  99. package/dist/types/datasource/auth.test.js.map +1 -1
  100. package/dist/types/group/index.d.ts +1 -3
  101. package/dist/types/group/index.d.ts.map +1 -1
  102. package/dist/types/group/index.js +1 -3
  103. package/dist/types/group/index.js.map +1 -1
  104. package/dist/types/integration/index.d.ts +11 -1
  105. package/dist/types/integration/index.d.ts.map +1 -1
  106. package/dist/types/integration/index.js +11 -1
  107. package/dist/types/integration/index.js.map +1 -1
  108. package/dist/types/plugin/form.d.ts +32 -0
  109. package/dist/types/plugin/form.d.ts.map +1 -1
  110. package/dist/types/plugin/form.js +73 -1
  111. package/dist/types/plugin/form.js.map +1 -1
  112. package/dist/types/plugin/form.test.js +128 -0
  113. package/dist/types/plugin/form.test.js.map +1 -1
  114. package/dist/types/profile/index.d.ts +16 -1
  115. package/dist/types/profile/index.d.ts.map +1 -1
  116. package/dist/types/profile/index.js +7 -1
  117. package/dist/types/profile/index.js.map +1 -1
  118. package/dist/types/rbac/index.d.ts +2 -0
  119. package/dist/types/rbac/index.d.ts.map +1 -1
  120. package/dist/types/rbac/index.js +2 -0
  121. package/dist/types/rbac/index.js.map +1 -1
  122. package/dist/types/response/GetRecentApplicationsResponseBody.d.ts +4 -0
  123. package/dist/types/response/GetRecentApplicationsResponseBody.d.ts.map +1 -0
  124. package/dist/types/response/GetRecentApplicationsResponseBody.js +3 -0
  125. package/dist/types/response/GetRecentApplicationsResponseBody.js.map +1 -0
  126. package/dist/types/response/index.d.ts +1 -0
  127. package/dist/types/response/index.d.ts.map +1 -1
  128. package/dist/types/response/index.js +1 -0
  129. package/dist/types/response/index.js.map +1 -1
  130. package/dist-esm/database-lifecycle/appDatabaseCompatibility.d.ts +49 -0
  131. package/dist-esm/database-lifecycle/appDatabaseCompatibility.d.ts.map +1 -0
  132. package/dist-esm/database-lifecycle/appDatabaseCompatibility.js +28 -0
  133. package/dist-esm/database-lifecycle/appDatabaseCompatibility.js.map +1 -0
  134. package/dist-esm/database-lifecycle/appDatabaseCompatibility.test.d.ts +2 -0
  135. package/dist-esm/database-lifecycle/appDatabaseCompatibility.test.d.ts.map +1 -0
  136. package/dist-esm/database-lifecycle/appDatabaseCompatibility.test.js +45 -0
  137. package/dist-esm/database-lifecycle/appDatabaseCompatibility.test.js.map +1 -0
  138. package/dist-esm/database-lifecycle/destructiveMigrations.d.ts +12 -0
  139. package/dist-esm/database-lifecycle/destructiveMigrations.d.ts.map +1 -1
  140. package/dist-esm/database-lifecycle/destructiveMigrations.js +113 -0
  141. package/dist-esm/database-lifecycle/destructiveMigrations.js.map +1 -1
  142. package/dist-esm/database-lifecycle/destructiveMigrations.test.js +80 -1
  143. package/dist-esm/database-lifecycle/destructiveMigrations.test.js.map +1 -1
  144. package/dist-esm/database-lifecycle/index.d.ts +46 -2
  145. package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
  146. package/dist-esm/database-lifecycle/index.js +123 -1
  147. package/dist-esm/database-lifecycle/index.js.map +1 -1
  148. package/dist-esm/database-lifecycle/providerDiagnostics.test.d.ts +2 -0
  149. package/dist-esm/database-lifecycle/providerDiagnostics.test.d.ts.map +1 -0
  150. package/dist-esm/database-lifecycle/providerDiagnostics.test.js +117 -0
  151. package/dist-esm/database-lifecycle/providerDiagnostics.test.js.map +1 -0
  152. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.d.ts +65 -0
  153. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.d.ts.map +1 -0
  154. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.js +79 -0
  155. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.js.map +1 -0
  156. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.test.d.ts +2 -0
  157. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.test.d.ts.map +1 -0
  158. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.test.js +283 -0
  159. package/dist-esm/environment-compatibility/appEnvironmentCompatibility.test.js.map +1 -0
  160. package/dist-esm/errors/http.d.ts +15 -4
  161. package/dist-esm/errors/http.d.ts.map +1 -1
  162. package/dist-esm/errors/http.js +22 -6
  163. package/dist-esm/errors/http.js.map +1 -1
  164. package/dist-esm/errors/http.test.d.ts +2 -0
  165. package/dist-esm/errors/http.test.d.ts.map +1 -0
  166. package/dist-esm/errors/http.test.js +26 -0
  167. package/dist-esm/errors/http.test.js.map +1 -0
  168. package/dist-esm/index.d.ts +2 -0
  169. package/dist-esm/index.d.ts.map +1 -1
  170. package/dist-esm/index.js +2 -0
  171. package/dist-esm/index.js.map +1 -1
  172. package/dist-esm/plugins/templates/restApiIntegration.d.ts.map +1 -1
  173. package/dist-esm/plugins/templates/restApiIntegration.js +2 -0
  174. package/dist-esm/plugins/templates/restApiIntegration.js.map +1 -1
  175. package/dist-esm/plugins/templates/shared/auth.d.ts +3 -1
  176. package/dist-esm/plugins/templates/shared/auth.d.ts.map +1 -1
  177. package/dist-esm/plugins/templates/shared/auth.js +129 -17
  178. package/dist-esm/plugins/templates/shared/auth.js.map +1 -1
  179. package/dist-esm/plugins/templates/shared/auth.test.d.ts +2 -0
  180. package/dist-esm/plugins/templates/shared/auth.test.d.ts.map +1 -0
  181. package/dist-esm/plugins/templates/shared/auth.test.js +114 -0
  182. package/dist-esm/plugins/templates/shared/auth.test.js.map +1 -0
  183. package/dist-esm/plugins/templates/shared/restapiintegration.d.ts +2 -0
  184. package/dist-esm/plugins/templates/shared/restapiintegration.d.ts.map +1 -1
  185. package/dist-esm/plugins/templates/shared/restapiintegration.js +3 -1
  186. package/dist-esm/plugins/templates/shared/restapiintegration.js.map +1 -1
  187. package/dist-esm/socket/dist-entry.test.d.ts +2 -0
  188. package/dist-esm/socket/dist-entry.test.d.ts.map +1 -0
  189. package/dist-esm/socket/dist-entry.test.js +29 -0
  190. package/dist-esm/socket/dist-entry.test.js.map +1 -0
  191. package/dist-esm/socket/heartbeat.d.ts +258 -0
  192. package/dist-esm/socket/heartbeat.d.ts.map +1 -0
  193. package/dist-esm/socket/heartbeat.js +303 -0
  194. package/dist-esm/socket/heartbeat.js.map +1 -0
  195. package/dist-esm/socket/heartbeat.test.d.ts +2 -0
  196. package/dist-esm/socket/heartbeat.test.d.ts.map +1 -0
  197. package/dist-esm/socket/heartbeat.test.js +429 -0
  198. package/dist-esm/socket/heartbeat.test.js.map +1 -0
  199. package/dist-esm/socket/index.d.ts +1 -0
  200. package/dist-esm/socket/index.d.ts.map +1 -1
  201. package/dist-esm/socket/index.js +1 -0
  202. package/dist-esm/socket/index.js.map +1 -1
  203. package/dist-esm/socket/util.d.ts.map +1 -1
  204. package/dist-esm/socket/util.js +15 -1
  205. package/dist-esm/socket/util.js.map +1 -1
  206. package/dist-esm/socket/util.test.js +12 -2
  207. package/dist-esm/socket/util.test.js.map +1 -1
  208. package/dist-esm/types/ai/index.d.ts +3 -0
  209. package/dist-esm/types/ai/index.d.ts.map +1 -1
  210. package/dist-esm/types/ai/index.js +3 -0
  211. package/dist-esm/types/ai/index.js.map +1 -1
  212. package/dist-esm/types/application/index.d.ts +12 -0
  213. package/dist-esm/types/application/index.d.ts.map +1 -1
  214. package/dist-esm/types/application/index.js.map +1 -1
  215. package/dist-esm/types/codeSecurity/index.d.ts +55 -1
  216. package/dist-esm/types/codeSecurity/index.d.ts.map +1 -1
  217. package/dist-esm/types/codeSecurity/index.js +17 -1
  218. package/dist-esm/types/codeSecurity/index.js.map +1 -1
  219. package/dist-esm/types/codeSecurity/index.test.d.ts +2 -0
  220. package/dist-esm/types/codeSecurity/index.test.d.ts.map +1 -0
  221. package/dist-esm/types/codeSecurity/index.test.js +17 -0
  222. package/dist-esm/types/codeSecurity/index.test.js.map +1 -0
  223. package/dist-esm/types/datasource/auth.d.ts +12 -1
  224. package/dist-esm/types/datasource/auth.d.ts.map +1 -1
  225. package/dist-esm/types/datasource/auth.js +19 -2
  226. package/dist-esm/types/datasource/auth.js.map +1 -1
  227. package/dist-esm/types/datasource/auth.test.js +3 -1
  228. package/dist-esm/types/datasource/auth.test.js.map +1 -1
  229. package/dist-esm/types/group/index.d.ts +1 -3
  230. package/dist-esm/types/group/index.d.ts.map +1 -1
  231. package/dist-esm/types/group/index.js +1 -3
  232. package/dist-esm/types/group/index.js.map +1 -1
  233. package/dist-esm/types/integration/index.d.ts +11 -1
  234. package/dist-esm/types/integration/index.d.ts.map +1 -1
  235. package/dist-esm/types/integration/index.js +11 -1
  236. package/dist-esm/types/integration/index.js.map +1 -1
  237. package/dist-esm/types/plugin/form.d.ts +32 -0
  238. package/dist-esm/types/plugin/form.d.ts.map +1 -1
  239. package/dist-esm/types/plugin/form.js +68 -0
  240. package/dist-esm/types/plugin/form.js.map +1 -1
  241. package/dist-esm/types/plugin/form.test.js +129 -1
  242. package/dist-esm/types/plugin/form.test.js.map +1 -1
  243. package/dist-esm/types/profile/index.d.ts +16 -1
  244. package/dist-esm/types/profile/index.d.ts.map +1 -1
  245. package/dist-esm/types/profile/index.js +7 -1
  246. package/dist-esm/types/profile/index.js.map +1 -1
  247. package/dist-esm/types/rbac/index.d.ts +2 -0
  248. package/dist-esm/types/rbac/index.d.ts.map +1 -1
  249. package/dist-esm/types/rbac/index.js +2 -0
  250. package/dist-esm/types/rbac/index.js.map +1 -1
  251. package/dist-esm/types/response/GetRecentApplicationsResponseBody.d.ts +4 -0
  252. package/dist-esm/types/response/GetRecentApplicationsResponseBody.d.ts.map +1 -0
  253. package/dist-esm/types/response/GetRecentApplicationsResponseBody.js +2 -0
  254. package/dist-esm/types/response/GetRecentApplicationsResponseBody.js.map +1 -0
  255. package/dist-esm/types/response/index.d.ts +1 -0
  256. package/dist-esm/types/response/index.d.ts.map +1 -1
  257. package/dist-esm/types/response/index.js +1 -0
  258. package/dist-esm/types/response/index.js.map +1 -1
  259. package/package.json +1 -1
  260. package/src/database-lifecycle/appDatabaseCompatibility.test.ts +55 -0
  261. package/src/database-lifecycle/appDatabaseCompatibility.ts +62 -0
  262. package/src/database-lifecycle/destructiveMigrations.test.ts +108 -1
  263. package/src/database-lifecycle/destructiveMigrations.ts +129 -0
  264. package/src/database-lifecycle/index.ts +173 -0
  265. package/src/database-lifecycle/providerDiagnostics.test.ts +134 -0
  266. package/src/environment-compatibility/appEnvironmentCompatibility.test.ts +326 -0
  267. package/src/environment-compatibility/appEnvironmentCompatibility.ts +173 -0
  268. package/src/errors/http.test.ts +32 -0
  269. package/src/errors/http.ts +24 -6
  270. package/src/index.ts +2 -0
  271. package/src/plugins/templates/restApiIntegration.ts +2 -0
  272. package/src/plugins/templates/shared/auth.test.ts +162 -0
  273. package/src/plugins/templates/shared/auth.ts +135 -18
  274. package/src/plugins/templates/shared/restapiintegration.ts +3 -1
  275. package/src/socket/dist-entry.test.ts +29 -0
  276. package/src/socket/heartbeat.test.ts +538 -0
  277. package/src/socket/heartbeat.ts +469 -0
  278. package/src/socket/index.ts +1 -0
  279. package/src/socket/util.test.ts +14 -2
  280. package/src/socket/util.ts +15 -1
  281. package/src/types/ai/index.ts +3 -0
  282. package/src/types/application/index.ts +14 -0
  283. package/src/types/codeSecurity/index.test.ts +21 -0
  284. package/src/types/codeSecurity/index.ts +83 -1
  285. package/src/types/datasource/auth.test.ts +3 -1
  286. package/src/types/datasource/auth.ts +24 -3
  287. package/src/types/group/index.ts +1 -3
  288. package/src/types/integration/index.ts +14 -0
  289. package/src/types/plugin/form.test.ts +174 -1
  290. package/src/types/plugin/form.ts +100 -2
  291. package/src/types/profile/index.ts +19 -1
  292. package/src/types/rbac/index.ts +2 -0
  293. package/src/types/response/GetRecentApplicationsResponseBody.ts +4 -0
  294. package/src/types/response/index.ts +1 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * A paced liveness beat over a request channel.
3
+ *
4
+ * Every long-lived socket in this repo eventually needs the same thing: proof
5
+ * that the peer is still answering, cheaply, for as long as the connection is
6
+ * held. The naive version - an interval that fires a request and counts the
7
+ * failures - is wrong in three ways that only show up under load, and each one
8
+ * was found the hard way:
9
+ *
10
+ * 1. A request inherits the channel's own budget, which is the whole
11
+ * caller-facing operation (minutes). A beat that borrows it cannot report a
12
+ * miss inside any window a caller waits in, so it holds its own deadline.
13
+ * 2. Without an in-flight guard, a peer that stopped answering collects a new
14
+ * pending request every interval, none of which can ever be reconciled.
15
+ * The answer paces the beats, not the interval.
16
+ * 3. A beat's response shares the socket with bulk pushes, so a large transfer
17
+ * head-of-line blocks the answer past any sane deadline while the
18
+ * connection is perfectly healthy. Acting on that kills a busy socket and,
19
+ * where the caller reconnects, re-sends the same payload and does it again.
20
+ *
21
+ * The third is the subtle one, and it takes two guards, because neither is
22
+ * sufficient alone. Inbound traffic during the beat's own wait proves the
23
+ * connection is alive - but a bulk RPC response is a single websocket message,
24
+ * and no receive event fires until its last byte lands, so that cannot be the
25
+ * only guard. A single missed beat therefore also gets grace: a transfer merely
26
+ * has to finish inside one extra interval for the next answer to get through.
27
+ *
28
+ * Both guards answer to one bound. Inbound data proves the receive path, not
29
+ * that the request channel answers, so no amount of either excuse may let a
30
+ * wedged channel hide forever: once
31
+ * {@link SocketHeartbeatOptions.maxConsecutiveUnanswered} beats in a row have
32
+ * gone unanswered, the next silence is believed whatever the reason, and the
33
+ * grace is bypassed with it.
34
+ *
35
+ * Extracted from the browser editor's dev-server heartbeat, which learned all
36
+ * of the above the hard way and is the first thing to beat with it.
37
+ */
38
+ /** Why a beat settled the way it did. */
39
+ export type BeatOutcome =
40
+ /** Answered. The peer is there and the channel works. */
41
+ 'success'
42
+ /** A definite answer that was an error. It arrived, so the channel works. */
43
+ | 'refused'
44
+ /** Unanswered, but the socket was provably delivering data. Excused. */
45
+ | 'lapse'
46
+ /** Unanswered and believed, but held back so a slow transfer can catch up. */
47
+ | 'grace'
48
+ /** Unanswered and believed. The caller should act on this one. */
49
+ | 'miss';
50
+ /**
51
+ * Running account of the beat, readable at any time and reported when a socket
52
+ * dies.
53
+ *
54
+ * A socket can die before any beat settles, so `sent` counts beats issued
55
+ * rather than outcomes observed: unanswered beats are the strongest evidence
56
+ * there is that the peer was gone, and an account built from settled outcomes
57
+ * alone would report the most telling deaths as having nothing to say.
58
+ */
59
+ export type HeartbeatAccount = {
60
+ /**
61
+ * Consecutive believed silences. Only an unanswered beat that nothing excused
62
+ * counts here, so this is what decides when the beat is lost - and why a
63
+ * refusal, which is an answer, can never close a connection on its own.
64
+ */
65
+ readonly failures: number;
66
+ /**
67
+ * Unanswered beats excused because the socket was provably delivering data at
68
+ * the time. Purely diagnostic: this is the difference between a dead
69
+ * connection and a live one whose request channel went quiet, which no close
70
+ * code can tell apart.
71
+ *
72
+ * Cleared by an answered beat or a new connection, not by a refusal - see
73
+ * {@link HeartbeatAccount.unanswered}.
74
+ */
75
+ readonly lapses: number;
76
+ readonly lastError?: string;
77
+ readonly lastReceivedAt?: number;
78
+ readonly lastSuccessAt?: number;
79
+ /**
80
+ * When the beat still waiting for its answer went out, or absent if none is.
81
+ *
82
+ * `sent` says a beat was issued and {@link HeartbeatAccount.lastSuccessAt}
83
+ * says one was answered; neither says whether the last one came back. Without
84
+ * this, a socket that answered a beat and then went silent is indistinguishable
85
+ * from one answering right up to the close.
86
+ *
87
+ * Deliberately survives {@link SocketHeartbeat.stop}, which is the first thing
88
+ * a caller does with a dead connection: the account is read after the socket
89
+ * is gone, no settle is coming for a retired beat, and clearing the stamp on
90
+ * the way out would erase the only record that a beat was outstanding.
91
+ */
92
+ readonly pendingSince?: number;
93
+ /**
94
+ * Consecutive beats answered with an error. Kept apart from
95
+ * {@link HeartbeatAccount.failures} because a refusal proves the channel
96
+ * works: it belongs in the report, never in the decision to give up on a
97
+ * connection.
98
+ */
99
+ readonly refusals: number;
100
+ /** Every beat issued, including one still waiting. */
101
+ readonly sent: number;
102
+ /**
103
+ * Beats that got no answer, excused or not. This is what the deferral bound is
104
+ * measured against, so it is also what says how long a silence had been
105
+ * tolerated when the beat was finally believed.
106
+ *
107
+ * Saturates at {@link SocketHeartbeatOptions.maxConsecutiveUnanswered}, so
108
+ * the run it reports can never exceed the bound it is measured against -
109
+ * beats keep going out after the alarm is raised, and an unbounded count
110
+ * would describe a silence longer than the one that was acted on.
111
+ *
112
+ * Cleared only by a definite success, proof of life, or a new connection. A
113
+ * refusal leaves it standing: it answers one beat, and were it to clear the
114
+ * run, a channel alternating between timing out and refusing could defer the
115
+ * bound forever while inbound traffic excused every silence.
116
+ */
117
+ readonly unanswered: number;
118
+ };
119
+ export interface SocketHeartbeatOptions {
120
+ /** Issues one beat. The cheapest authorized round trip the channel offers. */
121
+ beat: () => Promise<unknown>;
122
+ /** Gap between beats. A beat still in flight when one is due skips it. */
123
+ intervalMs: number;
124
+ /** How long one beat waits for its answer before it counts as unanswered. */
125
+ timeoutMs: number;
126
+ /**
127
+ * Consecutive genuine misses before the beat is declared lost. Defaults to
128
+ * {@link DEFAULT_MAX_CONSECUTIVE_MISSES}.
129
+ */
130
+ maxConsecutiveMisses?: number;
131
+ /**
132
+ * Consecutive unanswered beats to tolerate before neither proof of life nor
133
+ * grace may excuse another. Defaults to
134
+ * {@link DEFAULT_MAX_CONSECUTIVE_UNANSWERED}.
135
+ */
136
+ maxConsecutiveUnanswered?: number;
137
+ /**
138
+ * Renders a rejection for the account. Channels that reject with plain
139
+ * objects rather than `Error`s need this, or the cause reads
140
+ * `[object Object]`.
141
+ */
142
+ describeError?: (error: unknown) => string;
143
+ /**
144
+ * Whether a rejection means the beat went unanswered, as opposed to being
145
+ * refused. Only an unanswered beat can lapse or be believed: a refusal is a
146
+ * definite answer about the peer, and treating it as silence would hide the
147
+ * very thing it reports. Defaults to recognizing this heartbeat's own
148
+ * deadline.
149
+ */
150
+ isUnanswered?: (error: unknown) => boolean;
151
+ /**
152
+ * The beat is lost: enough consecutive beats went unanswered that the peer
153
+ * cannot be assumed to be there.
154
+ *
155
+ * Edge-triggered. It fires on the beat that crosses the threshold and stays
156
+ * quiet until proof of life re-arms it, because the counters it reports are
157
+ * an account read at socket death - resetting them to re-arm the alarm would
158
+ * erase the evidence the death report is built from.
159
+ *
160
+ * Throwing from here says the loss could not be acted on - the socket lookup
161
+ * failed, the close was rejected - and re-arms the alarm, because the
162
+ * connection this was meant to tear down is still there. A caller that
163
+ * swallows its own failure instead gets one alarm for a teardown that never
164
+ * happened.
165
+ *
166
+ * A refusal never reaches this. The peer answered, so whatever is wrong is
167
+ * not the connection, and tearing one down over an application-level refusal
168
+ * would replace a working channel with an outage.
169
+ */
170
+ onLost: (account: HeartbeatAccount) => void;
171
+ /** Every settled beat, for telemetry. Receives the account at settle time. */
172
+ onOutcome?: (outcome: BeatOutcome, durationMs: number, account: HeartbeatAccount) => void;
173
+ /**
174
+ * Whether the channel can carry a beat at all right now - an authorized
175
+ * credential, an open socket. A gate that is closed skips the beat entirely
176
+ * rather than counting it, because a channel with nothing to protect yet has
177
+ * neither missed nor answered anything.
178
+ */
179
+ shouldBeat?: () => boolean;
180
+ }
181
+ export declare const DEFAULT_MAX_CONSECUTIVE_MISSES = 2;
182
+ export declare const DEFAULT_MAX_CONSECUTIVE_UNANSWERED = 8;
183
+ export declare class SocketHeartbeat {
184
+ private readonly options;
185
+ private inFlight;
186
+ private intervalTimer;
187
+ /**
188
+ * Identifies the current chain of scheduled beats.
189
+ *
190
+ * Separate from {@link epoch} on purpose: a reset means a new connection, not
191
+ * a new cadence, so retiring an in-flight beat must not also unhook the
192
+ * interval that would have issued the next one.
193
+ */
194
+ private intervalToken;
195
+ /** Whether the alarm has already been raised for the current run of misses. */
196
+ private lost;
197
+ /**
198
+ * Bumped by every start, stop and reset.
199
+ *
200
+ * A deadline armed for a previous connection fires after a reconnect - an
201
+ * intentional close does not reject the request it was waiting on - and
202
+ * letting it tick the new run's counters would condemn a healthy socket on
203
+ * the strength of a dead one's silence.
204
+ */
205
+ private epoch;
206
+ private state;
207
+ constructor(options: SocketHeartbeatOptions);
208
+ /** A snapshot, so a caller holding it cannot be surprised by a later beat. */
209
+ get account(): HeartbeatAccount;
210
+ /**
211
+ * Begins beating on a fresh account, replacing any cadence already running.
212
+ *
213
+ * Returns the way to stop *this* cadence. The disposer is inert once a later
214
+ * start has replaced it, which matters whenever one heartbeat is shared
215
+ * across successive connections: a superseded connection's teardown arrives
216
+ * late, and an unguarded stop would silently take the live connection's beat
217
+ * down with it.
218
+ */
219
+ start(): () => void;
220
+ stop(): void;
221
+ /**
222
+ * Starts the account over for a new connection, without touching the
223
+ * interval.
224
+ *
225
+ * Separate from {@link start} because a caller may learn it has a new
226
+ * connection before it is ready to beat on one, and the old socket's armed
227
+ * deadline has to stop counting the moment that happens rather than when the
228
+ * first new beat goes out.
229
+ */
230
+ reset(): void;
231
+ /**
232
+ * Beats once, now, outside the interval.
233
+ *
234
+ * For callers with a reason to check immediately - a user who just came back
235
+ * to the keyboard - rather than waiting out a cadence sized for idle
236
+ * connections. Safe alongside a running interval: the in-flight guard means
237
+ * one of the two simply skips.
238
+ */
239
+ beatNow(): Promise<void>;
240
+ /**
241
+ * Stamps inbound traffic. Call this for every message the socket delivers:
242
+ * it is what lets a beat starved behind a bulk transfer be told apart from
243
+ * one starved by a peer that is gone.
244
+ */
245
+ recordReceive(): void;
246
+ /** Retires whatever is in flight, so its settle cannot touch the next run. */
247
+ private invalidate;
248
+ private schedule;
249
+ private fire;
250
+ private settle;
251
+ private recordAnswer;
252
+ private recordRejection;
253
+ /** Traffic that arrived while this beat was waiting, not merely at some point. */
254
+ private receivedSince;
255
+ private get maxConsecutiveMisses();
256
+ private get maxConsecutiveUnanswered();
257
+ }
258
+ //# sourceMappingURL=heartbeat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../src/socket/heartbeat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ /**
3
+ * A paced liveness beat over a request channel.
4
+ *
5
+ * Every long-lived socket in this repo eventually needs the same thing: proof
6
+ * that the peer is still answering, cheaply, for as long as the connection is
7
+ * held. The naive version - an interval that fires a request and counts the
8
+ * failures - is wrong in three ways that only show up under load, and each one
9
+ * was found the hard way:
10
+ *
11
+ * 1. A request inherits the channel's own budget, which is the whole
12
+ * caller-facing operation (minutes). A beat that borrows it cannot report a
13
+ * miss inside any window a caller waits in, so it holds its own deadline.
14
+ * 2. Without an in-flight guard, a peer that stopped answering collects a new
15
+ * pending request every interval, none of which can ever be reconciled.
16
+ * The answer paces the beats, not the interval.
17
+ * 3. A beat's response shares the socket with bulk pushes, so a large transfer
18
+ * head-of-line blocks the answer past any sane deadline while the
19
+ * connection is perfectly healthy. Acting on that kills a busy socket and,
20
+ * where the caller reconnects, re-sends the same payload and does it again.
21
+ *
22
+ * The third is the subtle one, and it takes two guards, because neither is
23
+ * sufficient alone. Inbound traffic during the beat's own wait proves the
24
+ * connection is alive - but a bulk RPC response is a single websocket message,
25
+ * and no receive event fires until its last byte lands, so that cannot be the
26
+ * only guard. A single missed beat therefore also gets grace: a transfer merely
27
+ * has to finish inside one extra interval for the next answer to get through.
28
+ *
29
+ * Both guards answer to one bound. Inbound data proves the receive path, not
30
+ * that the request channel answers, so no amount of either excuse may let a
31
+ * wedged channel hide forever: once
32
+ * {@link SocketHeartbeatOptions.maxConsecutiveUnanswered} beats in a row have
33
+ * gone unanswered, the next silence is believed whatever the reason, and the
34
+ * grace is bypassed with it.
35
+ *
36
+ * Extracted from the browser editor's dev-server heartbeat, which learned all
37
+ * of the above the hard way and is the first thing to beat with it.
38
+ */
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.SocketHeartbeat = exports.DEFAULT_MAX_CONSECUTIVE_UNANSWERED = exports.DEFAULT_MAX_CONSECUTIVE_MISSES = void 0;
41
+ exports.DEFAULT_MAX_CONSECUTIVE_MISSES = 2;
42
+ exports.DEFAULT_MAX_CONSECUTIVE_UNANSWERED = 8;
43
+ /** Keeps a Node timer from holding the process open; a no-op in a browser. */
44
+ function unref(timer) {
45
+ timer.unref?.();
46
+ }
47
+ /** This heartbeat's own deadline, so a rejection it raised is recognizable. */
48
+ class BeatTimeoutError extends Error {
49
+ constructor(timeoutMs) {
50
+ super(`beat timed out after ${timeoutMs}ms`);
51
+ this.name = 'BeatTimeoutError';
52
+ }
53
+ }
54
+ function describeUnknownError(error) {
55
+ if (error instanceof Error) {
56
+ return error.message;
57
+ }
58
+ if (typeof error === 'object' && error !== null) {
59
+ const { message } = error;
60
+ if (typeof message === 'string' && message.length > 0) {
61
+ return message;
62
+ }
63
+ try {
64
+ return JSON.stringify(error);
65
+ }
66
+ catch {
67
+ return String(error);
68
+ }
69
+ }
70
+ return String(error);
71
+ }
72
+ const emptyAccount = () => ({ failures: 0, lapses: 0, refusals: 0, sent: 0, unanswered: 0 });
73
+ class SocketHeartbeat {
74
+ options;
75
+ inFlight = false;
76
+ intervalTimer;
77
+ /**
78
+ * Identifies the current chain of scheduled beats.
79
+ *
80
+ * Separate from {@link epoch} on purpose: a reset means a new connection, not
81
+ * a new cadence, so retiring an in-flight beat must not also unhook the
82
+ * interval that would have issued the next one.
83
+ */
84
+ intervalToken = 0;
85
+ /** Whether the alarm has already been raised for the current run of misses. */
86
+ lost = false;
87
+ /**
88
+ * Bumped by every start, stop and reset.
89
+ *
90
+ * A deadline armed for a previous connection fires after a reconnect - an
91
+ * intentional close does not reject the request it was waiting on - and
92
+ * letting it tick the new run's counters would condemn a healthy socket on
93
+ * the strength of a dead one's silence.
94
+ */
95
+ epoch = 0;
96
+ state = emptyAccount();
97
+ constructor(options) {
98
+ this.options = options;
99
+ }
100
+ /** A snapshot, so a caller holding it cannot be surprised by a later beat. */
101
+ get account() {
102
+ return { ...this.state };
103
+ }
104
+ /**
105
+ * Begins beating on a fresh account, replacing any cadence already running.
106
+ *
107
+ * Returns the way to stop *this* cadence. The disposer is inert once a later
108
+ * start has replaced it, which matters whenever one heartbeat is shared
109
+ * across successive connections: a superseded connection's teardown arrives
110
+ * late, and an unguarded stop would silently take the live connection's beat
111
+ * down with it.
112
+ */
113
+ start() {
114
+ this.stop();
115
+ this.reset();
116
+ this.schedule();
117
+ const token = this.intervalToken;
118
+ return () => {
119
+ if (token === this.intervalToken) {
120
+ this.stop();
121
+ }
122
+ };
123
+ }
124
+ stop() {
125
+ this.invalidate();
126
+ this.intervalToken += 1;
127
+ if (this.intervalTimer) {
128
+ clearTimeout(this.intervalTimer);
129
+ this.intervalTimer = undefined;
130
+ }
131
+ }
132
+ /**
133
+ * Starts the account over for a new connection, without touching the
134
+ * interval.
135
+ *
136
+ * Separate from {@link start} because a caller may learn it has a new
137
+ * connection before it is ready to beat on one, and the old socket's armed
138
+ * deadline has to stop counting the moment that happens rather than when the
139
+ * first new beat goes out.
140
+ */
141
+ reset() {
142
+ this.invalidate();
143
+ this.state = emptyAccount();
144
+ }
145
+ /**
146
+ * Beats once, now, outside the interval.
147
+ *
148
+ * For callers with a reason to check immediately - a user who just came back
149
+ * to the keyboard - rather than waiting out a cadence sized for idle
150
+ * connections. Safe alongside a running interval: the in-flight guard means
151
+ * one of the two simply skips.
152
+ */
153
+ async beatNow() {
154
+ await this.fire(this.epoch);
155
+ }
156
+ /**
157
+ * Stamps inbound traffic. Call this for every message the socket delivers:
158
+ * it is what lets a beat starved behind a bulk transfer be told apart from
159
+ * one starved by a peer that is gone.
160
+ */
161
+ recordReceive() {
162
+ this.state.lastReceivedAt = Date.now();
163
+ }
164
+ /** Retires whatever is in flight, so its settle cannot touch the next run. */
165
+ invalidate() {
166
+ this.epoch += 1;
167
+ this.inFlight = false;
168
+ this.lost = false;
169
+ }
170
+ schedule() {
171
+ const token = this.intervalToken;
172
+ this.intervalTimer = setTimeout(() => {
173
+ if (token !== this.intervalToken) {
174
+ return;
175
+ }
176
+ // Rescheduled before the beat is issued rather than after it settles, so
177
+ // the interval stays the gap between beats instead of drifting by however
178
+ // long each answer took.
179
+ this.schedule();
180
+ void this.fire(this.epoch);
181
+ }, this.options.intervalMs);
182
+ unref(this.intervalTimer);
183
+ }
184
+ async fire(armedEpoch) {
185
+ if (this.inFlight || this.options.shouldBeat?.() === false) {
186
+ return;
187
+ }
188
+ this.inFlight = true;
189
+ this.state.sent += 1;
190
+ const sentAt = Date.now();
191
+ this.state.pendingSince = sentAt;
192
+ let deadline;
193
+ try {
194
+ await Promise.race([
195
+ this.options.beat(),
196
+ new Promise((_, reject) => {
197
+ deadline = setTimeout(() => reject(new BeatTimeoutError(this.options.timeoutMs)), this.options.timeoutMs);
198
+ unref(deadline);
199
+ })
200
+ ]);
201
+ this.settle(armedEpoch, sentAt, undefined);
202
+ }
203
+ catch (error) {
204
+ this.settle(armedEpoch, sentAt, error ?? new Error('beat failed'));
205
+ }
206
+ finally {
207
+ if (deadline !== undefined) {
208
+ clearTimeout(deadline);
209
+ }
210
+ // Only this epoch's beat owns the guard. Releasing it after a reset would
211
+ // let a stale settle admit a second concurrent beat.
212
+ if (armedEpoch === this.epoch) {
213
+ this.inFlight = false;
214
+ }
215
+ }
216
+ }
217
+ settle(armedEpoch, sentAt, error) {
218
+ if (armedEpoch !== this.epoch) {
219
+ // A beat belonging to a connection that is already gone. It says nothing
220
+ // about the current one, so it is neither counted nor reported.
221
+ return;
222
+ }
223
+ const durationMs = Date.now() - sentAt;
224
+ // Nothing is outstanding any more: this beat is the one that was, and a
225
+ // stale-epoch beat returned above without touching the current run's stamp.
226
+ delete this.state.pendingSince;
227
+ const outcome = error === undefined ? this.recordAnswer() : this.recordRejection(error, sentAt);
228
+ const account = this.account;
229
+ this.options.onOutcome?.(outcome, durationMs, account);
230
+ if (outcome === 'miss' && !this.lost) {
231
+ this.lost = true;
232
+ try {
233
+ this.options.onLost(account);
234
+ }
235
+ catch {
236
+ // The caller could not act on the loss, so the connection it meant to
237
+ // tear down is still there and still not answering. Re-arm, or the one
238
+ // alarm this run gets has been spent on a teardown that never happened.
239
+ // Swallowed rather than rethrown: `settle` already runs inside the
240
+ // beat's own catch, and throwing from here escapes `fire` as an
241
+ // unhandled rejection.
242
+ this.lost = false;
243
+ }
244
+ }
245
+ }
246
+ recordAnswer() {
247
+ this.state.failures = 0;
248
+ this.state.lapses = 0;
249
+ this.state.refusals = 0;
250
+ this.state.unanswered = 0;
251
+ this.state.lastSuccessAt = Date.now();
252
+ this.lost = false;
253
+ delete this.state.lastError;
254
+ return 'success';
255
+ }
256
+ recordRejection(error, sentAt) {
257
+ const describe = this.options.describeError ?? describeUnknownError;
258
+ if (!(this.options.isUnanswered ?? ((cause) => cause instanceof BeatTimeoutError))(error)) {
259
+ // A definite answer, and recorded as one: a peer that answers is
260
+ // reachable, so a refusal must never close a connection over what is an
261
+ // application-level problem.
262
+ //
263
+ // It answers this beat and no others, so every count of what went
264
+ // missing is left exactly as it stands. Clearing `failures` would spend
265
+ // a refusal on the grace meant for a starved pong; clearing `unanswered`
266
+ // would hand a channel that alternates between timing out and refusing
267
+ // an unlimited deferral, because where inbound traffic excuses every
268
+ // silence that counter is the only thing left that can end one.
269
+ this.state.refusals += 1;
270
+ this.state.lastError = describe(error);
271
+ return 'refused';
272
+ }
273
+ // Saturating rather than growing: this is what the caller reports as the
274
+ // length of the silence that killed the connection, and beats keep going
275
+ // out after the alarm is raised - the caller still has a socket to tear
276
+ // down - so an unbounded count would report a run longer than the bound it
277
+ // is measured against.
278
+ this.state.unanswered = Math.min(this.state.unanswered + 1, this.maxConsecutiveUnanswered);
279
+ // The cause of every unanswered beat, excused or not. A lapse that leaves
280
+ // this stale would have the log for an excused timeout name whatever failed
281
+ // before it.
282
+ this.state.lastError = describe(error);
283
+ const excusable = this.state.unanswered < this.maxConsecutiveUnanswered;
284
+ if (excusable && this.receivedSince(sentAt)) {
285
+ this.state.lapses += 1;
286
+ // Traffic during the beat's own wait proves the connection is alive, which
287
+ // clears an earlier suspicion as surely as an answer would.
288
+ this.state.failures = 0;
289
+ this.lost = false;
290
+ return 'lapse';
291
+ }
292
+ this.state.failures += 1;
293
+ return excusable && this.state.failures < this.maxConsecutiveMisses ? 'grace' : 'miss';
294
+ }
295
+ /** Traffic that arrived while this beat was waiting, not merely at some point. */
296
+ receivedSince(sentAt) {
297
+ return this.state.lastReceivedAt !== undefined && this.state.lastReceivedAt >= sentAt;
298
+ }
299
+ get maxConsecutiveMisses() {
300
+ return this.options.maxConsecutiveMisses ?? exports.DEFAULT_MAX_CONSECUTIVE_MISSES;
301
+ }
302
+ get maxConsecutiveUnanswered() {
303
+ return this.options.maxConsecutiveUnanswered ?? exports.DEFAULT_MAX_CONSECUTIVE_UNANSWERED;
304
+ }
305
+ }
306
+ exports.SocketHeartbeat = SocketHeartbeat;
307
+ //# sourceMappingURL=heartbeat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../src/socket/heartbeat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=heartbeat.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat.test.d.ts","sourceRoot":"","sources":["../../src/socket/heartbeat.test.ts"],"names":[],"mappings":""}