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.
- package/dist/cjs/BaseClient.js +2 -2
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/wrapper/WebsocketsClient.d.ts +15 -1
- package/dist/cjs/wrapper/WebsocketsClient.js +26 -8
- package/dist/cjs/wrapper/reconnect.d.ts +22 -0
- package/dist/cjs/wrapper/reconnect.js +246 -0
- package/dist/esm/BaseClient.mjs +2 -2
- package/dist/esm/version.d.mts +1 -1
- package/dist/esm/version.mjs +1 -1
- package/dist/esm/wrapper/WebsocketsClient.d.mts +15 -1
- package/dist/esm/wrapper/WebsocketsClient.mjs +26 -8
- package/dist/esm/wrapper/reconnect.d.mts +22 -0
- package/dist/esm/wrapper/reconnect.mjs +208 -0
- package/package.json +1 -1
package/dist/cjs/BaseClient.js
CHANGED
|
@@ -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.
|
|
47
|
-
"User-Agent": "agentmail/0.5.
|
|
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);
|
package/dist/cjs/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.5.
|
|
1
|
+
export declare const SDK_VERSION = "0.5.35";
|
package/dist/cjs/version.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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((
|
|
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({},
|
|
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 = (
|
|
80
|
-
connectArgs = Object.assign(Object.assign({},
|
|
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
|
+
}
|
package/dist/esm/BaseClient.mjs
CHANGED
|
@@ -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.
|
|
10
|
-
"User-Agent": "agentmail/0.5.
|
|
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);
|
package/dist/esm/version.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.5.
|
|
1
|
+
export declare const SDK_VERSION = "0.5.35";
|
package/dist/esm/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.5.
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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({},
|
|
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 = (
|
|
44
|
-
connectArgs = Object.assign(Object.assign({},
|
|
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
|
+
}
|