agentmail 0.5.34 → 0.5.35

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.
@@ -43,8 +43,8 @@ function normalizeClientOptions(options) {
43
43
  const headers = (0, headers_js_1.mergeHeaders)({
44
44
  "X-Fern-Language": "JavaScript",
45
45
  "X-Fern-SDK-Name": "agentmail",
46
- "X-Fern-SDK-Version": "0.5.34",
47
- "User-Agent": "agentmail/0.5.34",
46
+ "X-Fern-SDK-Version": "0.5.35",
47
+ "User-Agent": "agentmail/0.5.35",
48
48
  "X-Fern-Runtime": core.RUNTIME.type,
49
49
  "X-Fern-Runtime-Version": core.RUNTIME.version,
50
50
  }, options === null || options === void 0 ? void 0 : options.headers);
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "0.5.34";
1
+ export declare const SDK_VERSION = "0.5.35";
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
- exports.SDK_VERSION = "0.5.34";
4
+ exports.SDK_VERSION = "0.5.35";
@@ -1,10 +1,24 @@
1
1
  import { WebsocketsClient as FernWebsocketsClient } from "../api/resources/websockets/client/Client.js";
2
2
  import type { WebsocketsSocket } from "../api/resources/websockets/client/Socket.js";
3
+ import { type ReconnectPolicy } from "./reconnect.js";
3
4
  export type GetPaymentCredentials = (wsUrl: string) => Promise<Record<string, string>>;
4
5
  export declare class WebsocketsClient extends FernWebsocketsClient {
5
6
  private readonly _getPaymentCredentials;
6
- constructor(options: FernWebsocketsClient.Options, getPaymentCredentials?: GetPaymentCredentials);
7
+ private readonly _reconnectPolicy;
8
+ constructor(options: FernWebsocketsClient.Options, getPaymentCredentials?: GetPaymentCredentials, reconnectPolicy?: ReconnectPolicy);
7
9
  connect(args?: FernWebsocketsClient.ConnectArgs & {
10
+ /** Resolve only once the connection is open. Defaults to true. */
8
11
  waitForOpen?: boolean;
12
+ /**
13
+ * Keep the socket connected and subscribed. Whenever the connection drops, reconnect with
14
+ * exponential backoff (full jitter, 1 s doubling up to 60 s) and re-send every subscription
15
+ * sent through `sendSubscribe` on the new connection; `reconnectAttempts` then only bounds
16
+ * the generated socket's own quick retries, not the total. Stops on `close()`, on
17
+ * `abortSignal`, or when the server refuses the handshake with 401, 402 or 403, which is
18
+ * reported to the `error` handler. x402/MPP sockets reuse the payment credential minted by
19
+ * this call on every reconnect. Defaults to true. `false` (or `reconnectAttempts: 0`) keeps
20
+ * only the generated socket's own limited retries and does not re-subscribe.
21
+ */
22
+ autoReconnect?: boolean;
9
23
  }): Promise<WebsocketsSocket>;
10
24
  }
@@ -57,31 +57,49 @@ exports.WebsocketsClient = void 0;
57
57
  const Client_js_1 = require("../api/resources/websockets/client/Client.js");
58
58
  const core = __importStar(require("../core/index.js"));
59
59
  const environments = __importStar(require("../environments.js"));
60
+ const version_js_1 = require("../version.js");
61
+ const reconnect_js_1 = require("./reconnect.js");
62
+ // The generated client's default. Always passed explicitly: auto-reconnect has to know the generated
63
+ // socket's exact retry budget to take over before it runs out.
64
+ const DEFAULT_RECONNECT_ATTEMPTS = 30;
65
+ // Lets the server tell SDK releases apart on the handshake (Node only; browsers cannot set handshake
66
+ // headers). Caller-supplied headers win.
67
+ const SDK_HEADERS = { "X-Fern-SDK-Name": "agentmail", "X-Fern-SDK-Version": version_js_1.SDK_VERSION };
60
68
  class WebsocketsClient extends Client_js_1.WebsocketsClient {
61
- constructor(options, getPaymentCredentials) {
69
+ constructor(options, getPaymentCredentials, reconnectPolicy = reconnect_js_1.DEFAULT_RECONNECT_POLICY) {
62
70
  super(options);
63
71
  this._getPaymentCredentials = getPaymentCredentials;
72
+ this._reconnectPolicy = reconnectPolicy;
64
73
  }
65
74
  connect() {
66
75
  const _super = Object.create(null, {
67
76
  connect: { get: () => super.connect }
68
77
  });
69
78
  return __awaiter(this, arguments, void 0, function* (args = {}) {
70
- var _a, _b, _c;
71
- const { waitForOpen = true } = args, rest = __rest(args, ["waitForOpen"]);
72
- let connectArgs = rest;
79
+ var _a, _b, _c, _d;
80
+ const { waitForOpen = true, autoReconnect = true } = args, rest = __rest(args, ["waitForOpen", "autoReconnect"]);
81
+ const reconnectAttempts = (_a = rest.reconnectAttempts) !== null && _a !== void 0 ? _a : DEFAULT_RECONNECT_ATTEMPTS;
82
+ let connectArgs = Object.assign(Object.assign({}, rest), { reconnectAttempts, headers: Object.assign(Object.assign({}, SDK_HEADERS), rest.headers) });
73
83
  if (this._getPaymentCredentials) {
74
- const wsUrl = core.url.join((_a = (yield core.Supplier.get(this._options.baseUrl))) !== null && _a !== void 0 ? _a : ((_b = (yield core.Supplier.get(this._options.environment))) !== null && _b !== void 0 ? _b : environments.AgentMailEnvironment.Prod).websockets, "/v0");
84
+ const wsUrl = core.url.join((_b = (yield core.Supplier.get(this._options.baseUrl))) !== null && _b !== void 0 ? _b : ((_c = (yield core.Supplier.get(this._options.environment))) !== null && _c !== void 0 ? _c : environments.AgentMailEnvironment.Prod)
85
+ .websockets, "/v0");
75
86
  const credentials = yield this._getPaymentCredentials(wsUrl);
76
- connectArgs = Object.assign(Object.assign({}, rest), { queryParams: Object.assign(Object.assign({}, credentials), rest.queryParams) });
87
+ connectArgs = Object.assign(Object.assign({}, connectArgs), { queryParams: Object.assign(Object.assign({}, credentials), rest.queryParams) });
77
88
  }
78
89
  else if (!rest.apiKey) {
79
- const apiKey = (_c = (yield core.Supplier.get(this._options.apiKey))) !== null && _c !== void 0 ? _c : process.env.AGENTMAIL_API_KEY;
80
- connectArgs = Object.assign(Object.assign({}, rest), { apiKey });
90
+ const apiKey = (_d = (yield core.Supplier.get(this._options.apiKey))) !== null && _d !== void 0 ? _d : process.env.AGENTMAIL_API_KEY;
91
+ connectArgs = Object.assign(Object.assign({}, connectArgs), { apiKey });
81
92
  }
82
93
  const socket = yield _super.connect.call(this, connectArgs);
83
94
  if (waitForOpen)
84
95
  yield socket.waitForOpen();
96
+ if (autoReconnect && reconnectAttempts > 0) {
97
+ (0, reconnect_js_1.enableAutoReconnect)(socket, {
98
+ maxRetries: reconnectAttempts,
99
+ abortSignal: rest.abortSignal,
100
+ policy: this._reconnectPolicy,
101
+ });
102
+ }
85
103
  return socket;
86
104
  });
87
105
  }
@@ -0,0 +1,22 @@
1
+ import type { WebsocketsSocket } from "../api/resources/websockets/client/Socket.js";
2
+ export interface ReconnectPolicy {
3
+ baseDelayMs: number;
4
+ maxDelayMs: number;
5
+ /** How long a connection must stay open before the backoff starts over. */
6
+ stableAfterMs: number;
7
+ }
8
+ export declare const DEFAULT_RECONNECT_POLICY: ReconnectPolicy;
9
+ /** Exponential backoff with full jitter: uniform in [0, min(maxDelayMs, baseDelayMs * 2^attempt)). */
10
+ export declare function backoffDelay(attempt: number, policy: ReconnectPolicy, random?: () => number): number;
11
+ export interface AutoReconnectOptions {
12
+ /** The generated socket's own retry budget (`reconnectAttempts`). */
13
+ maxRetries: number;
14
+ abortSignal?: AbortSignal;
15
+ policy: ReconnectPolicy;
16
+ }
17
+ /**
18
+ * Keeps `socket` connected and subscribed: re-sends every subscription on each reconnect, takes over
19
+ * reconnecting whenever the generated socket gives up, and stops on close(), abort, or a terminal
20
+ * handshake refusal.
21
+ */
22
+ export declare function enableAutoReconnect(socket: WebsocketsSocket, options: AutoReconnectOptions): void;
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __rest = (this && this.__rest) || function (s, e) {
36
+ var t = {};
37
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
38
+ t[p] = s[p];
39
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
40
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
41
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
42
+ t[p[i]] = s[p[i]];
43
+ }
44
+ return t;
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.DEFAULT_RECONNECT_POLICY = void 0;
48
+ exports.backoffDelay = backoffDelay;
49
+ exports.enableAutoReconnect = enableAutoReconnect;
50
+ const index_js_1 = require("../core/index.js");
51
+ const Events = __importStar(require("../core/websocket/events.js"));
52
+ exports.DEFAULT_RECONNECT_POLICY = { baseDelayMs: 1000, maxDelayMs: 60000, stableAfterMs: 5000 };
53
+ /** Exponential backoff with full jitter: uniform in [0, min(maxDelayMs, baseDelayMs * 2^attempt)). */
54
+ function backoffDelay(attempt, policy, random = Math.random) {
55
+ return random() * Math.min(policy.maxDelayMs, policy.baseDelayMs * Math.pow(2, attempt));
56
+ }
57
+ const { OPEN, CLOSING, CLOSED } = index_js_1.ReconnectingWebSocket.ReadyState;
58
+ // ws reports a refused handshake as "Unexpected server response: <status>". Browsers expose no status,
59
+ // so there every refusal looks transient and is retried on the capped backoff.
60
+ const HANDSHAKE_STATUS = /^Unexpected server response: (\d{3})\b/;
61
+ // Refusals that retrying with the same credentials cannot fix. 402 comes from the x402/MPP proxies,
62
+ // whose payment credential is minted once per connect() call and reused on reconnects. The server
63
+ // also answers 403 when the organization is at its connection cap, which a retry could outlast.
64
+ const TERMINAL_FAILURES = new Map([
65
+ ["401", "WebSocket authentication failed (401): the API key is missing, invalid, or was deleted."],
66
+ [
67
+ "402",
68
+ "WebSocket payment required (402): the payment credential minted by connect() is no longer accepted; open a new socket with client.websockets.connect() to pay again.",
69
+ ],
70
+ [
71
+ "403",
72
+ "WebSocket authentication failed (403): the API key is invalid or was deleted, or the organization is at its WebSocket connection limit.",
73
+ ],
74
+ ]);
75
+ /**
76
+ * Keeps `socket` connected and subscribed: re-sends every subscription on each reconnect, takes over
77
+ * reconnecting whenever the generated socket gives up, and stops on close(), abort, or a terminal
78
+ * handshake refusal.
79
+ */
80
+ function enableAutoReconnect(socket, options) {
81
+ const { maxRetries, abortSignal, policy } = options;
82
+ const rws = socket.socket;
83
+ const sendSubscribe = socket.sendSubscribe.bind(socket);
84
+ const closeSocket = rws.close.bind(rws);
85
+ const reconnectSocket = rws.reconnect.bind(rws);
86
+ // Keyed by canonical JSON, in the order last sent. The server keeps one subscription per target and
87
+ // the last filter sent for it, so a new message takes its targets out of every earlier one: a
88
+ // replay can never put back a filter the caller has since changed.
89
+ const subscriptions = new Map();
90
+ // Keys already sent on the current connection, e.g. by the user's own open handler.
91
+ const sentOnConnection = new Set();
92
+ let opened = rws.readyState === OPEN;
93
+ let openedAt = opened ? Date.now() : undefined;
94
+ let stopped = false;
95
+ let attempt = 0;
96
+ let timer;
97
+ let emitting = false;
98
+ const isStopped = () => stopped || (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) === true;
99
+ const cancelTimer = () => {
100
+ clearTimeout(timer);
101
+ timer = undefined;
102
+ };
103
+ const stop = () => {
104
+ stopped = true;
105
+ cancelTimer();
106
+ };
107
+ const emitError = (message) => {
108
+ var _a;
109
+ const event = new Events.ErrorEvent(new Error(message), rws);
110
+ emitting = true;
111
+ try {
112
+ (_a = rws.onerror) === null || _a === void 0 ? void 0 : _a.call(rws, event);
113
+ rws.dispatchEvent(event);
114
+ }
115
+ finally {
116
+ emitting = false;
117
+ }
118
+ };
119
+ const scheduleReconnect = () => {
120
+ cancelTimer();
121
+ timer = setTimeout(() => {
122
+ timer = undefined;
123
+ if (isStopped())
124
+ return;
125
+ // reconnect() on a socket that is not CLOSED closes it with 1000 and then never connects.
126
+ if (rws.readyState === CLOSED)
127
+ reconnectSocket();
128
+ else if (rws.readyState === CLOSING)
129
+ scheduleReconnect();
130
+ }, backoffDelay(attempt++, policy));
131
+ };
132
+ rws.addEventListener("open", () => {
133
+ cancelTimer();
134
+ openedAt = Date.now();
135
+ if (!opened) {
136
+ opened = true;
137
+ return;
138
+ }
139
+ for (const [key, message] of subscriptions) {
140
+ if (isStopped() || rws.readyState !== OPEN)
141
+ return;
142
+ if (sentOnConnection.has(key))
143
+ continue;
144
+ try {
145
+ sendSubscribe(message);
146
+ sentOnConnection.add(key);
147
+ }
148
+ catch (error) {
149
+ emitError(`Failed to re-subscribe after reconnecting: ${error instanceof Error ? error.message : String(error)}`);
150
+ }
151
+ }
152
+ });
153
+ rws.addEventListener("close", (event) => {
154
+ sentOnConnection.clear();
155
+ if (openedAt !== undefined && Date.now() - openedAt >= policy.stableAfterMs)
156
+ attempt = 0;
157
+ openedAt = undefined;
158
+ if (isStopped())
159
+ return;
160
+ if (event.code !== 1000) {
161
+ // The generated socket retries these closes itself, up to maxRetries times.
162
+ if (rws.retryCount < maxRetries)
163
+ return;
164
+ // It has just scheduled its last retry. If that one fails too, it keeps its connect lock
165
+ // and ignores reconnect() for good, so cancel it and take over.
166
+ closeSocket();
167
+ }
168
+ scheduleReconnect();
169
+ });
170
+ rws.addEventListener("error", (event) => {
171
+ var _a;
172
+ if (emitting || isStopped())
173
+ return;
174
+ const status = (_a = HANDSHAKE_STATUS.exec(event.message)) === null || _a === void 0 ? void 0 : _a[1];
175
+ const failure = status === undefined ? undefined : TERMINAL_FAILURES.get(status);
176
+ if (failure === undefined)
177
+ return;
178
+ stop();
179
+ emitError(`${failure} Not reconnecting.`);
180
+ });
181
+ abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener("abort", stop, { once: true });
182
+ // Patched on the generated socket so WebsocketsSocket.close()/connect() and direct
183
+ // socket.socket.close()/reconnect() calls all count as the user's intent.
184
+ rws.close = (code, reason) => {
185
+ stop();
186
+ subscriptions.clear();
187
+ closeSocket(code, reason);
188
+ };
189
+ rws.reconnect = (code, reason) => {
190
+ stopped = false;
191
+ attempt = 0;
192
+ cancelTimer();
193
+ reconnectSocket(code, reason);
194
+ };
195
+ socket.sendSubscribe = (message) => {
196
+ sendSubscribe(message);
197
+ const recorded = JSON.parse(JSON.stringify(message));
198
+ const covered = new Set(targetsOf(recorded));
199
+ const kept = [...subscriptions].flatMap(([key, cached]) => {
200
+ const remaining = withoutTargets(cached, covered);
201
+ if (remaining === undefined)
202
+ return [];
203
+ return [[remaining === cached ? key : subscriptionKey(remaining), remaining]];
204
+ });
205
+ const key = subscriptionKey(recorded);
206
+ subscriptions.clear();
207
+ for (const [keptKey, keptMessage] of kept)
208
+ if (keptKey !== key)
209
+ subscriptions.set(keptKey, keptMessage);
210
+ subscriptions.set(key, recorded);
211
+ sentOnConnection.add(key);
212
+ };
213
+ }
214
+ // What a subscribe message sets on the server: one subscription per inbox and per pod it names, or the
215
+ // credential's own scope when it names neither. That default resolves server-side to an inbox, pod or
216
+ // organization the SDK cannot see, so it is tracked as its own target.
217
+ function targetsOf(message) {
218
+ var _a, _b;
219
+ if (message.inboxIds === undefined && message.podIds === undefined)
220
+ return ["default"];
221
+ return [...((_a = message.inboxIds) !== null && _a !== void 0 ? _a : []).map((id) => `inbox:${id}`), ...((_b = message.podIds) !== null && _b !== void 0 ? _b : []).map((id) => `pod:${id}`)];
222
+ }
223
+ // `message` without the covered targets; undefined once none of its targets is left.
224
+ function withoutTargets(message, covered) {
225
+ const targets = targetsOf(message);
226
+ if (!targets.some((target) => covered.has(target)))
227
+ return message;
228
+ if (targets.every((target) => covered.has(target)))
229
+ return undefined;
230
+ const { inboxIds, podIds } = message, rest = __rest(message, ["inboxIds", "podIds"]);
231
+ const keptInboxIds = inboxIds === null || inboxIds === void 0 ? void 0 : inboxIds.filter((id) => !covered.has(`inbox:${id}`));
232
+ const keptPodIds = podIds === null || podIds === void 0 ? void 0 : podIds.filter((id) => !covered.has(`pod:${id}`));
233
+ return Object.assign(Object.assign(Object.assign({}, rest), ((keptInboxIds === null || keptInboxIds === void 0 ? void 0 : keptInboxIds.length) ? { inboxIds: keptInboxIds } : {})), ((keptPodIds === null || keptPodIds === void 0 ? void 0 : keptPodIds.length) ? { podIds: keptPodIds } : {}));
234
+ }
235
+ // Canonical JSON (sorted object keys), so the same subscription written in a different key order dedupes.
236
+ function subscriptionKey(message) {
237
+ return JSON.stringify(message, (_key, value) => {
238
+ if (value === null || typeof value !== "object" || Array.isArray(value))
239
+ return value;
240
+ const sorted = {};
241
+ for (const key of Object.keys(value).sort()) {
242
+ sorted[key] = value[key];
243
+ }
244
+ return sorted;
245
+ });
246
+ }
@@ -6,8 +6,8 @@ export function normalizeClientOptions(options) {
6
6
  const headers = mergeHeaders({
7
7
  "X-Fern-Language": "JavaScript",
8
8
  "X-Fern-SDK-Name": "agentmail",
9
- "X-Fern-SDK-Version": "0.5.34",
10
- "User-Agent": "agentmail/0.5.34",
9
+ "X-Fern-SDK-Version": "0.5.35",
10
+ "User-Agent": "agentmail/0.5.35",
11
11
  "X-Fern-Runtime": core.RUNTIME.type,
12
12
  "X-Fern-Runtime-Version": core.RUNTIME.version,
13
13
  }, options === null || options === void 0 ? void 0 : options.headers);
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "0.5.34";
1
+ export declare const SDK_VERSION = "0.5.35";
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.5.34";
1
+ export const SDK_VERSION = "0.5.35";
@@ -1,10 +1,24 @@
1
1
  import { WebsocketsClient as FernWebsocketsClient } from "../api/resources/websockets/client/Client.mjs";
2
2
  import type { WebsocketsSocket } from "../api/resources/websockets/client/Socket.mjs";
3
+ import { type ReconnectPolicy } from "./reconnect.mjs";
3
4
  export type GetPaymentCredentials = (wsUrl: string) => Promise<Record<string, string>>;
4
5
  export declare class WebsocketsClient extends FernWebsocketsClient {
5
6
  private readonly _getPaymentCredentials;
6
- constructor(options: FernWebsocketsClient.Options, getPaymentCredentials?: GetPaymentCredentials);
7
+ private readonly _reconnectPolicy;
8
+ constructor(options: FernWebsocketsClient.Options, getPaymentCredentials?: GetPaymentCredentials, reconnectPolicy?: ReconnectPolicy);
7
9
  connect(args?: FernWebsocketsClient.ConnectArgs & {
10
+ /** Resolve only once the connection is open. Defaults to true. */
8
11
  waitForOpen?: boolean;
12
+ /**
13
+ * Keep the socket connected and subscribed. Whenever the connection drops, reconnect with
14
+ * exponential backoff (full jitter, 1 s doubling up to 60 s) and re-send every subscription
15
+ * sent through `sendSubscribe` on the new connection; `reconnectAttempts` then only bounds
16
+ * the generated socket's own quick retries, not the total. Stops on `close()`, on
17
+ * `abortSignal`, or when the server refuses the handshake with 401, 402 or 403, which is
18
+ * reported to the `error` handler. x402/MPP sockets reuse the payment credential minted by
19
+ * this call on every reconnect. Defaults to true. `false` (or `reconnectAttempts: 0`) keeps
20
+ * only the generated socket's own limited retries and does not re-subscribe.
21
+ */
22
+ autoReconnect?: boolean;
9
23
  }): Promise<WebsocketsSocket>;
10
24
  }
@@ -21,31 +21,49 @@ var __rest = (this && this.__rest) || function (s, e) {
21
21
  import { WebsocketsClient as FernWebsocketsClient } from "../api/resources/websockets/client/Client.mjs";
22
22
  import * as core from "../core/index.mjs";
23
23
  import * as environments from "../environments.mjs";
24
+ import { SDK_VERSION } from "../version.mjs";
25
+ import { DEFAULT_RECONNECT_POLICY, enableAutoReconnect } from "./reconnect.mjs";
26
+ // The generated client's default. Always passed explicitly: auto-reconnect has to know the generated
27
+ // socket's exact retry budget to take over before it runs out.
28
+ const DEFAULT_RECONNECT_ATTEMPTS = 30;
29
+ // Lets the server tell SDK releases apart on the handshake (Node only; browsers cannot set handshake
30
+ // headers). Caller-supplied headers win.
31
+ const SDK_HEADERS = { "X-Fern-SDK-Name": "agentmail", "X-Fern-SDK-Version": SDK_VERSION };
24
32
  export class WebsocketsClient extends FernWebsocketsClient {
25
- constructor(options, getPaymentCredentials) {
33
+ constructor(options, getPaymentCredentials, reconnectPolicy = DEFAULT_RECONNECT_POLICY) {
26
34
  super(options);
27
35
  this._getPaymentCredentials = getPaymentCredentials;
36
+ this._reconnectPolicy = reconnectPolicy;
28
37
  }
29
38
  connect() {
30
39
  const _super = Object.create(null, {
31
40
  connect: { get: () => super.connect }
32
41
  });
33
42
  return __awaiter(this, arguments, void 0, function* (args = {}) {
34
- var _a, _b, _c;
35
- const { waitForOpen = true } = args, rest = __rest(args, ["waitForOpen"]);
36
- let connectArgs = rest;
43
+ var _a, _b, _c, _d;
44
+ const { waitForOpen = true, autoReconnect = true } = args, rest = __rest(args, ["waitForOpen", "autoReconnect"]);
45
+ const reconnectAttempts = (_a = rest.reconnectAttempts) !== null && _a !== void 0 ? _a : DEFAULT_RECONNECT_ATTEMPTS;
46
+ let connectArgs = Object.assign(Object.assign({}, rest), { reconnectAttempts, headers: Object.assign(Object.assign({}, SDK_HEADERS), rest.headers) });
37
47
  if (this._getPaymentCredentials) {
38
- const wsUrl = core.url.join((_a = (yield core.Supplier.get(this._options.baseUrl))) !== null && _a !== void 0 ? _a : ((_b = (yield core.Supplier.get(this._options.environment))) !== null && _b !== void 0 ? _b : environments.AgentMailEnvironment.Prod).websockets, "/v0");
48
+ const wsUrl = core.url.join((_b = (yield core.Supplier.get(this._options.baseUrl))) !== null && _b !== void 0 ? _b : ((_c = (yield core.Supplier.get(this._options.environment))) !== null && _c !== void 0 ? _c : environments.AgentMailEnvironment.Prod)
49
+ .websockets, "/v0");
39
50
  const credentials = yield this._getPaymentCredentials(wsUrl);
40
- connectArgs = Object.assign(Object.assign({}, rest), { queryParams: Object.assign(Object.assign({}, credentials), rest.queryParams) });
51
+ connectArgs = Object.assign(Object.assign({}, connectArgs), { queryParams: Object.assign(Object.assign({}, credentials), rest.queryParams) });
41
52
  }
42
53
  else if (!rest.apiKey) {
43
- const apiKey = (_c = (yield core.Supplier.get(this._options.apiKey))) !== null && _c !== void 0 ? _c : process.env.AGENTMAIL_API_KEY;
44
- connectArgs = Object.assign(Object.assign({}, rest), { apiKey });
54
+ const apiKey = (_d = (yield core.Supplier.get(this._options.apiKey))) !== null && _d !== void 0 ? _d : process.env.AGENTMAIL_API_KEY;
55
+ connectArgs = Object.assign(Object.assign({}, connectArgs), { apiKey });
45
56
  }
46
57
  const socket = yield _super.connect.call(this, connectArgs);
47
58
  if (waitForOpen)
48
59
  yield socket.waitForOpen();
60
+ if (autoReconnect && reconnectAttempts > 0) {
61
+ enableAutoReconnect(socket, {
62
+ maxRetries: reconnectAttempts,
63
+ abortSignal: rest.abortSignal,
64
+ policy: this._reconnectPolicy,
65
+ });
66
+ }
49
67
  return socket;
50
68
  });
51
69
  }
@@ -0,0 +1,22 @@
1
+ import type { WebsocketsSocket } from "../api/resources/websockets/client/Socket.mjs";
2
+ export interface ReconnectPolicy {
3
+ baseDelayMs: number;
4
+ maxDelayMs: number;
5
+ /** How long a connection must stay open before the backoff starts over. */
6
+ stableAfterMs: number;
7
+ }
8
+ export declare const DEFAULT_RECONNECT_POLICY: ReconnectPolicy;
9
+ /** Exponential backoff with full jitter: uniform in [0, min(maxDelayMs, baseDelayMs * 2^attempt)). */
10
+ export declare function backoffDelay(attempt: number, policy: ReconnectPolicy, random?: () => number): number;
11
+ export interface AutoReconnectOptions {
12
+ /** The generated socket's own retry budget (`reconnectAttempts`). */
13
+ maxRetries: number;
14
+ abortSignal?: AbortSignal;
15
+ policy: ReconnectPolicy;
16
+ }
17
+ /**
18
+ * Keeps `socket` connected and subscribed: re-sends every subscription on each reconnect, takes over
19
+ * reconnecting whenever the generated socket gives up, and stops on close(), abort, or a terminal
20
+ * handshake refusal.
21
+ */
22
+ export declare function enableAutoReconnect(socket: WebsocketsSocket, options: AutoReconnectOptions): void;
@@ -0,0 +1,208 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { ReconnectingWebSocket } from "../core/index.mjs";
13
+ import * as Events from "../core/websocket/events.mjs";
14
+ export const DEFAULT_RECONNECT_POLICY = { baseDelayMs: 1000, maxDelayMs: 60000, stableAfterMs: 5000 };
15
+ /** Exponential backoff with full jitter: uniform in [0, min(maxDelayMs, baseDelayMs * 2^attempt)). */
16
+ export function backoffDelay(attempt, policy, random = Math.random) {
17
+ return random() * Math.min(policy.maxDelayMs, policy.baseDelayMs * Math.pow(2, attempt));
18
+ }
19
+ const { OPEN, CLOSING, CLOSED } = ReconnectingWebSocket.ReadyState;
20
+ // ws reports a refused handshake as "Unexpected server response: <status>". Browsers expose no status,
21
+ // so there every refusal looks transient and is retried on the capped backoff.
22
+ const HANDSHAKE_STATUS = /^Unexpected server response: (\d{3})\b/;
23
+ // Refusals that retrying with the same credentials cannot fix. 402 comes from the x402/MPP proxies,
24
+ // whose payment credential is minted once per connect() call and reused on reconnects. The server
25
+ // also answers 403 when the organization is at its connection cap, which a retry could outlast.
26
+ const TERMINAL_FAILURES = new Map([
27
+ ["401", "WebSocket authentication failed (401): the API key is missing, invalid, or was deleted."],
28
+ [
29
+ "402",
30
+ "WebSocket payment required (402): the payment credential minted by connect() is no longer accepted; open a new socket with client.websockets.connect() to pay again.",
31
+ ],
32
+ [
33
+ "403",
34
+ "WebSocket authentication failed (403): the API key is invalid or was deleted, or the organization is at its WebSocket connection limit.",
35
+ ],
36
+ ]);
37
+ /**
38
+ * Keeps `socket` connected and subscribed: re-sends every subscription on each reconnect, takes over
39
+ * reconnecting whenever the generated socket gives up, and stops on close(), abort, or a terminal
40
+ * handshake refusal.
41
+ */
42
+ export function enableAutoReconnect(socket, options) {
43
+ const { maxRetries, abortSignal, policy } = options;
44
+ const rws = socket.socket;
45
+ const sendSubscribe = socket.sendSubscribe.bind(socket);
46
+ const closeSocket = rws.close.bind(rws);
47
+ const reconnectSocket = rws.reconnect.bind(rws);
48
+ // Keyed by canonical JSON, in the order last sent. The server keeps one subscription per target and
49
+ // the last filter sent for it, so a new message takes its targets out of every earlier one: a
50
+ // replay can never put back a filter the caller has since changed.
51
+ const subscriptions = new Map();
52
+ // Keys already sent on the current connection, e.g. by the user's own open handler.
53
+ const sentOnConnection = new Set();
54
+ let opened = rws.readyState === OPEN;
55
+ let openedAt = opened ? Date.now() : undefined;
56
+ let stopped = false;
57
+ let attempt = 0;
58
+ let timer;
59
+ let emitting = false;
60
+ const isStopped = () => stopped || (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) === true;
61
+ const cancelTimer = () => {
62
+ clearTimeout(timer);
63
+ timer = undefined;
64
+ };
65
+ const stop = () => {
66
+ stopped = true;
67
+ cancelTimer();
68
+ };
69
+ const emitError = (message) => {
70
+ var _a;
71
+ const event = new Events.ErrorEvent(new Error(message), rws);
72
+ emitting = true;
73
+ try {
74
+ (_a = rws.onerror) === null || _a === void 0 ? void 0 : _a.call(rws, event);
75
+ rws.dispatchEvent(event);
76
+ }
77
+ finally {
78
+ emitting = false;
79
+ }
80
+ };
81
+ const scheduleReconnect = () => {
82
+ cancelTimer();
83
+ timer = setTimeout(() => {
84
+ timer = undefined;
85
+ if (isStopped())
86
+ return;
87
+ // reconnect() on a socket that is not CLOSED closes it with 1000 and then never connects.
88
+ if (rws.readyState === CLOSED)
89
+ reconnectSocket();
90
+ else if (rws.readyState === CLOSING)
91
+ scheduleReconnect();
92
+ }, backoffDelay(attempt++, policy));
93
+ };
94
+ rws.addEventListener("open", () => {
95
+ cancelTimer();
96
+ openedAt = Date.now();
97
+ if (!opened) {
98
+ opened = true;
99
+ return;
100
+ }
101
+ for (const [key, message] of subscriptions) {
102
+ if (isStopped() || rws.readyState !== OPEN)
103
+ return;
104
+ if (sentOnConnection.has(key))
105
+ continue;
106
+ try {
107
+ sendSubscribe(message);
108
+ sentOnConnection.add(key);
109
+ }
110
+ catch (error) {
111
+ emitError(`Failed to re-subscribe after reconnecting: ${error instanceof Error ? error.message : String(error)}`);
112
+ }
113
+ }
114
+ });
115
+ rws.addEventListener("close", (event) => {
116
+ sentOnConnection.clear();
117
+ if (openedAt !== undefined && Date.now() - openedAt >= policy.stableAfterMs)
118
+ attempt = 0;
119
+ openedAt = undefined;
120
+ if (isStopped())
121
+ return;
122
+ if (event.code !== 1000) {
123
+ // The generated socket retries these closes itself, up to maxRetries times.
124
+ if (rws.retryCount < maxRetries)
125
+ return;
126
+ // It has just scheduled its last retry. If that one fails too, it keeps its connect lock
127
+ // and ignores reconnect() for good, so cancel it and take over.
128
+ closeSocket();
129
+ }
130
+ scheduleReconnect();
131
+ });
132
+ rws.addEventListener("error", (event) => {
133
+ var _a;
134
+ if (emitting || isStopped())
135
+ return;
136
+ const status = (_a = HANDSHAKE_STATUS.exec(event.message)) === null || _a === void 0 ? void 0 : _a[1];
137
+ const failure = status === undefined ? undefined : TERMINAL_FAILURES.get(status);
138
+ if (failure === undefined)
139
+ return;
140
+ stop();
141
+ emitError(`${failure} Not reconnecting.`);
142
+ });
143
+ abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener("abort", stop, { once: true });
144
+ // Patched on the generated socket so WebsocketsSocket.close()/connect() and direct
145
+ // socket.socket.close()/reconnect() calls all count as the user's intent.
146
+ rws.close = (code, reason) => {
147
+ stop();
148
+ subscriptions.clear();
149
+ closeSocket(code, reason);
150
+ };
151
+ rws.reconnect = (code, reason) => {
152
+ stopped = false;
153
+ attempt = 0;
154
+ cancelTimer();
155
+ reconnectSocket(code, reason);
156
+ };
157
+ socket.sendSubscribe = (message) => {
158
+ sendSubscribe(message);
159
+ const recorded = JSON.parse(JSON.stringify(message));
160
+ const covered = new Set(targetsOf(recorded));
161
+ const kept = [...subscriptions].flatMap(([key, cached]) => {
162
+ const remaining = withoutTargets(cached, covered);
163
+ if (remaining === undefined)
164
+ return [];
165
+ return [[remaining === cached ? key : subscriptionKey(remaining), remaining]];
166
+ });
167
+ const key = subscriptionKey(recorded);
168
+ subscriptions.clear();
169
+ for (const [keptKey, keptMessage] of kept)
170
+ if (keptKey !== key)
171
+ subscriptions.set(keptKey, keptMessage);
172
+ subscriptions.set(key, recorded);
173
+ sentOnConnection.add(key);
174
+ };
175
+ }
176
+ // What a subscribe message sets on the server: one subscription per inbox and per pod it names, or the
177
+ // credential's own scope when it names neither. That default resolves server-side to an inbox, pod or
178
+ // organization the SDK cannot see, so it is tracked as its own target.
179
+ function targetsOf(message) {
180
+ var _a, _b;
181
+ if (message.inboxIds === undefined && message.podIds === undefined)
182
+ return ["default"];
183
+ return [...((_a = message.inboxIds) !== null && _a !== void 0 ? _a : []).map((id) => `inbox:${id}`), ...((_b = message.podIds) !== null && _b !== void 0 ? _b : []).map((id) => `pod:${id}`)];
184
+ }
185
+ // `message` without the covered targets; undefined once none of its targets is left.
186
+ function withoutTargets(message, covered) {
187
+ const targets = targetsOf(message);
188
+ if (!targets.some((target) => covered.has(target)))
189
+ return message;
190
+ if (targets.every((target) => covered.has(target)))
191
+ return undefined;
192
+ const { inboxIds, podIds } = message, rest = __rest(message, ["inboxIds", "podIds"]);
193
+ const keptInboxIds = inboxIds === null || inboxIds === void 0 ? void 0 : inboxIds.filter((id) => !covered.has(`inbox:${id}`));
194
+ const keptPodIds = podIds === null || podIds === void 0 ? void 0 : podIds.filter((id) => !covered.has(`pod:${id}`));
195
+ return Object.assign(Object.assign(Object.assign({}, rest), ((keptInboxIds === null || keptInboxIds === void 0 ? void 0 : keptInboxIds.length) ? { inboxIds: keptInboxIds } : {})), ((keptPodIds === null || keptPodIds === void 0 ? void 0 : keptPodIds.length) ? { podIds: keptPodIds } : {}));
196
+ }
197
+ // Canonical JSON (sorted object keys), so the same subscription written in a different key order dedupes.
198
+ function subscriptionKey(message) {
199
+ return JSON.stringify(message, (_key, value) => {
200
+ if (value === null || typeof value !== "object" || Array.isArray(value))
201
+ return value;
202
+ const sorted = {};
203
+ for (const key of Object.keys(value).sort()) {
204
+ sorted[key] = value[key];
205
+ }
206
+ return sorted;
207
+ });
208
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentmail",
3
- "version": "0.5.34",
3
+ "version": "0.5.35",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",