@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,469 @@
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
+
39
+ /** Why a beat settled the way it did. */
40
+ export type BeatOutcome =
41
+ /** Answered. The peer is there and the channel works. */
42
+ | 'success'
43
+ /** A definite answer that was an error. It arrived, so the channel works. */
44
+ | 'refused'
45
+ /** Unanswered, but the socket was provably delivering data. Excused. */
46
+ | 'lapse'
47
+ /** Unanswered and believed, but held back so a slow transfer can catch up. */
48
+ | 'grace'
49
+ /** Unanswered and believed. The caller should act on this one. */
50
+ | 'miss';
51
+
52
+ /**
53
+ * Running account of the beat, readable at any time and reported when a socket
54
+ * dies.
55
+ *
56
+ * A socket can die before any beat settles, so `sent` counts beats issued
57
+ * rather than outcomes observed: unanswered beats are the strongest evidence
58
+ * there is that the peer was gone, and an account built from settled outcomes
59
+ * alone would report the most telling deaths as having nothing to say.
60
+ */
61
+ export type HeartbeatAccount = {
62
+ /**
63
+ * Consecutive believed silences. Only an unanswered beat that nothing excused
64
+ * counts here, so this is what decides when the beat is lost - and why a
65
+ * refusal, which is an answer, can never close a connection on its own.
66
+ */
67
+ readonly failures: number;
68
+ /**
69
+ * Unanswered beats excused because the socket was provably delivering data at
70
+ * the time. Purely diagnostic: this is the difference between a dead
71
+ * connection and a live one whose request channel went quiet, which no close
72
+ * code can tell apart.
73
+ *
74
+ * Cleared by an answered beat or a new connection, not by a refusal - see
75
+ * {@link HeartbeatAccount.unanswered}.
76
+ */
77
+ readonly lapses: number;
78
+ readonly lastError?: string;
79
+ readonly lastReceivedAt?: number;
80
+ readonly lastSuccessAt?: number;
81
+ /**
82
+ * When the beat still waiting for its answer went out, or absent if none is.
83
+ *
84
+ * `sent` says a beat was issued and {@link HeartbeatAccount.lastSuccessAt}
85
+ * says one was answered; neither says whether the last one came back. Without
86
+ * this, a socket that answered a beat and then went silent is indistinguishable
87
+ * from one answering right up to the close.
88
+ *
89
+ * Deliberately survives {@link SocketHeartbeat.stop}, which is the first thing
90
+ * a caller does with a dead connection: the account is read after the socket
91
+ * is gone, no settle is coming for a retired beat, and clearing the stamp on
92
+ * the way out would erase the only record that a beat was outstanding.
93
+ */
94
+ readonly pendingSince?: number;
95
+ /**
96
+ * Consecutive beats answered with an error. Kept apart from
97
+ * {@link HeartbeatAccount.failures} because a refusal proves the channel
98
+ * works: it belongs in the report, never in the decision to give up on a
99
+ * connection.
100
+ */
101
+ readonly refusals: number;
102
+ /** Every beat issued, including one still waiting. */
103
+ readonly sent: number;
104
+ /**
105
+ * Beats that got no answer, excused or not. This is what the deferral bound is
106
+ * measured against, so it is also what says how long a silence had been
107
+ * tolerated when the beat was finally believed.
108
+ *
109
+ * Saturates at {@link SocketHeartbeatOptions.maxConsecutiveUnanswered}, so
110
+ * the run it reports can never exceed the bound it is measured against -
111
+ * beats keep going out after the alarm is raised, and an unbounded count
112
+ * would describe a silence longer than the one that was acted on.
113
+ *
114
+ * Cleared only by a definite success, proof of life, or a new connection. A
115
+ * refusal leaves it standing: it answers one beat, and were it to clear the
116
+ * run, a channel alternating between timing out and refusing could defer the
117
+ * bound forever while inbound traffic excused every silence.
118
+ */
119
+ readonly unanswered: number;
120
+ };
121
+
122
+ export interface SocketHeartbeatOptions {
123
+ /** Issues one beat. The cheapest authorized round trip the channel offers. */
124
+ beat: () => Promise<unknown>;
125
+ /** Gap between beats. A beat still in flight when one is due skips it. */
126
+ intervalMs: number;
127
+ /** How long one beat waits for its answer before it counts as unanswered. */
128
+ timeoutMs: number;
129
+ /**
130
+ * Consecutive genuine misses before the beat is declared lost. Defaults to
131
+ * {@link DEFAULT_MAX_CONSECUTIVE_MISSES}.
132
+ */
133
+ maxConsecutiveMisses?: number;
134
+ /**
135
+ * Consecutive unanswered beats to tolerate before neither proof of life nor
136
+ * grace may excuse another. Defaults to
137
+ * {@link DEFAULT_MAX_CONSECUTIVE_UNANSWERED}.
138
+ */
139
+ maxConsecutiveUnanswered?: number;
140
+ /**
141
+ * Renders a rejection for the account. Channels that reject with plain
142
+ * objects rather than `Error`s need this, or the cause reads
143
+ * `[object Object]`.
144
+ */
145
+ describeError?: (error: unknown) => string;
146
+ /**
147
+ * Whether a rejection means the beat went unanswered, as opposed to being
148
+ * refused. Only an unanswered beat can lapse or be believed: a refusal is a
149
+ * definite answer about the peer, and treating it as silence would hide the
150
+ * very thing it reports. Defaults to recognizing this heartbeat's own
151
+ * deadline.
152
+ */
153
+ isUnanswered?: (error: unknown) => boolean;
154
+ /**
155
+ * The beat is lost: enough consecutive beats went unanswered that the peer
156
+ * cannot be assumed to be there.
157
+ *
158
+ * Edge-triggered. It fires on the beat that crosses the threshold and stays
159
+ * quiet until proof of life re-arms it, because the counters it reports are
160
+ * an account read at socket death - resetting them to re-arm the alarm would
161
+ * erase the evidence the death report is built from.
162
+ *
163
+ * Throwing from here says the loss could not be acted on - the socket lookup
164
+ * failed, the close was rejected - and re-arms the alarm, because the
165
+ * connection this was meant to tear down is still there. A caller that
166
+ * swallows its own failure instead gets one alarm for a teardown that never
167
+ * happened.
168
+ *
169
+ * A refusal never reaches this. The peer answered, so whatever is wrong is
170
+ * not the connection, and tearing one down over an application-level refusal
171
+ * would replace a working channel with an outage.
172
+ */
173
+ onLost: (account: HeartbeatAccount) => void;
174
+ /** Every settled beat, for telemetry. Receives the account at settle time. */
175
+ onOutcome?: (outcome: BeatOutcome, durationMs: number, account: HeartbeatAccount) => void;
176
+ /**
177
+ * Whether the channel can carry a beat at all right now - an authorized
178
+ * credential, an open socket. A gate that is closed skips the beat entirely
179
+ * rather than counting it, because a channel with nothing to protect yet has
180
+ * neither missed nor answered anything.
181
+ */
182
+ shouldBeat?: () => boolean;
183
+ }
184
+
185
+ export const DEFAULT_MAX_CONSECUTIVE_MISSES = 2;
186
+ export const DEFAULT_MAX_CONSECUTIVE_UNANSWERED = 8;
187
+
188
+ type Timer = ReturnType<typeof setTimeout>;
189
+
190
+ /** Keeps a Node timer from holding the process open; a no-op in a browser. */
191
+ function unref(timer: Timer): void {
192
+ (timer as unknown as { unref?: () => void }).unref?.();
193
+ }
194
+
195
+ /** This heartbeat's own deadline, so a rejection it raised is recognizable. */
196
+ class BeatTimeoutError extends Error {
197
+ public constructor(timeoutMs: number) {
198
+ super(`beat timed out after ${timeoutMs}ms`);
199
+ this.name = 'BeatTimeoutError';
200
+ }
201
+ }
202
+
203
+ function describeUnknownError(error: unknown): string {
204
+ if (error instanceof Error) {
205
+ return error.message;
206
+ }
207
+ if (typeof error === 'object' && error !== null) {
208
+ const { message } = error as { message?: unknown };
209
+ if (typeof message === 'string' && message.length > 0) {
210
+ return message;
211
+ }
212
+ try {
213
+ return JSON.stringify(error);
214
+ } catch {
215
+ return String(error);
216
+ }
217
+ }
218
+ return String(error);
219
+ }
220
+
221
+ type MutableAccount = {
222
+ -readonly [K in keyof HeartbeatAccount]: HeartbeatAccount[K];
223
+ };
224
+
225
+ const emptyAccount = (): MutableAccount => ({ failures: 0, lapses: 0, refusals: 0, sent: 0, unanswered: 0 });
226
+
227
+ export class SocketHeartbeat {
228
+ private inFlight = false;
229
+ private intervalTimer: Timer | undefined;
230
+ /**
231
+ * Identifies the current chain of scheduled beats.
232
+ *
233
+ * Separate from {@link epoch} on purpose: a reset means a new connection, not
234
+ * a new cadence, so retiring an in-flight beat must not also unhook the
235
+ * interval that would have issued the next one.
236
+ */
237
+ private intervalToken = 0;
238
+ /** Whether the alarm has already been raised for the current run of misses. */
239
+ private lost = false;
240
+ /**
241
+ * Bumped by every start, stop and reset.
242
+ *
243
+ * A deadline armed for a previous connection fires after a reconnect - an
244
+ * intentional close does not reject the request it was waiting on - and
245
+ * letting it tick the new run's counters would condemn a healthy socket on
246
+ * the strength of a dead one's silence.
247
+ */
248
+ private epoch = 0;
249
+ private state: MutableAccount = emptyAccount();
250
+
251
+ public constructor(private readonly options: SocketHeartbeatOptions) {}
252
+
253
+ /** A snapshot, so a caller holding it cannot be surprised by a later beat. */
254
+ public get account(): HeartbeatAccount {
255
+ return { ...this.state };
256
+ }
257
+
258
+ /**
259
+ * Begins beating on a fresh account, replacing any cadence already running.
260
+ *
261
+ * Returns the way to stop *this* cadence. The disposer is inert once a later
262
+ * start has replaced it, which matters whenever one heartbeat is shared
263
+ * across successive connections: a superseded connection's teardown arrives
264
+ * late, and an unguarded stop would silently take the live connection's beat
265
+ * down with it.
266
+ */
267
+ public start(): () => void {
268
+ this.stop();
269
+ this.reset();
270
+ this.schedule();
271
+ const token = this.intervalToken;
272
+ return () => {
273
+ if (token === this.intervalToken) {
274
+ this.stop();
275
+ }
276
+ };
277
+ }
278
+
279
+ public stop(): void {
280
+ this.invalidate();
281
+ this.intervalToken += 1;
282
+ if (this.intervalTimer) {
283
+ clearTimeout(this.intervalTimer);
284
+ this.intervalTimer = undefined;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Starts the account over for a new connection, without touching the
290
+ * interval.
291
+ *
292
+ * Separate from {@link start} because a caller may learn it has a new
293
+ * connection before it is ready to beat on one, and the old socket's armed
294
+ * deadline has to stop counting the moment that happens rather than when the
295
+ * first new beat goes out.
296
+ */
297
+ public reset(): void {
298
+ this.invalidate();
299
+ this.state = emptyAccount();
300
+ }
301
+
302
+ /**
303
+ * Beats once, now, outside the interval.
304
+ *
305
+ * For callers with a reason to check immediately - a user who just came back
306
+ * to the keyboard - rather than waiting out a cadence sized for idle
307
+ * connections. Safe alongside a running interval: the in-flight guard means
308
+ * one of the two simply skips.
309
+ */
310
+ public async beatNow(): Promise<void> {
311
+ await this.fire(this.epoch);
312
+ }
313
+
314
+ /**
315
+ * Stamps inbound traffic. Call this for every message the socket delivers:
316
+ * it is what lets a beat starved behind a bulk transfer be told apart from
317
+ * one starved by a peer that is gone.
318
+ */
319
+ public recordReceive(): void {
320
+ this.state.lastReceivedAt = Date.now();
321
+ }
322
+
323
+ /** Retires whatever is in flight, so its settle cannot touch the next run. */
324
+ private invalidate(): void {
325
+ this.epoch += 1;
326
+ this.inFlight = false;
327
+ this.lost = false;
328
+ }
329
+
330
+ private schedule(): void {
331
+ const token = this.intervalToken;
332
+ this.intervalTimer = setTimeout(() => {
333
+ if (token !== this.intervalToken) {
334
+ return;
335
+ }
336
+ // Rescheduled before the beat is issued rather than after it settles, so
337
+ // the interval stays the gap between beats instead of drifting by however
338
+ // long each answer took.
339
+ this.schedule();
340
+ void this.fire(this.epoch);
341
+ }, this.options.intervalMs);
342
+ unref(this.intervalTimer);
343
+ }
344
+
345
+ private async fire(armedEpoch: number): Promise<void> {
346
+ if (this.inFlight || this.options.shouldBeat?.() === false) {
347
+ return;
348
+ }
349
+ this.inFlight = true;
350
+ this.state.sent += 1;
351
+ const sentAt = Date.now();
352
+ this.state.pendingSince = sentAt;
353
+ let deadline: Timer | undefined;
354
+ try {
355
+ await Promise.race([
356
+ this.options.beat(),
357
+ new Promise<never>((_, reject) => {
358
+ deadline = setTimeout(() => reject(new BeatTimeoutError(this.options.timeoutMs)), this.options.timeoutMs);
359
+ unref(deadline);
360
+ })
361
+ ]);
362
+ this.settle(armedEpoch, sentAt, undefined);
363
+ } catch (error) {
364
+ this.settle(armedEpoch, sentAt, error ?? new Error('beat failed'));
365
+ } finally {
366
+ if (deadline !== undefined) {
367
+ clearTimeout(deadline);
368
+ }
369
+ // Only this epoch's beat owns the guard. Releasing it after a reset would
370
+ // let a stale settle admit a second concurrent beat.
371
+ if (armedEpoch === this.epoch) {
372
+ this.inFlight = false;
373
+ }
374
+ }
375
+ }
376
+
377
+ private settle(armedEpoch: number, sentAt: number, error: unknown): void {
378
+ if (armedEpoch !== this.epoch) {
379
+ // A beat belonging to a connection that is already gone. It says nothing
380
+ // about the current one, so it is neither counted nor reported.
381
+ return;
382
+ }
383
+ const durationMs = Date.now() - sentAt;
384
+ // Nothing is outstanding any more: this beat is the one that was, and a
385
+ // stale-epoch beat returned above without touching the current run's stamp.
386
+ delete this.state.pendingSince;
387
+ const outcome = error === undefined ? this.recordAnswer() : this.recordRejection(error, sentAt);
388
+ const account = this.account;
389
+ this.options.onOutcome?.(outcome, durationMs, account);
390
+ if (outcome === 'miss' && !this.lost) {
391
+ this.lost = true;
392
+ try {
393
+ this.options.onLost(account);
394
+ } catch {
395
+ // The caller could not act on the loss, so the connection it meant to
396
+ // tear down is still there and still not answering. Re-arm, or the one
397
+ // alarm this run gets has been spent on a teardown that never happened.
398
+ // Swallowed rather than rethrown: `settle` already runs inside the
399
+ // beat's own catch, and throwing from here escapes `fire` as an
400
+ // unhandled rejection.
401
+ this.lost = false;
402
+ }
403
+ }
404
+ }
405
+
406
+ private recordAnswer(): BeatOutcome {
407
+ this.state.failures = 0;
408
+ this.state.lapses = 0;
409
+ this.state.refusals = 0;
410
+ this.state.unanswered = 0;
411
+ this.state.lastSuccessAt = Date.now();
412
+ this.lost = false;
413
+ delete this.state.lastError;
414
+ return 'success';
415
+ }
416
+
417
+ private recordRejection(error: unknown, sentAt: number): BeatOutcome {
418
+ const describe = this.options.describeError ?? describeUnknownError;
419
+ if (!(this.options.isUnanswered ?? ((cause) => cause instanceof BeatTimeoutError))(error)) {
420
+ // A definite answer, and recorded as one: a peer that answers is
421
+ // reachable, so a refusal must never close a connection over what is an
422
+ // application-level problem.
423
+ //
424
+ // It answers this beat and no others, so every count of what went
425
+ // missing is left exactly as it stands. Clearing `failures` would spend
426
+ // a refusal on the grace meant for a starved pong; clearing `unanswered`
427
+ // would hand a channel that alternates between timing out and refusing
428
+ // an unlimited deferral, because where inbound traffic excuses every
429
+ // silence that counter is the only thing left that can end one.
430
+ this.state.refusals += 1;
431
+ this.state.lastError = describe(error);
432
+ return 'refused';
433
+ }
434
+ // Saturating rather than growing: this is what the caller reports as the
435
+ // length of the silence that killed the connection, and beats keep going
436
+ // out after the alarm is raised - the caller still has a socket to tear
437
+ // down - so an unbounded count would report a run longer than the bound it
438
+ // is measured against.
439
+ this.state.unanswered = Math.min(this.state.unanswered + 1, this.maxConsecutiveUnanswered);
440
+ // The cause of every unanswered beat, excused or not. A lapse that leaves
441
+ // this stale would have the log for an excused timeout name whatever failed
442
+ // before it.
443
+ this.state.lastError = describe(error);
444
+ const excusable = this.state.unanswered < this.maxConsecutiveUnanswered;
445
+ if (excusable && this.receivedSince(sentAt)) {
446
+ this.state.lapses += 1;
447
+ // Traffic during the beat's own wait proves the connection is alive, which
448
+ // clears an earlier suspicion as surely as an answer would.
449
+ this.state.failures = 0;
450
+ this.lost = false;
451
+ return 'lapse';
452
+ }
453
+ this.state.failures += 1;
454
+ return excusable && this.state.failures < this.maxConsecutiveMisses ? 'grace' : 'miss';
455
+ }
456
+
457
+ /** Traffic that arrived while this beat was waiting, not merely at some point. */
458
+ private receivedSince(sentAt: number): boolean {
459
+ return this.state.lastReceivedAt !== undefined && this.state.lastReceivedAt >= sentAt;
460
+ }
461
+
462
+ private get maxConsecutiveMisses(): number {
463
+ return this.options.maxConsecutiveMisses ?? DEFAULT_MAX_CONSECUTIVE_MISSES;
464
+ }
465
+
466
+ private get maxConsecutiveUnanswered(): number {
467
+ return this.options.maxConsecutiveUnanswered ?? DEFAULT_MAX_CONSECUTIVE_UNANSWERED;
468
+ }
469
+ }
@@ -1,5 +1,6 @@
1
1
  // the implementation of ISocket
2
2
  // this is protocol agnostic, it only handles the communication
3
+ export * from './heartbeat.js';
3
4
  export * from './socket.js';
4
5
  export * from './socketWithClientAuth.js';
5
6
  export * from './types.js';
@@ -33,7 +33,7 @@ describe('connectWebSocket User-Agent', () => {
33
33
 
34
34
  expect(constructorSpy).toHaveBeenCalledWith(
35
35
  'wss://example.com/api/v1/rpc-ws',
36
- undefined,
36
+ [],
37
37
  expect.objectContaining({
38
38
  maxPayload: MAX_SOCKET_PAYLOAD_BYTES,
39
39
  headers: expect.objectContaining({ 'User-Agent': DEFAULT_WEBSOCKET_USER_AGENT })
@@ -46,7 +46,7 @@ describe('connectWebSocket User-Agent', () => {
46
46
 
47
47
  expect(constructorSpy).toHaveBeenCalledWith(
48
48
  'wss://example.com/api/v1/rpc-ws',
49
- undefined,
49
+ [],
50
50
  expect.objectContaining({
51
51
  headers: expect.objectContaining({ 'User-Agent': 'superblocks-cli/9.9.9' })
52
52
  })
@@ -58,6 +58,18 @@ describe('connectWebSocket User-Agent', () => {
58
58
 
59
59
  expect(constructorSpy).toHaveBeenCalledWith('wss://example.com/api/v1/rpc-ws', 'editor', expect.anything());
60
60
  });
61
+
62
+ it('offers no subprotocol at all when the caller asked for none', () => {
63
+ // A browser turns an explicit `undefined` here into the literal string
64
+ // "undefined" on the wire, so a caller that wanted no role ends up offering
65
+ // one no server implements. An empty list is the only value that reaches the
66
+ // handshake as "no subprotocol offered".
67
+ void connectWebSocket('wss://example.com/api/v1/rpc-ws');
68
+
69
+ const [, protocol] = constructorSpy.mock.calls[0] as [string, unknown, unknown];
70
+ expect(protocol).toEqual([]);
71
+ expect(protocol).not.toBeUndefined();
72
+ });
61
73
  });
62
74
 
63
75
  describe('buildClientUserAgent', () => {
@@ -45,7 +45,21 @@ export interface ConnectWebSocketOptions {
45
45
  export function connectWebSocket(wsUrl: string, options: ConnectWebSocketOptions = {}): Promise<WebSocket> {
46
46
  const { protocol, timeout = 30_000, headers } = options;
47
47
  return new Promise((resolve, reject) => {
48
- const ws = new WebSocket(wsUrl, protocol, {
48
+ // `protocol ?? []` normalizes "no role asked for" to the empty list. It is
49
+ // defensive only: both runtimes this reaches already collapse an explicit
50
+ // `undefined` to the same thing, so neither offers a subprotocol that was
51
+ // never requested. A browser applies the WebIDL default (`protocols` is
52
+ // optional with default `[]`, and an explicit `undefined` selects it), and
53
+ // isomorphic-ws hands browsers the native WebSocket. Node takes `ws`, which
54
+ // maps `undefined` to `[]` before building the header (websocket.js: `if
55
+ // (protocols === undefined) protocols = []`).
56
+ //
57
+ // What does break a handshake is a *string* naming a role the server does
58
+ // not implement: it answers without echoing a subprotocol, the client fails
59
+ // the handshake, and it reads as a dead dev server rather than a bad offer.
60
+ // `??` cannot catch that - a string is not nullish - so a caller that
61
+ // stringifies its own value still offers the bogus role.
62
+ const ws = new WebSocket(wsUrl, protocol ?? [], {
49
63
  maxPayload: MAX_SOCKET_PAYLOAD_BYTES,
50
64
  headers: { 'User-Agent': DEFAULT_WEBSOCKET_USER_AGENT, ...headers }
51
65
  });
@@ -60,9 +60,12 @@ export interface AiResolveAttachmentLeasesResponse {
60
60
  * System event trigger types for AI operations without prompt_id
61
61
  */
62
62
  export enum AiSystemTriggerType {
63
+ API_USAGE_SUMMARY = 'api_usage_summary',
63
64
  APP_NAME = 'app_name',
65
+ ATTACHMENT_LABEL = 'attachment_label',
64
66
  COMMIT_MESSAGE = 'commit_message',
65
67
  DASHBOARD_CHAT = 'dashboard_chat',
68
+ ERROR_SUMMARY = 'error_summary',
66
69
  FACT_METADATA = 'fact_metadata',
67
70
  PR_DESCRIPTION = 'pr_description'
68
71
  }
@@ -479,6 +479,20 @@ export interface IHomepageApplicationV2Dto extends IResourceMetadata {
479
479
  name: string;
480
480
  email: string;
481
481
  };
482
+ /** Per-requesting-user last editor open; null when this user has never opened the app. */
483
+ lastOpenedAt?: Date | null;
484
+ }
485
+
486
+ /** Where the user last opened the app; drives Home recent-card destination. */
487
+ export type ApplicationOpenMode = 'deployed' | 'editor';
488
+
489
+ /** One item in the per-user "recently opened" list on Home. */
490
+ export interface IRecentApplicationDto {
491
+ devEnvEnabled: boolean;
492
+ id: string;
493
+ lastOpenedAt: Date;
494
+ lastOpenMode: ApplicationOpenMode;
495
+ name: string;
482
496
  }
483
497
 
484
498
  export interface ApplicationV2GlobalDto {
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveScannedPackageVersion } from './index.js';
4
+
5
+ describe('resolveScannedPackageVersion', () => {
6
+ it('prefers an exact resolved version', () => {
7
+ expect(resolveScannedPackageVersion('^8.5.18', '8.5.20')).toBe('8.5.20');
8
+ });
9
+
10
+ it("falls back to the declared range's minimum version when unresolved", () => {
11
+ expect(resolveScannedPackageVersion('^8.5.18', null)).toBe('8.5.18');
12
+ });
13
+
14
+ it('falls back to the declared range when resolved is an empty string', () => {
15
+ expect(resolveScannedPackageVersion('^8.5.18', '')).toBe('8.5.18');
16
+ });
17
+
18
+ it('returns null for specs semver cannot parse', () => {
19
+ expect(resolveScannedPackageVersion('workspace:*', null)).toBeNull();
20
+ });
21
+ });