@axlsdk/axl 0.27.1 → 0.27.2
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/index.cjs +438 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -21
- package/dist/index.d.ts +88 -21
- package/dist/index.js +438 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -602,7 +602,7 @@ var sharedGuard = typeof installedGuard === "object" && installedGuard !== null
|
|
|
602
602
|
var buildPath = typeof __AXL_BUILD_PATH__ === "string" ? __AXL_BUILD_PATH__ : "<source>";
|
|
603
603
|
var thisCopy = {
|
|
604
604
|
path: buildPath.startsWith("file:") ? (0, import_node_url.fileURLToPath)(buildPath) : buildPath,
|
|
605
|
-
version: true ? "0.27.
|
|
605
|
+
version: true ? "0.27.2" : "unknown"
|
|
606
606
|
};
|
|
607
607
|
function warnCrossCopy(other, joined) {
|
|
608
608
|
if (other === thisCopy) return;
|
|
@@ -19068,6 +19068,21 @@ function codePointRank(unit) {
|
|
|
19068
19068
|
var import_node_fs = require("fs");
|
|
19069
19069
|
var import_node_path = require("path");
|
|
19070
19070
|
var import_node_os = require("os");
|
|
19071
|
+
|
|
19072
|
+
// src/state/session-update.ts
|
|
19073
|
+
async function updateSessionThroughOverrides(store, storeName, sessionId, update) {
|
|
19074
|
+
warnDiagnosticOnce(
|
|
19075
|
+
`session_update_overridden\0${storeName}`,
|
|
19076
|
+
`${storeName}: this subclass overrides saveSession or getSession, so session turns go through them, and overlapping turns from different instances can overwrite each other. Override updateSession too to keep that protection.`,
|
|
19077
|
+
void 0,
|
|
19078
|
+
"docs/api-reference.md#several-app-instances-sharing-a-session"
|
|
19079
|
+
);
|
|
19080
|
+
const stored = await store.getSession(sessionId);
|
|
19081
|
+
const next = update(stored.length > 0 ? stored : null);
|
|
19082
|
+
if (next !== null) await store.saveSession(sessionId, next);
|
|
19083
|
+
}
|
|
19084
|
+
|
|
19085
|
+
// src/state/memory.ts
|
|
19071
19086
|
function compareOldestFirst(a, b) {
|
|
19072
19087
|
return compareExecutionsNewestFirst(b, a);
|
|
19073
19088
|
}
|
|
@@ -19084,7 +19099,7 @@ function resolveMaxExecutions(value) {
|
|
|
19084
19099
|
}
|
|
19085
19100
|
var AWAIT_HUMAN_TEMP_DIR = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "axl-memory-store");
|
|
19086
19101
|
var AWAIT_HUMAN_TEMP_FILE = (0, import_node_path.join)(AWAIT_HUMAN_TEMP_DIR, "await-human-state.json");
|
|
19087
|
-
var MemoryStore = class {
|
|
19102
|
+
var MemoryStore = class _MemoryStore {
|
|
19088
19103
|
checkpoints = /* @__PURE__ */ new Map();
|
|
19089
19104
|
sessions = /* @__PURE__ */ new Map();
|
|
19090
19105
|
sessionMeta = /* @__PURE__ */ new Map();
|
|
@@ -19136,6 +19151,14 @@ var MemoryStore = class {
|
|
|
19136
19151
|
async saveSession(sessionId, history) {
|
|
19137
19152
|
this.sessions.set(sessionId, structuredClone(normalizePersistedSessionHistory(history)));
|
|
19138
19153
|
}
|
|
19154
|
+
async updateSession(sessionId, update) {
|
|
19155
|
+
if (this.saveSession !== _MemoryStore.prototype.saveSession || this.getSession !== _MemoryStore.prototype.getSession) {
|
|
19156
|
+
return updateSessionThroughOverrides(this, "MemoryStore", sessionId, update);
|
|
19157
|
+
}
|
|
19158
|
+
const stored = this.sessions.get(sessionId);
|
|
19159
|
+
const next = update(stored ? structuredClone(stored) : null);
|
|
19160
|
+
if (next !== null) await this.saveSession(sessionId, next);
|
|
19161
|
+
}
|
|
19139
19162
|
async getSession(sessionId) {
|
|
19140
19163
|
const history = this.sessions.get(sessionId);
|
|
19141
19164
|
return history ? structuredClone(history) : [];
|
|
@@ -19559,7 +19582,7 @@ function rowToExecutionInfo(row) {
|
|
|
19559
19582
|
}
|
|
19560
19583
|
return normalizeStoredExecution(info);
|
|
19561
19584
|
}
|
|
19562
|
-
var SQLiteStore = class {
|
|
19585
|
+
var SQLiteStore = class _SQLiteStore {
|
|
19563
19586
|
db;
|
|
19564
19587
|
ttls;
|
|
19565
19588
|
now;
|
|
@@ -19793,6 +19816,17 @@ var SQLiteStore = class {
|
|
|
19793
19816
|
);
|
|
19794
19817
|
stmt.run(sessionId, JSON.stringify(normalizePersistedSessionHistory(history)));
|
|
19795
19818
|
}
|
|
19819
|
+
async updateSession(sessionId, update) {
|
|
19820
|
+
if (this.saveSession !== _SQLiteStore.prototype.saveSession || this.getSession !== _SQLiteStore.prototype.getSession) {
|
|
19821
|
+
return updateSessionThroughOverrides(this, "SQLiteStore", sessionId, update);
|
|
19822
|
+
}
|
|
19823
|
+
this.db.transaction(() => {
|
|
19824
|
+
const row = this.db.prepare("SELECT history FROM sessions WHERE session_id = ?").get(sessionId);
|
|
19825
|
+
const next = update(row ? safeJsonParse(row.history) ?? [] : null);
|
|
19826
|
+
if (next === null) return;
|
|
19827
|
+
this.db.prepare("INSERT OR REPLACE INTO sessions (session_id, history) VALUES (?, ?)").run(sessionId, JSON.stringify(normalizePersistedSessionHistory(next)));
|
|
19828
|
+
}).immediate();
|
|
19829
|
+
}
|
|
19796
19830
|
async getSession(sessionId) {
|
|
19797
19831
|
const stmt = this.db.prepare("SELECT history FROM sessions WHERE session_id = ?");
|
|
19798
19832
|
const row = stmt.get(sessionId);
|
|
@@ -20127,10 +20161,10 @@ var Session = class _Session {
|
|
|
20127
20161
|
);
|
|
20128
20162
|
}
|
|
20129
20163
|
async sendImpl(workflowName, input, signal, events) {
|
|
20130
|
-
const { history, metadata } = await this.prepareHistory(input);
|
|
20164
|
+
const { history, metadata, base } = await this.prepareHistory(input);
|
|
20131
20165
|
try {
|
|
20132
20166
|
const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
|
|
20133
|
-
await this.commitHistory(history, result);
|
|
20167
|
+
await this.commitHistory(history, base, result);
|
|
20134
20168
|
return result;
|
|
20135
20169
|
} finally {
|
|
20136
20170
|
clearSessionInput(history);
|
|
@@ -20146,6 +20180,8 @@ var Session = class _Session {
|
|
|
20146
20180
|
async prepareHistory(input) {
|
|
20147
20181
|
const preparedInput = prepareSessionInput(input);
|
|
20148
20182
|
const history = await this.store.getSession(this.sessionId);
|
|
20183
|
+
const storedJson = JSON.stringify(history);
|
|
20184
|
+
const hadHistory = history.length > 0;
|
|
20149
20185
|
let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
|
|
20150
20186
|
const maxMessages = this.options.history?.maxMessages;
|
|
20151
20187
|
if (maxMessages && history.length > maxMessages) {
|
|
@@ -20160,13 +20196,17 @@ var Session = class _Session {
|
|
|
20160
20196
|
summaryModel,
|
|
20161
20197
|
cachedSummary ? { previousSummary: cachedSummary } : void 0
|
|
20162
20198
|
);
|
|
20163
|
-
await this.store.
|
|
20199
|
+
const stored = JSON.stringify(await this.store.getSession(this.sessionId));
|
|
20200
|
+
if (stored === storedJson) {
|
|
20201
|
+
await this.store.saveSessionMeta(this.sessionId, "summaryCache", summary);
|
|
20202
|
+
}
|
|
20164
20203
|
cachedSummary = summary;
|
|
20165
20204
|
}
|
|
20166
20205
|
const trimmed = history.slice(-maxMessages);
|
|
20167
20206
|
history.length = 0;
|
|
20168
20207
|
history.push(...trimmed.map(withoutAnthropicThinking));
|
|
20169
20208
|
}
|
|
20209
|
+
const base = { storedJson, hadHistory, firstOwnMessage: history.length };
|
|
20170
20210
|
const userMessage = { role: "user", content: preparedInput.content };
|
|
20171
20211
|
history.push(userMessage);
|
|
20172
20212
|
if (this.options.deduplicateInput !== false && preparedInput.normalizedInput !== void 0) {
|
|
@@ -20180,13 +20220,13 @@ var Session = class _Session {
|
|
|
20180
20220
|
// agent-scoped ask-summary records the runtime would otherwise cache.
|
|
20181
20221
|
...this.options.persist === false ? { sessionPersist: false } : {}
|
|
20182
20222
|
};
|
|
20183
|
-
return { history, metadata };
|
|
20223
|
+
return { history, metadata, base };
|
|
20184
20224
|
}
|
|
20185
20225
|
/** Push the assistant reply (if `executeAgentCall` didn't already) and
|
|
20186
20226
|
* persist. Shared by `sendImpl` and `streamImpl`. The fallback assistant
|
|
20187
20227
|
* push has no agent context and so leaves `agent` undefined — see the
|
|
20188
20228
|
* `ChatMessage.agent` field doc. */
|
|
20189
|
-
async commitHistory(history, result) {
|
|
20229
|
+
async commitHistory(history, base, result) {
|
|
20190
20230
|
const lastMsg = history[history.length - 1];
|
|
20191
20231
|
if (!(lastMsg && lastMsg.role === "assistant")) {
|
|
20192
20232
|
history.push({
|
|
@@ -20194,8 +20234,35 @@ var Session = class _Session {
|
|
|
20194
20234
|
content: typeof result === "string" ? result : JSON.stringify(result)
|
|
20195
20235
|
});
|
|
20196
20236
|
}
|
|
20197
|
-
if (this.options.persist
|
|
20237
|
+
if (this.options.persist === false) return;
|
|
20238
|
+
if (!this.store.updateSession) {
|
|
20198
20239
|
await this.store.saveSession(this.sessionId, history);
|
|
20240
|
+
return;
|
|
20241
|
+
}
|
|
20242
|
+
let outcome = "saved";
|
|
20243
|
+
await this.store.updateSession(this.sessionId, (current) => {
|
|
20244
|
+
if (current === null) {
|
|
20245
|
+
outcome = base.hadHistory ? "discarded" : "saved";
|
|
20246
|
+
return base.hadHistory ? null : history;
|
|
20247
|
+
}
|
|
20248
|
+
if (JSON.stringify(current) === base.storedJson) {
|
|
20249
|
+
outcome = "saved";
|
|
20250
|
+
return history;
|
|
20251
|
+
}
|
|
20252
|
+
outcome = "merged";
|
|
20253
|
+
return [...current, ...history.slice(base.firstOwnMessage).map(withoutAnthropicThinking)];
|
|
20254
|
+
});
|
|
20255
|
+
if (outcome === "merged") this.notify("session_turns_merged");
|
|
20256
|
+
else if (outcome === "discarded") this.notify("session_turn_discarded");
|
|
20257
|
+
}
|
|
20258
|
+
/** The turn is already saved, so a listener that throws can't be allowed to fail it. */
|
|
20259
|
+
notify(event) {
|
|
20260
|
+
try {
|
|
20261
|
+
this.runtime.emit(event, { sessionId: this.sessionId });
|
|
20262
|
+
} catch (err) {
|
|
20263
|
+
console.error(
|
|
20264
|
+
`[axl] A ${event} listener failed: ${err instanceof Error ? err.message : String(err)}`
|
|
20265
|
+
);
|
|
20199
20266
|
}
|
|
20200
20267
|
}
|
|
20201
20268
|
async stream(workflowName, input, options) {
|
|
@@ -20223,10 +20290,12 @@ var Session = class _Session {
|
|
|
20223
20290
|
}
|
|
20224
20291
|
async streamImpl(workflowName, input, resolveReady, rejectReady, signal, events) {
|
|
20225
20292
|
let history;
|
|
20293
|
+
let base;
|
|
20226
20294
|
let axlStream;
|
|
20227
20295
|
try {
|
|
20228
20296
|
const prepared = await this.prepareHistory(input);
|
|
20229
20297
|
history = prepared.history;
|
|
20298
|
+
base = prepared.base;
|
|
20230
20299
|
axlStream = this.runtime.stream(workflowName, input, {
|
|
20231
20300
|
metadata: prepared.metadata,
|
|
20232
20301
|
signal,
|
|
@@ -20240,7 +20309,7 @@ var Session = class _Session {
|
|
|
20240
20309
|
resolveReady(axlStream);
|
|
20241
20310
|
try {
|
|
20242
20311
|
const completion = axlStream.promise.then(
|
|
20243
|
-
(result) => this.commitHistory(history, result).catch((err) => {
|
|
20312
|
+
(result) => this.commitHistory(history, base, result).catch((err) => {
|
|
20244
20313
|
const error = err instanceof Error ? err.message : String(err);
|
|
20245
20314
|
try {
|
|
20246
20315
|
this.runtime.emit("error", {
|
|
@@ -24790,6 +24859,7 @@ async function* stringStreamFromEvents(source, opts) {
|
|
|
24790
24859
|
}
|
|
24791
24860
|
|
|
24792
24861
|
// src/state/redis.ts
|
|
24862
|
+
var import_node_crypto6 = require("crypto");
|
|
24793
24863
|
var PRUNE_HISTORY_INDEX_SCRIPT = `
|
|
24794
24864
|
local now = tonumber(ARGV[1])
|
|
24795
24865
|
local removed = 0
|
|
@@ -24809,6 +24879,34 @@ for i = 2, #ARGV do
|
|
|
24809
24879
|
end
|
|
24810
24880
|
return removed
|
|
24811
24881
|
`;
|
|
24882
|
+
var PRUNE_SESSION_IDS_SCRIPT = `
|
|
24883
|
+
local removed = {}
|
|
24884
|
+
for i, id in ipairs(ARGV) do
|
|
24885
|
+
if redis.call('EXISTS', KEYS[i * 2]) == 0 then
|
|
24886
|
+
redis.call('SREM', KEYS[1], id)
|
|
24887
|
+
redis.call('DEL', KEYS[i * 2 + 1])
|
|
24888
|
+
removed[#removed + 1] = id
|
|
24889
|
+
end
|
|
24890
|
+
end
|
|
24891
|
+
return removed
|
|
24892
|
+
`;
|
|
24893
|
+
var SESSION_ID_BATCH = 100;
|
|
24894
|
+
var SESSION_SWEEP_COUNT = 50;
|
|
24895
|
+
var UPDATE_SESSION_SCRIPT = `
|
|
24896
|
+
local stored = redis.call('GET', KEYS[1])
|
|
24897
|
+
local actual = stored and redis.sha1hex(stored) or ''
|
|
24898
|
+
if actual ~= ARGV[1] then
|
|
24899
|
+
return 0
|
|
24900
|
+
end
|
|
24901
|
+
if ARGV[3] ~= '' then
|
|
24902
|
+
redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
|
|
24903
|
+
else
|
|
24904
|
+
redis.call('SET', KEYS[1], ARGV[2])
|
|
24905
|
+
end
|
|
24906
|
+
redis.call('SADD', KEYS[2], ARGV[4])
|
|
24907
|
+
return 1
|
|
24908
|
+
`;
|
|
24909
|
+
var UPDATE_SESSION_ATTEMPTS = 10;
|
|
24812
24910
|
function compareMembers(a, b) {
|
|
24813
24911
|
return Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
24814
24912
|
}
|
|
@@ -24830,7 +24928,6 @@ function abandonConnection(client) {
|
|
|
24830
24928
|
client.destroy?.();
|
|
24831
24929
|
} catch {
|
|
24832
24930
|
}
|
|
24833
|
-
client.unref?.();
|
|
24834
24931
|
}
|
|
24835
24932
|
function errorMessage(error) {
|
|
24836
24933
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -24859,18 +24956,106 @@ function connectionEventReporter(client, onConnectionEvent) {
|
|
|
24859
24956
|
}
|
|
24860
24957
|
};
|
|
24861
24958
|
}
|
|
24959
|
+
var LOOP_STALL_TOLERANCE_MS = 100;
|
|
24960
|
+
var LOOP_STALL_GRACE_MS = 1e3;
|
|
24961
|
+
function watchResponses(client, timeoutMs, reportConnectionEvent) {
|
|
24962
|
+
const checkEveryMs = timeoutMs / 3;
|
|
24963
|
+
let timer;
|
|
24964
|
+
let stopped = false;
|
|
24965
|
+
const schedule = (run, delayMs) => {
|
|
24966
|
+
timer = setTimeout(run, delayMs);
|
|
24967
|
+
timer.unref?.();
|
|
24968
|
+
};
|
|
24969
|
+
const check = () => {
|
|
24970
|
+
if (client.isReady === false) {
|
|
24971
|
+
schedule(check, checkEveryMs);
|
|
24972
|
+
return;
|
|
24973
|
+
}
|
|
24974
|
+
let settled = false;
|
|
24975
|
+
const onReply = () => {
|
|
24976
|
+
if (settled || stopped) return;
|
|
24977
|
+
settled = true;
|
|
24978
|
+
clearTimeout(timer);
|
|
24979
|
+
schedule(check, checkEveryMs);
|
|
24980
|
+
};
|
|
24981
|
+
const expireAfter = (delayMs, mayWaitForBlockedLoop) => {
|
|
24982
|
+
const dueAt = Date.now() + delayMs;
|
|
24983
|
+
schedule(() => {
|
|
24984
|
+
if (settled || stopped) return;
|
|
24985
|
+
if (mayWaitForBlockedLoop && Date.now() - dueAt > LOOP_STALL_TOLERANCE_MS) {
|
|
24986
|
+
expireAfter(LOOP_STALL_GRACE_MS, false);
|
|
24987
|
+
return;
|
|
24988
|
+
}
|
|
24989
|
+
reset();
|
|
24990
|
+
}, delayMs);
|
|
24991
|
+
};
|
|
24992
|
+
const reset = () => {
|
|
24993
|
+
settled = true;
|
|
24994
|
+
if (client.isReady === false) {
|
|
24995
|
+
schedule(check, checkEveryMs);
|
|
24996
|
+
return;
|
|
24997
|
+
}
|
|
24998
|
+
client.destroy?.();
|
|
24999
|
+
client.connect().catch(() => {
|
|
25000
|
+
});
|
|
25001
|
+
reportConnectionEvent({
|
|
25002
|
+
type: "error",
|
|
25003
|
+
error: new Error(`Redis did not answer within ${timeoutMs}ms; reconnecting.`)
|
|
25004
|
+
});
|
|
25005
|
+
schedule(check, checkEveryMs);
|
|
25006
|
+
};
|
|
25007
|
+
client.ping().then(onReply, onReply);
|
|
25008
|
+
expireAfter(timeoutMs, true);
|
|
25009
|
+
};
|
|
25010
|
+
schedule(check, checkEveryMs);
|
|
25011
|
+
return () => {
|
|
25012
|
+
stopped = true;
|
|
25013
|
+
clearTimeout(timer);
|
|
25014
|
+
};
|
|
25015
|
+
}
|
|
25016
|
+
async function connectWithin(connecting, timeoutMs) {
|
|
25017
|
+
let timer;
|
|
25018
|
+
try {
|
|
25019
|
+
await Promise.race([
|
|
25020
|
+
connecting,
|
|
25021
|
+
new Promise((_resolve, reject) => {
|
|
25022
|
+
timer = setTimeout(
|
|
25023
|
+
() => reject(
|
|
25024
|
+
new AxlError(
|
|
25025
|
+
"REDIS_UNAVAILABLE",
|
|
25026
|
+
`RedisStore: Redis did not finish connecting within ${timeoutMs}ms.`
|
|
25027
|
+
)
|
|
25028
|
+
),
|
|
25029
|
+
timeoutMs
|
|
25030
|
+
);
|
|
25031
|
+
})
|
|
25032
|
+
]);
|
|
25033
|
+
} finally {
|
|
25034
|
+
clearTimeout(timer);
|
|
25035
|
+
}
|
|
25036
|
+
}
|
|
24862
25037
|
async function connectClient(createClient, opts) {
|
|
24863
|
-
|
|
25038
|
+
const inBackground = opts.waitForConnection === false;
|
|
25039
|
+
let established = inBackground;
|
|
25040
|
+
let everFailed = false;
|
|
25041
|
+
const responseTimeoutMs = opts.responseTimeoutMs;
|
|
24864
25042
|
const client = createClient({
|
|
24865
25043
|
...opts.url ? { url: opts.url } : {},
|
|
24866
25044
|
// A command accepted just before a drop rejects instead of waiting for
|
|
24867
25045
|
// the reconnect.
|
|
24868
25046
|
disableOfflineQueue: true,
|
|
24869
|
-
socket: {
|
|
25047
|
+
socket: {
|
|
25048
|
+
reconnectStrategy: (retries) => established ? reconnectDelayMs(retries) : false,
|
|
25049
|
+
// Bounds a reconnect's handshake, where the response watch doesn't run.
|
|
25050
|
+
// Twice the timeout so that on an open connection the watch acts first:
|
|
25051
|
+
// this timer fires after any long event-loop block, which the watch allows for.
|
|
25052
|
+
...responseTimeoutMs !== void 0 ? { socketTimeout: responseTimeoutMs * 2 } : {}
|
|
25053
|
+
}
|
|
24870
25054
|
});
|
|
24871
25055
|
const reportConnectionEvent = connectionEventReporter(client, opts.onConnectionEvent);
|
|
24872
25056
|
client.on("error", (error) => {
|
|
24873
25057
|
if (!established || client.isOpen === false) return;
|
|
25058
|
+
everFailed = true;
|
|
24874
25059
|
reportConnectionEvent({
|
|
24875
25060
|
type: "error",
|
|
24876
25061
|
error: error instanceof Error ? error : new Error(String(error))
|
|
@@ -24882,12 +25067,32 @@ async function connectClient(createClient, opts) {
|
|
|
24882
25067
|
client.on("ready", () => {
|
|
24883
25068
|
if (!established) {
|
|
24884
25069
|
established = true;
|
|
24885
|
-
} else if (client.isOpen !== false) {
|
|
25070
|
+
} else if (client.isOpen !== false && (!inBackground || everFailed)) {
|
|
24886
25071
|
reportConnectionEvent({ type: "reconnected" });
|
|
24887
25072
|
}
|
|
24888
25073
|
});
|
|
24889
|
-
|
|
24890
|
-
|
|
25074
|
+
if (inBackground) {
|
|
25075
|
+
client.connect().catch(() => {
|
|
25076
|
+
});
|
|
25077
|
+
return {
|
|
25078
|
+
client,
|
|
25079
|
+
stopResponseWatch: watchResponses(client, responseTimeoutMs, reportConnectionEvent)
|
|
25080
|
+
};
|
|
25081
|
+
}
|
|
25082
|
+
if (responseTimeoutMs === void 0) {
|
|
25083
|
+
await client.connect();
|
|
25084
|
+
return { client };
|
|
25085
|
+
}
|
|
25086
|
+
try {
|
|
25087
|
+
await connectWithin(client.connect(), responseTimeoutMs);
|
|
25088
|
+
} catch (error) {
|
|
25089
|
+
abandonConnection(client);
|
|
25090
|
+
throw error;
|
|
25091
|
+
}
|
|
25092
|
+
return {
|
|
25093
|
+
client,
|
|
25094
|
+
stopResponseWatch: watchResponses(client, responseTimeoutMs, reportConnectionEvent)
|
|
25095
|
+
};
|
|
24891
25096
|
}
|
|
24892
25097
|
var RedisStore = class _RedisStore {
|
|
24893
25098
|
constructor(connection, keyPrefix, ttls) {
|
|
@@ -24895,13 +25100,19 @@ var RedisStore = class _RedisStore {
|
|
|
24895
25100
|
this.keyPrefix = keyPrefix;
|
|
24896
25101
|
this.ttls = ttls;
|
|
24897
25102
|
}
|
|
25103
|
+
/** Set when `close()` starts, while the connection is still open. */
|
|
25104
|
+
closing = false;
|
|
25105
|
+
/** Where the next save's check of listed sessions picks up. */
|
|
25106
|
+
sessionSweepCursor = "0";
|
|
25107
|
+
/** Overlapping saves would all check the same page, so only one checks at a time. */
|
|
25108
|
+
sessionSweepRunning = false;
|
|
24898
25109
|
/**
|
|
24899
25110
|
* Whether the store can send commands right now, for a health check or for
|
|
24900
25111
|
* telling an outage from other failures when a call rejects. `false` while
|
|
24901
25112
|
* reconnecting and after `close()`.
|
|
24902
25113
|
*/
|
|
24903
25114
|
get isConnected() {
|
|
24904
|
-
return this.connection.isOpen !== false && this.connection.isReady !== false;
|
|
25115
|
+
return !this.closing && this.connection.isOpen !== false && this.connection.isReady !== false;
|
|
24905
25116
|
}
|
|
24906
25117
|
/**
|
|
24907
25118
|
* The client for sending a command, failing immediately while disconnected.
|
|
@@ -24915,6 +25126,7 @@ var RedisStore = class _RedisStore {
|
|
|
24915
25126
|
* "The client is closed" rejection.
|
|
24916
25127
|
*/
|
|
24917
25128
|
get client() {
|
|
25129
|
+
if (this.closing) throw new Error("The client is closed");
|
|
24918
25130
|
if (this.connection.isOpen !== false && this.connection.isReady === false) {
|
|
24919
25131
|
throw new AxlError(
|
|
24920
25132
|
"REDIS_UNAVAILABLE",
|
|
@@ -24930,6 +25142,8 @@ var RedisStore = class _RedisStore {
|
|
|
24930
25142
|
* instance needs no constructor state.
|
|
24931
25143
|
*/
|
|
24932
25144
|
prunesInFlight;
|
|
25145
|
+
/** Stops the `responseTimeoutMs` health checks; unset when the option is off. */
|
|
25146
|
+
stopResponseWatch;
|
|
24933
25147
|
/**
|
|
24934
25148
|
* Supports `listExecutions(limit, { cursor })`. A getter rather than a
|
|
24935
25149
|
* field so it lives on the prototype.
|
|
@@ -24979,6 +25193,17 @@ var RedisStore = class _RedisStore {
|
|
|
24979
25193
|
'RedisStore: keyPrefix cannot be empty string. Omit it for the default "axl:" prefix.'
|
|
24980
25194
|
);
|
|
24981
25195
|
}
|
|
25196
|
+
const responseTimeoutMs = opts.responseTimeoutMs;
|
|
25197
|
+
if (responseTimeoutMs !== void 0 && !(Number.isFinite(responseTimeoutMs) && responseTimeoutMs > 0)) {
|
|
25198
|
+
throw new Error(
|
|
25199
|
+
`RedisStore: responseTimeoutMs must be a positive number of milliseconds, got ${responseTimeoutMs}.`
|
|
25200
|
+
);
|
|
25201
|
+
}
|
|
25202
|
+
if (opts.waitForConnection === false && responseTimeoutMs === void 0) {
|
|
25203
|
+
throw new Error(
|
|
25204
|
+
"RedisStore: waitForConnection: false requires responseTimeoutMs, so a server that accepts the connection but never answers cannot leave it connecting forever."
|
|
25205
|
+
);
|
|
25206
|
+
}
|
|
24982
25207
|
let createClient;
|
|
24983
25208
|
try {
|
|
24984
25209
|
const mod = require("redis");
|
|
@@ -24992,7 +25217,7 @@ var RedisStore = class _RedisStore {
|
|
|
24992
25217
|
if (err instanceof Error && err.message.includes("createClient")) throw err;
|
|
24993
25218
|
throw new Error("redis is required for RedisStore. Install it with: npm install redis");
|
|
24994
25219
|
}
|
|
24995
|
-
const client = await connectClient(createClient, opts);
|
|
25220
|
+
const { client, stopResponseWatch } = await connectClient(createClient, opts);
|
|
24996
25221
|
const resolveCategory = (override) => {
|
|
24997
25222
|
if (override === null) return null;
|
|
24998
25223
|
if (typeof override === "number") return override;
|
|
@@ -25014,18 +25239,48 @@ var RedisStore = class _RedisStore {
|
|
|
25014
25239
|
streamingEvents: resolveStreamingEvents(opts.ttls?.streamingEvents)
|
|
25015
25240
|
};
|
|
25016
25241
|
const store = new _RedisStore(client, keyPrefix, effectiveTtls);
|
|
25242
|
+
store.stopResponseWatch = stopResponseWatch;
|
|
25017
25243
|
warnIfRedisExecutionRetentionUnset(opts);
|
|
25244
|
+
if (opts.waitForConnection === false) {
|
|
25245
|
+
if (!opts.skipMigration) store.backfillOnceConnected(client);
|
|
25246
|
+
return store;
|
|
25247
|
+
}
|
|
25018
25248
|
if (!opts.skipMigration) {
|
|
25019
25249
|
try {
|
|
25020
25250
|
await store.backfillExecutionIndex();
|
|
25021
25251
|
await store.backfillEvalIndex();
|
|
25022
25252
|
} catch (error) {
|
|
25253
|
+
stopResponseWatch?.();
|
|
25023
25254
|
abandonConnection(client);
|
|
25024
25255
|
throw error;
|
|
25025
25256
|
}
|
|
25026
25257
|
}
|
|
25027
25258
|
return store;
|
|
25028
25259
|
}
|
|
25260
|
+
/**
|
|
25261
|
+
* Run the startup backfill when a connection becomes ready, until it succeeds.
|
|
25262
|
+
* Nothing is waiting on it, and listings work without it, so a failure is
|
|
25263
|
+
* reported and the next connection tries again.
|
|
25264
|
+
*/
|
|
25265
|
+
backfillOnceConnected(client) {
|
|
25266
|
+
let state = "waiting";
|
|
25267
|
+
client.on("ready", () => {
|
|
25268
|
+
if (state !== "waiting") return;
|
|
25269
|
+
state = "running";
|
|
25270
|
+
this.backfillExecutionIndex().then(() => this.backfillEvalIndex()).then(
|
|
25271
|
+
() => {
|
|
25272
|
+
state = "done";
|
|
25273
|
+
},
|
|
25274
|
+
(err) => {
|
|
25275
|
+
state = "waiting";
|
|
25276
|
+
if (client.isReady === false || client.isOpen === false) return;
|
|
25277
|
+
console.warn(
|
|
25278
|
+
`[axl] RedisStore: the startup index backfill failed (${errorMessage(err)}). Listings stay correct meanwhile.`
|
|
25279
|
+
);
|
|
25280
|
+
}
|
|
25281
|
+
);
|
|
25282
|
+
});
|
|
25283
|
+
}
|
|
25029
25284
|
/**
|
|
25030
25285
|
* Backfill the execution-history sorted-set from the legacy ID set.
|
|
25031
25286
|
* Idempotent — runs only if the ZSET is missing entries the SET has.
|
|
@@ -25033,36 +25288,30 @@ var RedisStore = class _RedisStore {
|
|
|
25033
25288
|
* `skipMigration: true`.
|
|
25034
25289
|
*/
|
|
25035
25290
|
async backfillExecutionIndex() {
|
|
25036
|
-
await this.backfillHistoryIndex(
|
|
25037
|
-
|
|
25038
|
-
|
|
25039
|
-
|
|
25040
|
-
(raw) => {
|
|
25041
|
-
const exec = JSON.parse(raw);
|
|
25042
|
-
return typeof exec.startedAt === "number" ? { score: exec.startedAt, value: exec.executionId } : null;
|
|
25043
|
-
},
|
|
25044
|
-
"execution-history"
|
|
25045
|
-
);
|
|
25291
|
+
await this.backfillHistoryIndex(this.historyIndex("execution"), (raw) => {
|
|
25292
|
+
const exec = JSON.parse(raw);
|
|
25293
|
+
return typeof exec.startedAt === "number" ? { score: exec.startedAt, value: exec.executionId } : null;
|
|
25294
|
+
});
|
|
25046
25295
|
}
|
|
25047
25296
|
/** Backfill the eval-history sorted-set. Symmetric to `backfillExecutionIndex`. */
|
|
25048
25297
|
async backfillEvalIndex() {
|
|
25049
|
-
await this.backfillHistoryIndex(
|
|
25050
|
-
|
|
25051
|
-
|
|
25052
|
-
|
|
25053
|
-
(raw) => {
|
|
25054
|
-
const entry = JSON.parse(raw);
|
|
25055
|
-
return typeof entry.timestamp === "number" ? { score: entry.timestamp, value: entry.id } : null;
|
|
25056
|
-
},
|
|
25057
|
-
"eval-history"
|
|
25058
|
-
);
|
|
25298
|
+
await this.backfillHistoryIndex(this.historyIndex("eval"), (raw) => {
|
|
25299
|
+
const entry = JSON.parse(raw);
|
|
25300
|
+
return typeof entry.timestamp === "number" ? { score: entry.timestamp, value: entry.id } : null;
|
|
25301
|
+
});
|
|
25059
25302
|
}
|
|
25060
25303
|
/**
|
|
25061
25304
|
* Shared implementation for both backfill paths. Reads SET members,
|
|
25062
25305
|
* batch-MGETs their data blobs in chunks, and ZADDs each into the new
|
|
25063
25306
|
* sorted-set inside a single MULTI per chunk.
|
|
25307
|
+
*
|
|
25308
|
+
* Ids with no data blob are removed from the id set. Left there, the set stays
|
|
25309
|
+
* larger than the sorted set, so every startup repeats this walk and listing
|
|
25310
|
+
* stays on its slow path.
|
|
25064
25311
|
*/
|
|
25065
|
-
async backfillHistoryIndex(
|
|
25312
|
+
async backfillHistoryIndex(index, scoreOf) {
|
|
25313
|
+
const setKey = index.ids;
|
|
25314
|
+
const zsetKey = index.time;
|
|
25066
25315
|
const [setSize, zsetSize] = await Promise.all([
|
|
25067
25316
|
this.client.sCard(setKey),
|
|
25068
25317
|
this.client.zCard(zsetKey)
|
|
@@ -25073,9 +25322,10 @@ var RedisStore = class _RedisStore {
|
|
|
25073
25322
|
const CHUNK_SIZE = 500;
|
|
25074
25323
|
let migrated = 0;
|
|
25075
25324
|
let skipped = 0;
|
|
25325
|
+
const missing = [];
|
|
25076
25326
|
for (let i = 0; i < allIds.length; i += CHUNK_SIZE) {
|
|
25077
25327
|
const chunk = allIds.slice(i, i + CHUNK_SIZE);
|
|
25078
|
-
const dataKeys = chunk.map((id) => dataKey(id));
|
|
25328
|
+
const dataKeys = chunk.map((id) => index.dataKey(id));
|
|
25079
25329
|
const raws = await this.client.mGet(dataKeys);
|
|
25080
25330
|
const tx = this.client.multi();
|
|
25081
25331
|
let queuedInChunk = 0;
|
|
@@ -25083,6 +25333,7 @@ var RedisStore = class _RedisStore {
|
|
|
25083
25333
|
const raw = raws[j];
|
|
25084
25334
|
if (raw == null) {
|
|
25085
25335
|
skipped++;
|
|
25336
|
+
missing.push(chunk[j]);
|
|
25086
25337
|
continue;
|
|
25087
25338
|
}
|
|
25088
25339
|
try {
|
|
@@ -25100,9 +25351,14 @@ var RedisStore = class _RedisStore {
|
|
|
25100
25351
|
}
|
|
25101
25352
|
if (queuedInChunk > 0) await tx.exec();
|
|
25102
25353
|
}
|
|
25354
|
+
if (missing.length > 0) {
|
|
25355
|
+
await this.runPruneScript(index, missing).catch(
|
|
25356
|
+
(err) => this.warnPruneFailed(index, err)
|
|
25357
|
+
);
|
|
25358
|
+
}
|
|
25103
25359
|
if (migrated > 0 || skipped > 0) {
|
|
25104
25360
|
console.log(
|
|
25105
|
-
`[axl] RedisStore: backfilled ${migrated} ${label} entries into sorted-set index` + (skipped > 0 ? ` (${skipped} skipped \u2014 missing or malformed)` : "")
|
|
25361
|
+
`[axl] RedisStore: backfilled ${migrated} ${index.label} entries into sorted-set index` + (skipped > 0 ? ` (${skipped} skipped \u2014 missing or malformed)` : "")
|
|
25106
25362
|
);
|
|
25107
25363
|
}
|
|
25108
25364
|
}
|
|
@@ -25228,6 +25484,51 @@ var RedisStore = class _RedisStore {
|
|
|
25228
25484
|
JSON.stringify(normalizePersistedSessionHistory(history)),
|
|
25229
25485
|
setOptions
|
|
25230
25486
|
).sAdd(this.sessionIdsKey(), sessionId).exec();
|
|
25487
|
+
await this.sweepSessionIds();
|
|
25488
|
+
}
|
|
25489
|
+
async updateSession(sessionId, update) {
|
|
25490
|
+
if (this.saveSession !== _RedisStore.prototype.saveSession || this.getSession !== _RedisStore.prototype.getSession) {
|
|
25491
|
+
return updateSessionThroughOverrides(this, "RedisStore", sessionId, update);
|
|
25492
|
+
}
|
|
25493
|
+
const key = this.sessionKey(sessionId);
|
|
25494
|
+
const ttl = this.ttlFor("session");
|
|
25495
|
+
for (let attempt = 0; attempt < UPDATE_SESSION_ATTEMPTS; attempt++) {
|
|
25496
|
+
const raw = await this.client.get(key);
|
|
25497
|
+
const next = update(raw ? JSON.parse(raw) : null);
|
|
25498
|
+
if (next === null) return;
|
|
25499
|
+
let written;
|
|
25500
|
+
try {
|
|
25501
|
+
written = await this.client.eval(UPDATE_SESSION_SCRIPT, {
|
|
25502
|
+
keys: [key, this.sessionIdsKey()],
|
|
25503
|
+
arguments: [
|
|
25504
|
+
raw === null ? "" : (0, import_node_crypto6.createHash)("sha1").update(raw).digest("hex"),
|
|
25505
|
+
JSON.stringify(normalizePersistedSessionHistory(next)),
|
|
25506
|
+
ttl === void 0 ? "" : String(ttl),
|
|
25507
|
+
sessionId
|
|
25508
|
+
]
|
|
25509
|
+
});
|
|
25510
|
+
} catch (err) {
|
|
25511
|
+
if (!errorMessage(err).includes("NOPERM")) throw err;
|
|
25512
|
+
warnDiagnosticOnce(
|
|
25513
|
+
"redis_update_session_denied",
|
|
25514
|
+
"RedisStore: saving a session turn safely runs a Lua script, which this Redis user may not run, so it was saved without that protection. Overlapping turns from different instances can overwrite each other until the user may run EVAL.",
|
|
25515
|
+
void 0,
|
|
25516
|
+
"docs/api-reference.md#concurrency-and-races"
|
|
25517
|
+
);
|
|
25518
|
+
await this.saveSession(sessionId, next);
|
|
25519
|
+
return;
|
|
25520
|
+
}
|
|
25521
|
+
if (Number(written) === 1) {
|
|
25522
|
+
await this.sweepSessionIds();
|
|
25523
|
+
return;
|
|
25524
|
+
}
|
|
25525
|
+
const backoffMs = Math.min(20 * 2 ** attempt, 200);
|
|
25526
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.random() * backoffMs));
|
|
25527
|
+
}
|
|
25528
|
+
throw new AxlError(
|
|
25529
|
+
"SESSION_CONFLICT",
|
|
25530
|
+
`RedisStore: gave up saving session "${sessionId}" after ${UPDATE_SESSION_ATTEMPTS} conflicting writes from other instances.`
|
|
25531
|
+
);
|
|
25231
25532
|
}
|
|
25232
25533
|
async getSession(sessionId) {
|
|
25233
25534
|
const raw = await this.client.get(this.sessionKey(sessionId));
|
|
@@ -25629,17 +25930,20 @@ var RedisStore = class _RedisStore {
|
|
|
25629
25930
|
try {
|
|
25630
25931
|
await this.pruneHistory(index);
|
|
25631
25932
|
} catch (err) {
|
|
25632
|
-
|
|
25633
|
-
warnDiagnosticOnce(
|
|
25634
|
-
`redis_history_prune_failed\0${index.kind}`,
|
|
25635
|
-
`RedisStore: removing expired ${index.label} index entries failed (${err instanceof Error ? err.message : String(err)}). The record was saved; the next save retries the cleanup. Listings stay correct meanwhile. The cleanup runs a Lua script, so the Redis user needs permission to run EVAL.`,
|
|
25636
|
-
void 0,
|
|
25637
|
-
"docs/api-reference.md#redisstore-options"
|
|
25638
|
-
);
|
|
25933
|
+
this.warnPruneFailed(index, err);
|
|
25639
25934
|
} finally {
|
|
25640
25935
|
inFlight.delete(index.kind);
|
|
25641
25936
|
}
|
|
25642
25937
|
}
|
|
25938
|
+
warnPruneFailed(index, err) {
|
|
25939
|
+
if (!this.isConnected) return;
|
|
25940
|
+
warnDiagnosticOnce(
|
|
25941
|
+
`redis_history_prune_failed\0${index.kind}`,
|
|
25942
|
+
`RedisStore: removing expired ${index.label} index entries failed (${err instanceof Error ? err.message : String(err)}). The cleanup is tried again later, and listings stay correct meanwhile. The cleanup runs a Lua script, so the Redis user needs permission to run EVAL.`,
|
|
25943
|
+
void 0,
|
|
25944
|
+
"docs/api-reference.md#redisstore-options"
|
|
25945
|
+
);
|
|
25946
|
+
}
|
|
25643
25947
|
async pruneHistory(index) {
|
|
25644
25948
|
const candidates = await this.client.zRange(index.expiry, "-inf", Date.now(), {
|
|
25645
25949
|
BY: "SCORE",
|
|
@@ -25794,7 +26098,69 @@ var RedisStore = class _RedisStore {
|
|
|
25794
26098
|
}
|
|
25795
26099
|
// ── Sessions (Studio introspection) ────────────────────────────────────
|
|
25796
26100
|
async listSessions() {
|
|
25797
|
-
|
|
26101
|
+
const live = /* @__PURE__ */ new Set();
|
|
26102
|
+
let cursor = "0";
|
|
26103
|
+
do {
|
|
26104
|
+
const page = await this.client.sScan(this.sessionIdsKey(), cursor, {
|
|
26105
|
+
COUNT: SESSION_ID_BATCH
|
|
26106
|
+
});
|
|
26107
|
+
cursor = page.cursor;
|
|
26108
|
+
const removed = new Set(await this.pruneSessionIds(page.members));
|
|
26109
|
+
for (const id of page.members) if (!removed.has(id)) live.add(id);
|
|
26110
|
+
} while (cursor !== "0");
|
|
26111
|
+
return [...live];
|
|
26112
|
+
}
|
|
26113
|
+
/**
|
|
26114
|
+
* Check a few listed sessions after a save, so ids of expired sessions go
|
|
26115
|
+
* away for apps that never list. Sessions without a TTL don't expire, so
|
|
26116
|
+
* there's nothing to do. The save has already succeeded, so failing here
|
|
26117
|
+
* can't fail it.
|
|
26118
|
+
*/
|
|
26119
|
+
async sweepSessionIds() {
|
|
26120
|
+
if (this.ttlFor("session") === void 0 || this.sessionSweepRunning) return;
|
|
26121
|
+
this.sessionSweepRunning = true;
|
|
26122
|
+
try {
|
|
26123
|
+
const page = await this.client.sScan(this.sessionIdsKey(), this.sessionSweepCursor, {
|
|
26124
|
+
COUNT: SESSION_SWEEP_COUNT
|
|
26125
|
+
});
|
|
26126
|
+
this.sessionSweepCursor = page.cursor;
|
|
26127
|
+
await this.pruneSessionIds(page.members);
|
|
26128
|
+
} catch (err) {
|
|
26129
|
+
this.warnSessionPruneFailed(err);
|
|
26130
|
+
} finally {
|
|
26131
|
+
this.sessionSweepRunning = false;
|
|
26132
|
+
}
|
|
26133
|
+
}
|
|
26134
|
+
/** Returns the ids removed. Without permission to run the script, removes none and warns. */
|
|
26135
|
+
async pruneSessionIds(ids) {
|
|
26136
|
+
const removed = [];
|
|
26137
|
+
for (let start = 0; start < ids.length; start += SESSION_ID_BATCH) {
|
|
26138
|
+
const batch = ids.slice(start, start + SESSION_ID_BATCH);
|
|
26139
|
+
try {
|
|
26140
|
+
const result = await this.client.eval(PRUNE_SESSION_IDS_SCRIPT, {
|
|
26141
|
+
keys: [
|
|
26142
|
+
this.sessionIdsKey(),
|
|
26143
|
+
...batch.flatMap((id) => [this.sessionKey(id), this.sessionMetaKey(id)])
|
|
26144
|
+
],
|
|
26145
|
+
arguments: batch
|
|
26146
|
+
});
|
|
26147
|
+
removed.push(...result);
|
|
26148
|
+
} catch (err) {
|
|
26149
|
+
if (!errorMessage(err).includes("NOPERM")) throw err;
|
|
26150
|
+
this.warnSessionPruneFailed(err);
|
|
26151
|
+
return removed;
|
|
26152
|
+
}
|
|
26153
|
+
}
|
|
26154
|
+
return removed;
|
|
26155
|
+
}
|
|
26156
|
+
warnSessionPruneFailed(err) {
|
|
26157
|
+
if (!this.isConnected) return;
|
|
26158
|
+
warnDiagnosticOnce(
|
|
26159
|
+
"redis_session_ids_prune_failed",
|
|
26160
|
+
`RedisStore: removing expired session ids failed (${errorMessage(err)}), so listSessions can return sessions that have expired. The cleanup runs a Lua script, so the Redis user needs permission to run EVAL.`,
|
|
26161
|
+
void 0,
|
|
26162
|
+
"docs/api-reference.md#redisstore-options"
|
|
26163
|
+
);
|
|
25798
26164
|
}
|
|
25799
26165
|
// ── Streaming events (for state.persist: 'streaming') ──────────────────
|
|
25800
26166
|
async appendStreamingEvents(executionId, events) {
|
|
@@ -25835,30 +26201,33 @@ var RedisStore = class _RedisStore {
|
|
|
25835
26201
|
* better-sqlite3's `close()` are both no-ops the second time; node-redis
|
|
25836
26202
|
* instead rejects with "The client is closed", so absorb that here.
|
|
25837
26203
|
*
|
|
25838
|
-
* Bounded, so a shutdown can't hang on Redis.
|
|
25839
|
-
*
|
|
25840
|
-
*
|
|
25841
|
-
*
|
|
26204
|
+
* Bounded, so a shutdown can't hang on Redis. Replies arrive in order, so a
|
|
26205
|
+
* reply to this PING means every earlier command has finished.
|
|
26206
|
+
*
|
|
26207
|
+
* Not `quit()`: it marks the client closed before the server answers, and
|
|
26208
|
+
* `destroy()` then throws without touching the socket, so a stalled server's
|
|
26209
|
+
* socket could never be closed. There's no other public way to close it. A
|
|
26210
|
+
* closed socket is all Redis needs to clean up, so nothing is lost.
|
|
25842
26211
|
*/
|
|
25843
26212
|
async close() {
|
|
26213
|
+
this.closing = true;
|
|
26214
|
+
this.stopResponseWatch?.();
|
|
25844
26215
|
if (this.connection.isOpen === false) return;
|
|
25845
|
-
if (this.connection.isReady
|
|
25846
|
-
|
|
25847
|
-
|
|
25848
|
-
|
|
25849
|
-
|
|
25850
|
-
|
|
25851
|
-
|
|
25852
|
-
|
|
25853
|
-
|
|
25854
|
-
|
|
25855
|
-
|
|
25856
|
-
|
|
25857
|
-
|
|
25858
|
-
abandonConnection(this.connection);
|
|
25859
|
-
} finally {
|
|
25860
|
-
clearTimeout(timer);
|
|
26216
|
+
if (this.connection.isReady !== false) {
|
|
26217
|
+
let timer;
|
|
26218
|
+
try {
|
|
26219
|
+
await Promise.race([
|
|
26220
|
+
this.connection.ping(),
|
|
26221
|
+
new Promise((_resolve, reject) => {
|
|
26222
|
+
timer = setTimeout(() => reject(new Error("PING timed out")), CLOSE_TIMEOUT_MS);
|
|
26223
|
+
})
|
|
26224
|
+
]);
|
|
26225
|
+
} catch {
|
|
26226
|
+
} finally {
|
|
26227
|
+
clearTimeout(timer);
|
|
26228
|
+
}
|
|
25861
26229
|
}
|
|
26230
|
+
abandonConnection(this.connection);
|
|
25862
26231
|
}
|
|
25863
26232
|
async deleteCheckpoints(executionId) {
|
|
25864
26233
|
await this.client.del(this.checkpointKey(executionId));
|