handmux 0.23.0 → 0.25.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 (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -0,0 +1,385 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PrivateStateStore } from '../src/privateStateStore.js';
4
+ import { connectBridgeTransport } from '../src/agent-runtime/bridgeTransport.js';
5
+ const NAME_RE = /^[a-z][a-z0-9._-]{0,63}$/;
6
+ const EVENT_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
7
+ function isRecord(value) {
8
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
9
+ }
10
+ function parseState(value) {
11
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.snapshots)
12
+ || !Array.isArray(value.durable)) {
13
+ return { version: 1, snapshots: {}, durable: [] };
14
+ }
15
+ const durable = [];
16
+ const ids = new Set();
17
+ for (const item of value.durable) {
18
+ if (!isRecord(item) || typeof item.channel !== 'string' || !NAME_RE.test(item.channel)
19
+ || typeof item.eventId !== 'string' || !EVENT_ID_RE.test(item.eventId)
20
+ || ids.has(`${item.channel}\0${item.eventId}`))
21
+ continue;
22
+ ids.add(`${item.channel}\0${item.eventId}`);
23
+ durable.push({ channel: item.channel, eventId: item.eventId, payload: item.payload });
24
+ }
25
+ return { version: 1, snapshots: structuredClone(value.snapshots), durable };
26
+ }
27
+ function credential(file) {
28
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
29
+ if (!isRecord(value) || value.version !== 1 || typeof value.authToken !== 'string'
30
+ || value.authToken.length < 32 || value.authToken.length > 1024) {
31
+ throw new Error('Handmux Agent Runtime credential is unavailable or invalid');
32
+ }
33
+ return value.authToken;
34
+ }
35
+ function samePayload(first, second) {
36
+ try {
37
+ return JSON.stringify(first) === JSON.stringify(second);
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ function durableKey(channel, eventId) {
44
+ return `${channel}\0${eventId}`;
45
+ }
46
+ // Shared only by Handmux-owned built-in Connectors. Provider callbacks enqueue local state synchronously;
47
+ // socket retries and ordered flushing stay here so every Connector gets the same offline semantics.
48
+ export class LocalConnectorBridgeClient {
49
+ #adapterId;
50
+ #socketPath;
51
+ #credentialFile;
52
+ #candidate;
53
+ #connect;
54
+ #retryDelayMs;
55
+ #maxRetryDelayMs;
56
+ #maxEphemeralPerChannel;
57
+ #logger;
58
+ #store;
59
+ #snapshots = new Map();
60
+ #dirtySnapshots = new Set();
61
+ #unpersistedSnapshots = new Set();
62
+ #ephemeral = new Map();
63
+ #dropped = new Set();
64
+ #handlers = new Map();
65
+ #durableWaiters = new Map();
66
+ #state;
67
+ #generation;
68
+ #connection;
69
+ #retryTimer;
70
+ #retryAttempt = 0;
71
+ #flushTail = Promise.resolve();
72
+ #started = false;
73
+ #closed = false;
74
+ constructor({ adapterId, socketPath, credentialFile, stateFile, candidate, generation, connect = connectBridgeTransport, retryDelayMs = 250, maxRetryDelayMs = 5_000, maxEphemeralPerChannel = 256, logger = () => { }, }) {
75
+ if (!NAME_RE.test(adapterId) || !path.isAbsolute(socketPath)
76
+ || !path.isAbsolute(credentialFile) || !path.isAbsolute(stateFile)
77
+ || !candidate || !Number.isSafeInteger(retryDelayMs) || retryDelayMs <= 0
78
+ || !Number.isSafeInteger(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs
79
+ || !Number.isSafeInteger(maxEphemeralPerChannel) || maxEphemeralPerChannel <= 0) {
80
+ throw new TypeError('Connector Bridge client requires an adapter, private paths, and bounded retries');
81
+ }
82
+ this.#adapterId = adapterId;
83
+ this.#socketPath = socketPath;
84
+ this.#credentialFile = credentialFile;
85
+ this.#candidate = structuredClone(candidate);
86
+ this.#generation = generation;
87
+ this.#connect = connect;
88
+ this.#retryDelayMs = retryDelayMs;
89
+ this.#maxRetryDelayMs = maxRetryDelayMs;
90
+ this.#maxEphemeralPerChannel = maxEphemeralPerChannel;
91
+ this.#logger = logger;
92
+ this.#store = new PrivateStateStore(stateFile);
93
+ this.#state = parseState(this.#store.read());
94
+ for (const [channel, value] of Object.entries(this.#state.snapshots)) {
95
+ if (!NAME_RE.test(channel))
96
+ continue;
97
+ this.#snapshots.set(channel, { revision: 1, value });
98
+ this.#dirtySnapshots.add(channel);
99
+ }
100
+ }
101
+ start() {
102
+ if (this.#closed)
103
+ throw new Error('Connector Bridge client is closed');
104
+ if (this.#started)
105
+ return;
106
+ this.#started = true;
107
+ this.#scheduleConnect(0);
108
+ }
109
+ setSnapshot(channel, value) {
110
+ this.#assertName(channel);
111
+ if (this.#closed)
112
+ return false;
113
+ const previous = this.#snapshots.get(channel);
114
+ if (previous && samePayload(previous.value, value)) {
115
+ const persisted = !this.#unpersistedSnapshots.has(channel) || this.#persist();
116
+ if (persisted)
117
+ this.#unpersistedSnapshots.delete(channel);
118
+ this.#kickFlush();
119
+ return persisted;
120
+ }
121
+ this.#snapshots.set(channel, { revision: (previous?.revision ?? 0) + 1, value: structuredClone(value) });
122
+ this.#dirtySnapshots.add(channel);
123
+ this.#state.snapshots[channel] = structuredClone(value);
124
+ const persisted = this.#persist();
125
+ if (persisted)
126
+ this.#unpersistedSnapshots.delete(channel);
127
+ else
128
+ this.#unpersistedSnapshots.add(channel);
129
+ this.#kickFlush();
130
+ return persisted;
131
+ }
132
+ publishDurable(channel, eventId, payload) {
133
+ this.#assertName(channel);
134
+ if (!EVENT_ID_RE.test(eventId))
135
+ throw new Error('Durable event requires a stable bounded eventId');
136
+ if (this.#closed)
137
+ return false;
138
+ const existing = this.#state.durable.find((item) => (item.channel === channel && item.eventId === eventId));
139
+ if (existing) {
140
+ if (!samePayload(existing.payload, payload)) {
141
+ throw new Error('Durable eventId was reused with a different payload');
142
+ }
143
+ return true;
144
+ }
145
+ const item = { channel, eventId, payload: structuredClone(payload) };
146
+ this.#state.durable.push(item);
147
+ if (!this.#persist()) {
148
+ const index = this.#state.durable.indexOf(item);
149
+ if (index !== -1)
150
+ this.#state.durable.splice(index, 1);
151
+ return false;
152
+ }
153
+ this.#kickFlush();
154
+ return true;
155
+ }
156
+ waitForDurableAck(channel, eventId) {
157
+ this.#assertName(channel);
158
+ if (!EVENT_ID_RE.test(eventId))
159
+ return Promise.reject(new Error('Invalid durable eventId'));
160
+ if (!this.#state.durable.some((item) => item.channel === channel && item.eventId === eventId)) {
161
+ return Promise.resolve();
162
+ }
163
+ if (this.#closed)
164
+ return Promise.reject(new Error('Connector Bridge client is closed'));
165
+ const key = durableKey(channel, eventId);
166
+ return new Promise((resolve, reject) => {
167
+ const waiters = this.#durableWaiters.get(key) ?? new Set();
168
+ if (!this.#durableWaiters.has(key))
169
+ this.#durableWaiters.set(key, waiters);
170
+ waiters.add({ resolve, reject });
171
+ });
172
+ }
173
+ waitForDurableDrain() {
174
+ if (this.#closed)
175
+ return Promise.reject(new Error('Connector Bridge client is closed'));
176
+ const pending = this.#state.durable.map(({ channel, eventId }) => (this.waitForDurableAck(channel, eventId)));
177
+ return Promise.all(pending).then(() => undefined);
178
+ }
179
+ closeAndRemoveIfDrained() {
180
+ if (this.#state.durable.length !== 0)
181
+ return false;
182
+ this.close();
183
+ try {
184
+ this.#store.remove();
185
+ return true;
186
+ }
187
+ catch (error) {
188
+ this.#logger('Drained Connector Bridge state could not be removed', error);
189
+ return false;
190
+ }
191
+ }
192
+ publishEphemeral(channel, payload) {
193
+ this.#assertName(channel);
194
+ if (this.#closed)
195
+ return;
196
+ const queue = this.#ephemeral.get(channel) ?? [];
197
+ if (!this.#ephemeral.has(channel))
198
+ this.#ephemeral.set(channel, queue);
199
+ if (queue.length >= this.#maxEphemeralPerChannel) {
200
+ queue.shift();
201
+ this.#dropped.add(channel);
202
+ }
203
+ queue.push({ payload: structuredClone(payload) });
204
+ this.#kickFlush();
205
+ }
206
+ handle(channel, method, handler) {
207
+ this.#assertName(channel);
208
+ this.#assertName(method);
209
+ if (typeof handler !== 'function' || this.#closed)
210
+ throw new Error('Invalid Connector Bridge handler');
211
+ const key = `${channel}\0${method}`;
212
+ if (this.#handlers.has(key))
213
+ throw new Error('Connector Bridge handler is already registered');
214
+ const wrapped = async (payload, context) => handler(payload, context);
215
+ const entry = { channel, method, handler: wrapped };
216
+ this.#handlers.set(key, entry);
217
+ if (this.#connection) {
218
+ void this.#connection.channel(channel).handle(method, wrapped).catch((error) => {
219
+ this.#logger(`Failed to register Bridge handler ${channel}.${method}`, error);
220
+ this.#connection?.close();
221
+ });
222
+ }
223
+ return () => { if (this.#handlers.get(key) === entry)
224
+ this.#handlers.delete(key); };
225
+ }
226
+ close() {
227
+ if (this.#closed)
228
+ return;
229
+ this.#closed = true;
230
+ if (this.#retryTimer)
231
+ clearTimeout(this.#retryTimer);
232
+ this.#retryTimer = undefined;
233
+ this.#connection?.close();
234
+ this.#connection = undefined;
235
+ const error = new Error('Connector Bridge client is closed');
236
+ for (const waiters of this.#durableWaiters.values()) {
237
+ for (const waiter of waiters)
238
+ waiter.reject(error);
239
+ }
240
+ this.#durableWaiters.clear();
241
+ }
242
+ #assertName(value) {
243
+ if (!NAME_RE.test(value))
244
+ throw new Error('Invalid Bridge channel or method name');
245
+ }
246
+ #persist() {
247
+ try {
248
+ this.#store.write(this.#state);
249
+ return true;
250
+ }
251
+ catch (error) {
252
+ this.#logger('Connector Bridge local state could not be persisted', error);
253
+ return false;
254
+ }
255
+ }
256
+ #resolveDurable(channel, eventId) {
257
+ const key = durableKey(channel, eventId);
258
+ const waiters = this.#durableWaiters.get(key);
259
+ if (!waiters)
260
+ return;
261
+ this.#durableWaiters.delete(key);
262
+ for (const waiter of waiters)
263
+ waiter.resolve();
264
+ }
265
+ #invalidateEphemeral() {
266
+ for (const [channel, queue] of this.#ephemeral) {
267
+ if (!queue.length)
268
+ continue;
269
+ queue.splice(0);
270
+ this.#dropped.add(channel);
271
+ }
272
+ }
273
+ #scheduleConnect(delay) {
274
+ if (!this.#started || this.#closed || this.#connection || this.#retryTimer)
275
+ return;
276
+ this.#retryTimer = setTimeout(() => {
277
+ this.#retryTimer = undefined;
278
+ void this.#open();
279
+ }, delay);
280
+ this.#retryTimer.unref?.();
281
+ }
282
+ async #open() {
283
+ if (this.#closed || this.#connection)
284
+ return;
285
+ try {
286
+ const connection = await this.#connect({
287
+ socketPath: this.#socketPath,
288
+ authToken: credential(this.#credentialFile),
289
+ adapterId: this.#adapterId,
290
+ candidate: this.#candidate,
291
+ ...(this.#generation === undefined ? {} : { generation: this.#generation }),
292
+ });
293
+ if (this.#closed) {
294
+ connection.close();
295
+ return;
296
+ }
297
+ this.#connection = connection;
298
+ this.#generation = undefined;
299
+ this.#retryAttempt = 0;
300
+ for (const channel of this.#snapshots.keys())
301
+ this.#dirtySnapshots.add(channel);
302
+ for (const entry of this.#handlers.values()) {
303
+ await connection.channel(entry.channel).handle(entry.method, entry.handler);
304
+ }
305
+ connection.signal.addEventListener('abort', () => {
306
+ if (this.#connection === connection)
307
+ this.#connection = undefined;
308
+ this.#scheduleRetry();
309
+ }, { once: true });
310
+ this.#kickFlush();
311
+ }
312
+ catch (error) {
313
+ this.#connection?.close();
314
+ this.#connection = undefined;
315
+ this.#invalidateEphemeral();
316
+ this.#logger('Connector Bridge connection failed; retrying in background', error);
317
+ this.#scheduleRetry();
318
+ }
319
+ }
320
+ #scheduleRetry() {
321
+ if (this.#closed)
322
+ return;
323
+ const delay = Math.min(this.#maxRetryDelayMs, this.#retryDelayMs * (2 ** this.#retryAttempt));
324
+ this.#retryAttempt = Math.min(this.#retryAttempt + 1, 20);
325
+ this.#scheduleConnect(delay);
326
+ }
327
+ #kickFlush() {
328
+ if (!this.#connection || this.#closed)
329
+ return;
330
+ const pending = this.#flushTail.then(() => this.#flush());
331
+ this.#flushTail = pending.catch((error) => {
332
+ this.#invalidateEphemeral();
333
+ this.#logger('Connector Bridge flush failed; state remains queued', error);
334
+ this.#connection?.close();
335
+ });
336
+ }
337
+ async #flush() {
338
+ const connection = this.#connection;
339
+ if (!connection || connection.signal.aborted || this.#closed)
340
+ return;
341
+ while (this.#state.durable.length) {
342
+ const item = this.#state.durable[0];
343
+ const receipt = await connection.channel(item.channel).publish({
344
+ eventId: item.eventId, payload: item.payload,
345
+ }, { delivery: 'durable' });
346
+ if (!receipt.accepted && receipt.reason !== 'duplicate') {
347
+ throw new Error(`Durable Bridge write rejected: ${receipt.reason ?? 'unknown'}`);
348
+ }
349
+ this.#state.durable.shift();
350
+ if (!this.#persist()) {
351
+ this.#state.durable.unshift(item);
352
+ throw new Error('Durable Bridge acknowledgement could not be persisted locally');
353
+ }
354
+ this.#resolveDurable(item.channel, item.eventId);
355
+ }
356
+ for (const channel of [...this.#dirtySnapshots]) {
357
+ const entry = this.#snapshots.get(channel);
358
+ if (!entry) {
359
+ this.#dirtySnapshots.delete(channel);
360
+ continue;
361
+ }
362
+ const revision = entry.revision;
363
+ const receipt = await connection.channel(channel).setSnapshot(entry.value);
364
+ if (!receipt.accepted)
365
+ throw new Error(`Bridge snapshot rejected: ${receipt.reason ?? 'unknown'}`);
366
+ if (this.#snapshots.get(channel)?.revision === revision)
367
+ this.#dirtySnapshots.delete(channel);
368
+ }
369
+ for (const channel of [...this.#dropped]) {
370
+ const receipt = await connection.channel(channel).publish({ payload: { type: 'stream.gap' } });
371
+ if (!receipt.accepted)
372
+ throw new Error(`Bridge gap rejected: ${receipt.reason ?? 'unknown'}`);
373
+ this.#dropped.delete(channel);
374
+ }
375
+ for (const [channel, queue] of this.#ephemeral) {
376
+ while (queue.length) {
377
+ const event = queue[0];
378
+ const receipt = await connection.channel(channel).publish(event);
379
+ if (!receipt.accepted)
380
+ throw new Error(`Bridge event rejected: ${receipt.reason ?? 'unknown'}`);
381
+ queue.shift();
382
+ }
383
+ }
384
+ }
385
+ }