@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.js
CHANGED
|
@@ -348,7 +348,7 @@ var sharedGuard = typeof installedGuard === "object" && installedGuard !== null
|
|
|
348
348
|
var buildPath = typeof __AXL_BUILD_PATH__ === "string" ? __AXL_BUILD_PATH__ : "<source>";
|
|
349
349
|
var thisCopy = {
|
|
350
350
|
path: buildPath.startsWith("file:") ? fileURLToPath(buildPath) : buildPath,
|
|
351
|
-
version: true ? "0.27.
|
|
351
|
+
version: true ? "0.27.2" : "unknown"
|
|
352
352
|
};
|
|
353
353
|
function warnCrossCopy(other, joined) {
|
|
354
354
|
if (other === thisCopy) return;
|
|
@@ -18814,6 +18814,21 @@ function codePointRank(unit) {
|
|
|
18814
18814
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
18815
18815
|
import { join } from "path";
|
|
18816
18816
|
import { tmpdir } from "os";
|
|
18817
|
+
|
|
18818
|
+
// src/state/session-update.ts
|
|
18819
|
+
async function updateSessionThroughOverrides(store, storeName, sessionId, update) {
|
|
18820
|
+
warnDiagnosticOnce(
|
|
18821
|
+
`session_update_overridden\0${storeName}`,
|
|
18822
|
+
`${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.`,
|
|
18823
|
+
void 0,
|
|
18824
|
+
"docs/api-reference.md#several-app-instances-sharing-a-session"
|
|
18825
|
+
);
|
|
18826
|
+
const stored = await store.getSession(sessionId);
|
|
18827
|
+
const next = update(stored.length > 0 ? stored : null);
|
|
18828
|
+
if (next !== null) await store.saveSession(sessionId, next);
|
|
18829
|
+
}
|
|
18830
|
+
|
|
18831
|
+
// src/state/memory.ts
|
|
18817
18832
|
function compareOldestFirst(a, b) {
|
|
18818
18833
|
return compareExecutionsNewestFirst(b, a);
|
|
18819
18834
|
}
|
|
@@ -18830,7 +18845,7 @@ function resolveMaxExecutions(value) {
|
|
|
18830
18845
|
}
|
|
18831
18846
|
var AWAIT_HUMAN_TEMP_DIR = join(tmpdir(), "axl-memory-store");
|
|
18832
18847
|
var AWAIT_HUMAN_TEMP_FILE = join(AWAIT_HUMAN_TEMP_DIR, "await-human-state.json");
|
|
18833
|
-
var MemoryStore = class {
|
|
18848
|
+
var MemoryStore = class _MemoryStore {
|
|
18834
18849
|
checkpoints = /* @__PURE__ */ new Map();
|
|
18835
18850
|
sessions = /* @__PURE__ */ new Map();
|
|
18836
18851
|
sessionMeta = /* @__PURE__ */ new Map();
|
|
@@ -18882,6 +18897,14 @@ var MemoryStore = class {
|
|
|
18882
18897
|
async saveSession(sessionId, history) {
|
|
18883
18898
|
this.sessions.set(sessionId, structuredClone(normalizePersistedSessionHistory(history)));
|
|
18884
18899
|
}
|
|
18900
|
+
async updateSession(sessionId, update) {
|
|
18901
|
+
if (this.saveSession !== _MemoryStore.prototype.saveSession || this.getSession !== _MemoryStore.prototype.getSession) {
|
|
18902
|
+
return updateSessionThroughOverrides(this, "MemoryStore", sessionId, update);
|
|
18903
|
+
}
|
|
18904
|
+
const stored = this.sessions.get(sessionId);
|
|
18905
|
+
const next = update(stored ? structuredClone(stored) : null);
|
|
18906
|
+
if (next !== null) await this.saveSession(sessionId, next);
|
|
18907
|
+
}
|
|
18885
18908
|
async getSession(sessionId) {
|
|
18886
18909
|
const history = this.sessions.get(sessionId);
|
|
18887
18910
|
return history ? structuredClone(history) : [];
|
|
@@ -19305,7 +19328,7 @@ function rowToExecutionInfo(row) {
|
|
|
19305
19328
|
}
|
|
19306
19329
|
return normalizeStoredExecution(info);
|
|
19307
19330
|
}
|
|
19308
|
-
var SQLiteStore = class {
|
|
19331
|
+
var SQLiteStore = class _SQLiteStore {
|
|
19309
19332
|
db;
|
|
19310
19333
|
ttls;
|
|
19311
19334
|
now;
|
|
@@ -19539,6 +19562,17 @@ var SQLiteStore = class {
|
|
|
19539
19562
|
);
|
|
19540
19563
|
stmt.run(sessionId, JSON.stringify(normalizePersistedSessionHistory(history)));
|
|
19541
19564
|
}
|
|
19565
|
+
async updateSession(sessionId, update) {
|
|
19566
|
+
if (this.saveSession !== _SQLiteStore.prototype.saveSession || this.getSession !== _SQLiteStore.prototype.getSession) {
|
|
19567
|
+
return updateSessionThroughOverrides(this, "SQLiteStore", sessionId, update);
|
|
19568
|
+
}
|
|
19569
|
+
this.db.transaction(() => {
|
|
19570
|
+
const row = this.db.prepare("SELECT history FROM sessions WHERE session_id = ?").get(sessionId);
|
|
19571
|
+
const next = update(row ? safeJsonParse(row.history) ?? [] : null);
|
|
19572
|
+
if (next === null) return;
|
|
19573
|
+
this.db.prepare("INSERT OR REPLACE INTO sessions (session_id, history) VALUES (?, ?)").run(sessionId, JSON.stringify(normalizePersistedSessionHistory(next)));
|
|
19574
|
+
}).immediate();
|
|
19575
|
+
}
|
|
19542
19576
|
async getSession(sessionId) {
|
|
19543
19577
|
const stmt = this.db.prepare("SELECT history FROM sessions WHERE session_id = ?");
|
|
19544
19578
|
const row = stmt.get(sessionId);
|
|
@@ -19873,10 +19907,10 @@ var Session = class _Session {
|
|
|
19873
19907
|
);
|
|
19874
19908
|
}
|
|
19875
19909
|
async sendImpl(workflowName, input, signal, events) {
|
|
19876
|
-
const { history, metadata } = await this.prepareHistory(input);
|
|
19910
|
+
const { history, metadata, base } = await this.prepareHistory(input);
|
|
19877
19911
|
try {
|
|
19878
19912
|
const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
|
|
19879
|
-
await this.commitHistory(history, result);
|
|
19913
|
+
await this.commitHistory(history, base, result);
|
|
19880
19914
|
return result;
|
|
19881
19915
|
} finally {
|
|
19882
19916
|
clearSessionInput(history);
|
|
@@ -19892,6 +19926,8 @@ var Session = class _Session {
|
|
|
19892
19926
|
async prepareHistory(input) {
|
|
19893
19927
|
const preparedInput = prepareSessionInput(input);
|
|
19894
19928
|
const history = await this.store.getSession(this.sessionId);
|
|
19929
|
+
const storedJson = JSON.stringify(history);
|
|
19930
|
+
const hadHistory = history.length > 0;
|
|
19895
19931
|
let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
|
|
19896
19932
|
const maxMessages = this.options.history?.maxMessages;
|
|
19897
19933
|
if (maxMessages && history.length > maxMessages) {
|
|
@@ -19906,13 +19942,17 @@ var Session = class _Session {
|
|
|
19906
19942
|
summaryModel,
|
|
19907
19943
|
cachedSummary ? { previousSummary: cachedSummary } : void 0
|
|
19908
19944
|
);
|
|
19909
|
-
await this.store.
|
|
19945
|
+
const stored = JSON.stringify(await this.store.getSession(this.sessionId));
|
|
19946
|
+
if (stored === storedJson) {
|
|
19947
|
+
await this.store.saveSessionMeta(this.sessionId, "summaryCache", summary);
|
|
19948
|
+
}
|
|
19910
19949
|
cachedSummary = summary;
|
|
19911
19950
|
}
|
|
19912
19951
|
const trimmed = history.slice(-maxMessages);
|
|
19913
19952
|
history.length = 0;
|
|
19914
19953
|
history.push(...trimmed.map(withoutAnthropicThinking));
|
|
19915
19954
|
}
|
|
19955
|
+
const base = { storedJson, hadHistory, firstOwnMessage: history.length };
|
|
19916
19956
|
const userMessage = { role: "user", content: preparedInput.content };
|
|
19917
19957
|
history.push(userMessage);
|
|
19918
19958
|
if (this.options.deduplicateInput !== false && preparedInput.normalizedInput !== void 0) {
|
|
@@ -19926,13 +19966,13 @@ var Session = class _Session {
|
|
|
19926
19966
|
// agent-scoped ask-summary records the runtime would otherwise cache.
|
|
19927
19967
|
...this.options.persist === false ? { sessionPersist: false } : {}
|
|
19928
19968
|
};
|
|
19929
|
-
return { history, metadata };
|
|
19969
|
+
return { history, metadata, base };
|
|
19930
19970
|
}
|
|
19931
19971
|
/** Push the assistant reply (if `executeAgentCall` didn't already) and
|
|
19932
19972
|
* persist. Shared by `sendImpl` and `streamImpl`. The fallback assistant
|
|
19933
19973
|
* push has no agent context and so leaves `agent` undefined — see the
|
|
19934
19974
|
* `ChatMessage.agent` field doc. */
|
|
19935
|
-
async commitHistory(history, result) {
|
|
19975
|
+
async commitHistory(history, base, result) {
|
|
19936
19976
|
const lastMsg = history[history.length - 1];
|
|
19937
19977
|
if (!(lastMsg && lastMsg.role === "assistant")) {
|
|
19938
19978
|
history.push({
|
|
@@ -19940,8 +19980,35 @@ var Session = class _Session {
|
|
|
19940
19980
|
content: typeof result === "string" ? result : JSON.stringify(result)
|
|
19941
19981
|
});
|
|
19942
19982
|
}
|
|
19943
|
-
if (this.options.persist
|
|
19983
|
+
if (this.options.persist === false) return;
|
|
19984
|
+
if (!this.store.updateSession) {
|
|
19944
19985
|
await this.store.saveSession(this.sessionId, history);
|
|
19986
|
+
return;
|
|
19987
|
+
}
|
|
19988
|
+
let outcome = "saved";
|
|
19989
|
+
await this.store.updateSession(this.sessionId, (current) => {
|
|
19990
|
+
if (current === null) {
|
|
19991
|
+
outcome = base.hadHistory ? "discarded" : "saved";
|
|
19992
|
+
return base.hadHistory ? null : history;
|
|
19993
|
+
}
|
|
19994
|
+
if (JSON.stringify(current) === base.storedJson) {
|
|
19995
|
+
outcome = "saved";
|
|
19996
|
+
return history;
|
|
19997
|
+
}
|
|
19998
|
+
outcome = "merged";
|
|
19999
|
+
return [...current, ...history.slice(base.firstOwnMessage).map(withoutAnthropicThinking)];
|
|
20000
|
+
});
|
|
20001
|
+
if (outcome === "merged") this.notify("session_turns_merged");
|
|
20002
|
+
else if (outcome === "discarded") this.notify("session_turn_discarded");
|
|
20003
|
+
}
|
|
20004
|
+
/** The turn is already saved, so a listener that throws can't be allowed to fail it. */
|
|
20005
|
+
notify(event) {
|
|
20006
|
+
try {
|
|
20007
|
+
this.runtime.emit(event, { sessionId: this.sessionId });
|
|
20008
|
+
} catch (err) {
|
|
20009
|
+
console.error(
|
|
20010
|
+
`[axl] A ${event} listener failed: ${err instanceof Error ? err.message : String(err)}`
|
|
20011
|
+
);
|
|
19945
20012
|
}
|
|
19946
20013
|
}
|
|
19947
20014
|
async stream(workflowName, input, options) {
|
|
@@ -19969,10 +20036,12 @@ var Session = class _Session {
|
|
|
19969
20036
|
}
|
|
19970
20037
|
async streamImpl(workflowName, input, resolveReady, rejectReady, signal, events) {
|
|
19971
20038
|
let history;
|
|
20039
|
+
let base;
|
|
19972
20040
|
let axlStream;
|
|
19973
20041
|
try {
|
|
19974
20042
|
const prepared = await this.prepareHistory(input);
|
|
19975
20043
|
history = prepared.history;
|
|
20044
|
+
base = prepared.base;
|
|
19976
20045
|
axlStream = this.runtime.stream(workflowName, input, {
|
|
19977
20046
|
metadata: prepared.metadata,
|
|
19978
20047
|
signal,
|
|
@@ -19986,7 +20055,7 @@ var Session = class _Session {
|
|
|
19986
20055
|
resolveReady(axlStream);
|
|
19987
20056
|
try {
|
|
19988
20057
|
const completion = axlStream.promise.then(
|
|
19989
|
-
(result) => this.commitHistory(history, result).catch((err) => {
|
|
20058
|
+
(result) => this.commitHistory(history, base, result).catch((err) => {
|
|
19990
20059
|
const error = err instanceof Error ? err.message : String(err);
|
|
19991
20060
|
try {
|
|
19992
20061
|
this.runtime.emit("error", {
|
|
@@ -24545,6 +24614,7 @@ async function* stringStreamFromEvents(source, opts) {
|
|
|
24545
24614
|
}
|
|
24546
24615
|
|
|
24547
24616
|
// src/state/redis.ts
|
|
24617
|
+
import { createHash as createHash2 } from "crypto";
|
|
24548
24618
|
var PRUNE_HISTORY_INDEX_SCRIPT = `
|
|
24549
24619
|
local now = tonumber(ARGV[1])
|
|
24550
24620
|
local removed = 0
|
|
@@ -24564,6 +24634,34 @@ for i = 2, #ARGV do
|
|
|
24564
24634
|
end
|
|
24565
24635
|
return removed
|
|
24566
24636
|
`;
|
|
24637
|
+
var PRUNE_SESSION_IDS_SCRIPT = `
|
|
24638
|
+
local removed = {}
|
|
24639
|
+
for i, id in ipairs(ARGV) do
|
|
24640
|
+
if redis.call('EXISTS', KEYS[i * 2]) == 0 then
|
|
24641
|
+
redis.call('SREM', KEYS[1], id)
|
|
24642
|
+
redis.call('DEL', KEYS[i * 2 + 1])
|
|
24643
|
+
removed[#removed + 1] = id
|
|
24644
|
+
end
|
|
24645
|
+
end
|
|
24646
|
+
return removed
|
|
24647
|
+
`;
|
|
24648
|
+
var SESSION_ID_BATCH = 100;
|
|
24649
|
+
var SESSION_SWEEP_COUNT = 50;
|
|
24650
|
+
var UPDATE_SESSION_SCRIPT = `
|
|
24651
|
+
local stored = redis.call('GET', KEYS[1])
|
|
24652
|
+
local actual = stored and redis.sha1hex(stored) or ''
|
|
24653
|
+
if actual ~= ARGV[1] then
|
|
24654
|
+
return 0
|
|
24655
|
+
end
|
|
24656
|
+
if ARGV[3] ~= '' then
|
|
24657
|
+
redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
|
|
24658
|
+
else
|
|
24659
|
+
redis.call('SET', KEYS[1], ARGV[2])
|
|
24660
|
+
end
|
|
24661
|
+
redis.call('SADD', KEYS[2], ARGV[4])
|
|
24662
|
+
return 1
|
|
24663
|
+
`;
|
|
24664
|
+
var UPDATE_SESSION_ATTEMPTS = 10;
|
|
24567
24665
|
function compareMembers(a, b) {
|
|
24568
24666
|
return Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
24569
24667
|
}
|
|
@@ -24585,7 +24683,6 @@ function abandonConnection(client) {
|
|
|
24585
24683
|
client.destroy?.();
|
|
24586
24684
|
} catch {
|
|
24587
24685
|
}
|
|
24588
|
-
client.unref?.();
|
|
24589
24686
|
}
|
|
24590
24687
|
function errorMessage(error) {
|
|
24591
24688
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -24614,18 +24711,106 @@ function connectionEventReporter(client, onConnectionEvent) {
|
|
|
24614
24711
|
}
|
|
24615
24712
|
};
|
|
24616
24713
|
}
|
|
24714
|
+
var LOOP_STALL_TOLERANCE_MS = 100;
|
|
24715
|
+
var LOOP_STALL_GRACE_MS = 1e3;
|
|
24716
|
+
function watchResponses(client, timeoutMs, reportConnectionEvent) {
|
|
24717
|
+
const checkEveryMs = timeoutMs / 3;
|
|
24718
|
+
let timer;
|
|
24719
|
+
let stopped = false;
|
|
24720
|
+
const schedule = (run, delayMs) => {
|
|
24721
|
+
timer = setTimeout(run, delayMs);
|
|
24722
|
+
timer.unref?.();
|
|
24723
|
+
};
|
|
24724
|
+
const check = () => {
|
|
24725
|
+
if (client.isReady === false) {
|
|
24726
|
+
schedule(check, checkEveryMs);
|
|
24727
|
+
return;
|
|
24728
|
+
}
|
|
24729
|
+
let settled = false;
|
|
24730
|
+
const onReply = () => {
|
|
24731
|
+
if (settled || stopped) return;
|
|
24732
|
+
settled = true;
|
|
24733
|
+
clearTimeout(timer);
|
|
24734
|
+
schedule(check, checkEveryMs);
|
|
24735
|
+
};
|
|
24736
|
+
const expireAfter = (delayMs, mayWaitForBlockedLoop) => {
|
|
24737
|
+
const dueAt = Date.now() + delayMs;
|
|
24738
|
+
schedule(() => {
|
|
24739
|
+
if (settled || stopped) return;
|
|
24740
|
+
if (mayWaitForBlockedLoop && Date.now() - dueAt > LOOP_STALL_TOLERANCE_MS) {
|
|
24741
|
+
expireAfter(LOOP_STALL_GRACE_MS, false);
|
|
24742
|
+
return;
|
|
24743
|
+
}
|
|
24744
|
+
reset();
|
|
24745
|
+
}, delayMs);
|
|
24746
|
+
};
|
|
24747
|
+
const reset = () => {
|
|
24748
|
+
settled = true;
|
|
24749
|
+
if (client.isReady === false) {
|
|
24750
|
+
schedule(check, checkEveryMs);
|
|
24751
|
+
return;
|
|
24752
|
+
}
|
|
24753
|
+
client.destroy?.();
|
|
24754
|
+
client.connect().catch(() => {
|
|
24755
|
+
});
|
|
24756
|
+
reportConnectionEvent({
|
|
24757
|
+
type: "error",
|
|
24758
|
+
error: new Error(`Redis did not answer within ${timeoutMs}ms; reconnecting.`)
|
|
24759
|
+
});
|
|
24760
|
+
schedule(check, checkEveryMs);
|
|
24761
|
+
};
|
|
24762
|
+
client.ping().then(onReply, onReply);
|
|
24763
|
+
expireAfter(timeoutMs, true);
|
|
24764
|
+
};
|
|
24765
|
+
schedule(check, checkEveryMs);
|
|
24766
|
+
return () => {
|
|
24767
|
+
stopped = true;
|
|
24768
|
+
clearTimeout(timer);
|
|
24769
|
+
};
|
|
24770
|
+
}
|
|
24771
|
+
async function connectWithin(connecting, timeoutMs) {
|
|
24772
|
+
let timer;
|
|
24773
|
+
try {
|
|
24774
|
+
await Promise.race([
|
|
24775
|
+
connecting,
|
|
24776
|
+
new Promise((_resolve, reject) => {
|
|
24777
|
+
timer = setTimeout(
|
|
24778
|
+
() => reject(
|
|
24779
|
+
new AxlError(
|
|
24780
|
+
"REDIS_UNAVAILABLE",
|
|
24781
|
+
`RedisStore: Redis did not finish connecting within ${timeoutMs}ms.`
|
|
24782
|
+
)
|
|
24783
|
+
),
|
|
24784
|
+
timeoutMs
|
|
24785
|
+
);
|
|
24786
|
+
})
|
|
24787
|
+
]);
|
|
24788
|
+
} finally {
|
|
24789
|
+
clearTimeout(timer);
|
|
24790
|
+
}
|
|
24791
|
+
}
|
|
24617
24792
|
async function connectClient(createClient, opts) {
|
|
24618
|
-
|
|
24793
|
+
const inBackground = opts.waitForConnection === false;
|
|
24794
|
+
let established = inBackground;
|
|
24795
|
+
let everFailed = false;
|
|
24796
|
+
const responseTimeoutMs = opts.responseTimeoutMs;
|
|
24619
24797
|
const client = createClient({
|
|
24620
24798
|
...opts.url ? { url: opts.url } : {},
|
|
24621
24799
|
// A command accepted just before a drop rejects instead of waiting for
|
|
24622
24800
|
// the reconnect.
|
|
24623
24801
|
disableOfflineQueue: true,
|
|
24624
|
-
socket: {
|
|
24802
|
+
socket: {
|
|
24803
|
+
reconnectStrategy: (retries) => established ? reconnectDelayMs(retries) : false,
|
|
24804
|
+
// Bounds a reconnect's handshake, where the response watch doesn't run.
|
|
24805
|
+
// Twice the timeout so that on an open connection the watch acts first:
|
|
24806
|
+
// this timer fires after any long event-loop block, which the watch allows for.
|
|
24807
|
+
...responseTimeoutMs !== void 0 ? { socketTimeout: responseTimeoutMs * 2 } : {}
|
|
24808
|
+
}
|
|
24625
24809
|
});
|
|
24626
24810
|
const reportConnectionEvent = connectionEventReporter(client, opts.onConnectionEvent);
|
|
24627
24811
|
client.on("error", (error) => {
|
|
24628
24812
|
if (!established || client.isOpen === false) return;
|
|
24813
|
+
everFailed = true;
|
|
24629
24814
|
reportConnectionEvent({
|
|
24630
24815
|
type: "error",
|
|
24631
24816
|
error: error instanceof Error ? error : new Error(String(error))
|
|
@@ -24637,12 +24822,32 @@ async function connectClient(createClient, opts) {
|
|
|
24637
24822
|
client.on("ready", () => {
|
|
24638
24823
|
if (!established) {
|
|
24639
24824
|
established = true;
|
|
24640
|
-
} else if (client.isOpen !== false) {
|
|
24825
|
+
} else if (client.isOpen !== false && (!inBackground || everFailed)) {
|
|
24641
24826
|
reportConnectionEvent({ type: "reconnected" });
|
|
24642
24827
|
}
|
|
24643
24828
|
});
|
|
24644
|
-
|
|
24645
|
-
|
|
24829
|
+
if (inBackground) {
|
|
24830
|
+
client.connect().catch(() => {
|
|
24831
|
+
});
|
|
24832
|
+
return {
|
|
24833
|
+
client,
|
|
24834
|
+
stopResponseWatch: watchResponses(client, responseTimeoutMs, reportConnectionEvent)
|
|
24835
|
+
};
|
|
24836
|
+
}
|
|
24837
|
+
if (responseTimeoutMs === void 0) {
|
|
24838
|
+
await client.connect();
|
|
24839
|
+
return { client };
|
|
24840
|
+
}
|
|
24841
|
+
try {
|
|
24842
|
+
await connectWithin(client.connect(), responseTimeoutMs);
|
|
24843
|
+
} catch (error) {
|
|
24844
|
+
abandonConnection(client);
|
|
24845
|
+
throw error;
|
|
24846
|
+
}
|
|
24847
|
+
return {
|
|
24848
|
+
client,
|
|
24849
|
+
stopResponseWatch: watchResponses(client, responseTimeoutMs, reportConnectionEvent)
|
|
24850
|
+
};
|
|
24646
24851
|
}
|
|
24647
24852
|
var RedisStore = class _RedisStore {
|
|
24648
24853
|
constructor(connection, keyPrefix, ttls) {
|
|
@@ -24650,13 +24855,19 @@ var RedisStore = class _RedisStore {
|
|
|
24650
24855
|
this.keyPrefix = keyPrefix;
|
|
24651
24856
|
this.ttls = ttls;
|
|
24652
24857
|
}
|
|
24858
|
+
/** Set when `close()` starts, while the connection is still open. */
|
|
24859
|
+
closing = false;
|
|
24860
|
+
/** Where the next save's check of listed sessions picks up. */
|
|
24861
|
+
sessionSweepCursor = "0";
|
|
24862
|
+
/** Overlapping saves would all check the same page, so only one checks at a time. */
|
|
24863
|
+
sessionSweepRunning = false;
|
|
24653
24864
|
/**
|
|
24654
24865
|
* Whether the store can send commands right now, for a health check or for
|
|
24655
24866
|
* telling an outage from other failures when a call rejects. `false` while
|
|
24656
24867
|
* reconnecting and after `close()`.
|
|
24657
24868
|
*/
|
|
24658
24869
|
get isConnected() {
|
|
24659
|
-
return this.connection.isOpen !== false && this.connection.isReady !== false;
|
|
24870
|
+
return !this.closing && this.connection.isOpen !== false && this.connection.isReady !== false;
|
|
24660
24871
|
}
|
|
24661
24872
|
/**
|
|
24662
24873
|
* The client for sending a command, failing immediately while disconnected.
|
|
@@ -24670,6 +24881,7 @@ var RedisStore = class _RedisStore {
|
|
|
24670
24881
|
* "The client is closed" rejection.
|
|
24671
24882
|
*/
|
|
24672
24883
|
get client() {
|
|
24884
|
+
if (this.closing) throw new Error("The client is closed");
|
|
24673
24885
|
if (this.connection.isOpen !== false && this.connection.isReady === false) {
|
|
24674
24886
|
throw new AxlError(
|
|
24675
24887
|
"REDIS_UNAVAILABLE",
|
|
@@ -24685,6 +24897,8 @@ var RedisStore = class _RedisStore {
|
|
|
24685
24897
|
* instance needs no constructor state.
|
|
24686
24898
|
*/
|
|
24687
24899
|
prunesInFlight;
|
|
24900
|
+
/** Stops the `responseTimeoutMs` health checks; unset when the option is off. */
|
|
24901
|
+
stopResponseWatch;
|
|
24688
24902
|
/**
|
|
24689
24903
|
* Supports `listExecutions(limit, { cursor })`. A getter rather than a
|
|
24690
24904
|
* field so it lives on the prototype.
|
|
@@ -24734,6 +24948,17 @@ var RedisStore = class _RedisStore {
|
|
|
24734
24948
|
'RedisStore: keyPrefix cannot be empty string. Omit it for the default "axl:" prefix.'
|
|
24735
24949
|
);
|
|
24736
24950
|
}
|
|
24951
|
+
const responseTimeoutMs = opts.responseTimeoutMs;
|
|
24952
|
+
if (responseTimeoutMs !== void 0 && !(Number.isFinite(responseTimeoutMs) && responseTimeoutMs > 0)) {
|
|
24953
|
+
throw new Error(
|
|
24954
|
+
`RedisStore: responseTimeoutMs must be a positive number of milliseconds, got ${responseTimeoutMs}.`
|
|
24955
|
+
);
|
|
24956
|
+
}
|
|
24957
|
+
if (opts.waitForConnection === false && responseTimeoutMs === void 0) {
|
|
24958
|
+
throw new Error(
|
|
24959
|
+
"RedisStore: waitForConnection: false requires responseTimeoutMs, so a server that accepts the connection but never answers cannot leave it connecting forever."
|
|
24960
|
+
);
|
|
24961
|
+
}
|
|
24737
24962
|
let createClient;
|
|
24738
24963
|
try {
|
|
24739
24964
|
const mod = __require("redis");
|
|
@@ -24747,7 +24972,7 @@ var RedisStore = class _RedisStore {
|
|
|
24747
24972
|
if (err instanceof Error && err.message.includes("createClient")) throw err;
|
|
24748
24973
|
throw new Error("redis is required for RedisStore. Install it with: npm install redis");
|
|
24749
24974
|
}
|
|
24750
|
-
const client = await connectClient(createClient, opts);
|
|
24975
|
+
const { client, stopResponseWatch } = await connectClient(createClient, opts);
|
|
24751
24976
|
const resolveCategory = (override) => {
|
|
24752
24977
|
if (override === null) return null;
|
|
24753
24978
|
if (typeof override === "number") return override;
|
|
@@ -24769,18 +24994,48 @@ var RedisStore = class _RedisStore {
|
|
|
24769
24994
|
streamingEvents: resolveStreamingEvents(opts.ttls?.streamingEvents)
|
|
24770
24995
|
};
|
|
24771
24996
|
const store = new _RedisStore(client, keyPrefix, effectiveTtls);
|
|
24997
|
+
store.stopResponseWatch = stopResponseWatch;
|
|
24772
24998
|
warnIfRedisExecutionRetentionUnset(opts);
|
|
24999
|
+
if (opts.waitForConnection === false) {
|
|
25000
|
+
if (!opts.skipMigration) store.backfillOnceConnected(client);
|
|
25001
|
+
return store;
|
|
25002
|
+
}
|
|
24773
25003
|
if (!opts.skipMigration) {
|
|
24774
25004
|
try {
|
|
24775
25005
|
await store.backfillExecutionIndex();
|
|
24776
25006
|
await store.backfillEvalIndex();
|
|
24777
25007
|
} catch (error) {
|
|
25008
|
+
stopResponseWatch?.();
|
|
24778
25009
|
abandonConnection(client);
|
|
24779
25010
|
throw error;
|
|
24780
25011
|
}
|
|
24781
25012
|
}
|
|
24782
25013
|
return store;
|
|
24783
25014
|
}
|
|
25015
|
+
/**
|
|
25016
|
+
* Run the startup backfill when a connection becomes ready, until it succeeds.
|
|
25017
|
+
* Nothing is waiting on it, and listings work without it, so a failure is
|
|
25018
|
+
* reported and the next connection tries again.
|
|
25019
|
+
*/
|
|
25020
|
+
backfillOnceConnected(client) {
|
|
25021
|
+
let state = "waiting";
|
|
25022
|
+
client.on("ready", () => {
|
|
25023
|
+
if (state !== "waiting") return;
|
|
25024
|
+
state = "running";
|
|
25025
|
+
this.backfillExecutionIndex().then(() => this.backfillEvalIndex()).then(
|
|
25026
|
+
() => {
|
|
25027
|
+
state = "done";
|
|
25028
|
+
},
|
|
25029
|
+
(err) => {
|
|
25030
|
+
state = "waiting";
|
|
25031
|
+
if (client.isReady === false || client.isOpen === false) return;
|
|
25032
|
+
console.warn(
|
|
25033
|
+
`[axl] RedisStore: the startup index backfill failed (${errorMessage(err)}). Listings stay correct meanwhile.`
|
|
25034
|
+
);
|
|
25035
|
+
}
|
|
25036
|
+
);
|
|
25037
|
+
});
|
|
25038
|
+
}
|
|
24784
25039
|
/**
|
|
24785
25040
|
* Backfill the execution-history sorted-set from the legacy ID set.
|
|
24786
25041
|
* Idempotent — runs only if the ZSET is missing entries the SET has.
|
|
@@ -24788,36 +25043,30 @@ var RedisStore = class _RedisStore {
|
|
|
24788
25043
|
* `skipMigration: true`.
|
|
24789
25044
|
*/
|
|
24790
25045
|
async backfillExecutionIndex() {
|
|
24791
|
-
await this.backfillHistoryIndex(
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
(raw) => {
|
|
24796
|
-
const exec = JSON.parse(raw);
|
|
24797
|
-
return typeof exec.startedAt === "number" ? { score: exec.startedAt, value: exec.executionId } : null;
|
|
24798
|
-
},
|
|
24799
|
-
"execution-history"
|
|
24800
|
-
);
|
|
25046
|
+
await this.backfillHistoryIndex(this.historyIndex("execution"), (raw) => {
|
|
25047
|
+
const exec = JSON.parse(raw);
|
|
25048
|
+
return typeof exec.startedAt === "number" ? { score: exec.startedAt, value: exec.executionId } : null;
|
|
25049
|
+
});
|
|
24801
25050
|
}
|
|
24802
25051
|
/** Backfill the eval-history sorted-set. Symmetric to `backfillExecutionIndex`. */
|
|
24803
25052
|
async backfillEvalIndex() {
|
|
24804
|
-
await this.backfillHistoryIndex(
|
|
24805
|
-
|
|
24806
|
-
|
|
24807
|
-
|
|
24808
|
-
(raw) => {
|
|
24809
|
-
const entry = JSON.parse(raw);
|
|
24810
|
-
return typeof entry.timestamp === "number" ? { score: entry.timestamp, value: entry.id } : null;
|
|
24811
|
-
},
|
|
24812
|
-
"eval-history"
|
|
24813
|
-
);
|
|
25053
|
+
await this.backfillHistoryIndex(this.historyIndex("eval"), (raw) => {
|
|
25054
|
+
const entry = JSON.parse(raw);
|
|
25055
|
+
return typeof entry.timestamp === "number" ? { score: entry.timestamp, value: entry.id } : null;
|
|
25056
|
+
});
|
|
24814
25057
|
}
|
|
24815
25058
|
/**
|
|
24816
25059
|
* Shared implementation for both backfill paths. Reads SET members,
|
|
24817
25060
|
* batch-MGETs their data blobs in chunks, and ZADDs each into the new
|
|
24818
25061
|
* sorted-set inside a single MULTI per chunk.
|
|
25062
|
+
*
|
|
25063
|
+
* Ids with no data blob are removed from the id set. Left there, the set stays
|
|
25064
|
+
* larger than the sorted set, so every startup repeats this walk and listing
|
|
25065
|
+
* stays on its slow path.
|
|
24819
25066
|
*/
|
|
24820
|
-
async backfillHistoryIndex(
|
|
25067
|
+
async backfillHistoryIndex(index, scoreOf) {
|
|
25068
|
+
const setKey = index.ids;
|
|
25069
|
+
const zsetKey = index.time;
|
|
24821
25070
|
const [setSize, zsetSize] = await Promise.all([
|
|
24822
25071
|
this.client.sCard(setKey),
|
|
24823
25072
|
this.client.zCard(zsetKey)
|
|
@@ -24828,9 +25077,10 @@ var RedisStore = class _RedisStore {
|
|
|
24828
25077
|
const CHUNK_SIZE = 500;
|
|
24829
25078
|
let migrated = 0;
|
|
24830
25079
|
let skipped = 0;
|
|
25080
|
+
const missing = [];
|
|
24831
25081
|
for (let i = 0; i < allIds.length; i += CHUNK_SIZE) {
|
|
24832
25082
|
const chunk = allIds.slice(i, i + CHUNK_SIZE);
|
|
24833
|
-
const dataKeys = chunk.map((id) => dataKey(id));
|
|
25083
|
+
const dataKeys = chunk.map((id) => index.dataKey(id));
|
|
24834
25084
|
const raws = await this.client.mGet(dataKeys);
|
|
24835
25085
|
const tx = this.client.multi();
|
|
24836
25086
|
let queuedInChunk = 0;
|
|
@@ -24838,6 +25088,7 @@ var RedisStore = class _RedisStore {
|
|
|
24838
25088
|
const raw = raws[j];
|
|
24839
25089
|
if (raw == null) {
|
|
24840
25090
|
skipped++;
|
|
25091
|
+
missing.push(chunk[j]);
|
|
24841
25092
|
continue;
|
|
24842
25093
|
}
|
|
24843
25094
|
try {
|
|
@@ -24855,9 +25106,14 @@ var RedisStore = class _RedisStore {
|
|
|
24855
25106
|
}
|
|
24856
25107
|
if (queuedInChunk > 0) await tx.exec();
|
|
24857
25108
|
}
|
|
25109
|
+
if (missing.length > 0) {
|
|
25110
|
+
await this.runPruneScript(index, missing).catch(
|
|
25111
|
+
(err) => this.warnPruneFailed(index, err)
|
|
25112
|
+
);
|
|
25113
|
+
}
|
|
24858
25114
|
if (migrated > 0 || skipped > 0) {
|
|
24859
25115
|
console.log(
|
|
24860
|
-
`[axl] RedisStore: backfilled ${migrated} ${label} entries into sorted-set index` + (skipped > 0 ? ` (${skipped} skipped \u2014 missing or malformed)` : "")
|
|
25116
|
+
`[axl] RedisStore: backfilled ${migrated} ${index.label} entries into sorted-set index` + (skipped > 0 ? ` (${skipped} skipped \u2014 missing or malformed)` : "")
|
|
24861
25117
|
);
|
|
24862
25118
|
}
|
|
24863
25119
|
}
|
|
@@ -24983,6 +25239,51 @@ var RedisStore = class _RedisStore {
|
|
|
24983
25239
|
JSON.stringify(normalizePersistedSessionHistory(history)),
|
|
24984
25240
|
setOptions
|
|
24985
25241
|
).sAdd(this.sessionIdsKey(), sessionId).exec();
|
|
25242
|
+
await this.sweepSessionIds();
|
|
25243
|
+
}
|
|
25244
|
+
async updateSession(sessionId, update) {
|
|
25245
|
+
if (this.saveSession !== _RedisStore.prototype.saveSession || this.getSession !== _RedisStore.prototype.getSession) {
|
|
25246
|
+
return updateSessionThroughOverrides(this, "RedisStore", sessionId, update);
|
|
25247
|
+
}
|
|
25248
|
+
const key = this.sessionKey(sessionId);
|
|
25249
|
+
const ttl = this.ttlFor("session");
|
|
25250
|
+
for (let attempt = 0; attempt < UPDATE_SESSION_ATTEMPTS; attempt++) {
|
|
25251
|
+
const raw = await this.client.get(key);
|
|
25252
|
+
const next = update(raw ? JSON.parse(raw) : null);
|
|
25253
|
+
if (next === null) return;
|
|
25254
|
+
let written;
|
|
25255
|
+
try {
|
|
25256
|
+
written = await this.client.eval(UPDATE_SESSION_SCRIPT, {
|
|
25257
|
+
keys: [key, this.sessionIdsKey()],
|
|
25258
|
+
arguments: [
|
|
25259
|
+
raw === null ? "" : createHash2("sha1").update(raw).digest("hex"),
|
|
25260
|
+
JSON.stringify(normalizePersistedSessionHistory(next)),
|
|
25261
|
+
ttl === void 0 ? "" : String(ttl),
|
|
25262
|
+
sessionId
|
|
25263
|
+
]
|
|
25264
|
+
});
|
|
25265
|
+
} catch (err) {
|
|
25266
|
+
if (!errorMessage(err).includes("NOPERM")) throw err;
|
|
25267
|
+
warnDiagnosticOnce(
|
|
25268
|
+
"redis_update_session_denied",
|
|
25269
|
+
"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.",
|
|
25270
|
+
void 0,
|
|
25271
|
+
"docs/api-reference.md#concurrency-and-races"
|
|
25272
|
+
);
|
|
25273
|
+
await this.saveSession(sessionId, next);
|
|
25274
|
+
return;
|
|
25275
|
+
}
|
|
25276
|
+
if (Number(written) === 1) {
|
|
25277
|
+
await this.sweepSessionIds();
|
|
25278
|
+
return;
|
|
25279
|
+
}
|
|
25280
|
+
const backoffMs = Math.min(20 * 2 ** attempt, 200);
|
|
25281
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.random() * backoffMs));
|
|
25282
|
+
}
|
|
25283
|
+
throw new AxlError(
|
|
25284
|
+
"SESSION_CONFLICT",
|
|
25285
|
+
`RedisStore: gave up saving session "${sessionId}" after ${UPDATE_SESSION_ATTEMPTS} conflicting writes from other instances.`
|
|
25286
|
+
);
|
|
24986
25287
|
}
|
|
24987
25288
|
async getSession(sessionId) {
|
|
24988
25289
|
const raw = await this.client.get(this.sessionKey(sessionId));
|
|
@@ -25384,17 +25685,20 @@ var RedisStore = class _RedisStore {
|
|
|
25384
25685
|
try {
|
|
25385
25686
|
await this.pruneHistory(index);
|
|
25386
25687
|
} catch (err) {
|
|
25387
|
-
|
|
25388
|
-
warnDiagnosticOnce(
|
|
25389
|
-
`redis_history_prune_failed\0${index.kind}`,
|
|
25390
|
-
`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.`,
|
|
25391
|
-
void 0,
|
|
25392
|
-
"docs/api-reference.md#redisstore-options"
|
|
25393
|
-
);
|
|
25688
|
+
this.warnPruneFailed(index, err);
|
|
25394
25689
|
} finally {
|
|
25395
25690
|
inFlight.delete(index.kind);
|
|
25396
25691
|
}
|
|
25397
25692
|
}
|
|
25693
|
+
warnPruneFailed(index, err) {
|
|
25694
|
+
if (!this.isConnected) return;
|
|
25695
|
+
warnDiagnosticOnce(
|
|
25696
|
+
`redis_history_prune_failed\0${index.kind}`,
|
|
25697
|
+
`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.`,
|
|
25698
|
+
void 0,
|
|
25699
|
+
"docs/api-reference.md#redisstore-options"
|
|
25700
|
+
);
|
|
25701
|
+
}
|
|
25398
25702
|
async pruneHistory(index) {
|
|
25399
25703
|
const candidates = await this.client.zRange(index.expiry, "-inf", Date.now(), {
|
|
25400
25704
|
BY: "SCORE",
|
|
@@ -25549,7 +25853,69 @@ var RedisStore = class _RedisStore {
|
|
|
25549
25853
|
}
|
|
25550
25854
|
// ── Sessions (Studio introspection) ────────────────────────────────────
|
|
25551
25855
|
async listSessions() {
|
|
25552
|
-
|
|
25856
|
+
const live = /* @__PURE__ */ new Set();
|
|
25857
|
+
let cursor = "0";
|
|
25858
|
+
do {
|
|
25859
|
+
const page = await this.client.sScan(this.sessionIdsKey(), cursor, {
|
|
25860
|
+
COUNT: SESSION_ID_BATCH
|
|
25861
|
+
});
|
|
25862
|
+
cursor = page.cursor;
|
|
25863
|
+
const removed = new Set(await this.pruneSessionIds(page.members));
|
|
25864
|
+
for (const id of page.members) if (!removed.has(id)) live.add(id);
|
|
25865
|
+
} while (cursor !== "0");
|
|
25866
|
+
return [...live];
|
|
25867
|
+
}
|
|
25868
|
+
/**
|
|
25869
|
+
* Check a few listed sessions after a save, so ids of expired sessions go
|
|
25870
|
+
* away for apps that never list. Sessions without a TTL don't expire, so
|
|
25871
|
+
* there's nothing to do. The save has already succeeded, so failing here
|
|
25872
|
+
* can't fail it.
|
|
25873
|
+
*/
|
|
25874
|
+
async sweepSessionIds() {
|
|
25875
|
+
if (this.ttlFor("session") === void 0 || this.sessionSweepRunning) return;
|
|
25876
|
+
this.sessionSweepRunning = true;
|
|
25877
|
+
try {
|
|
25878
|
+
const page = await this.client.sScan(this.sessionIdsKey(), this.sessionSweepCursor, {
|
|
25879
|
+
COUNT: SESSION_SWEEP_COUNT
|
|
25880
|
+
});
|
|
25881
|
+
this.sessionSweepCursor = page.cursor;
|
|
25882
|
+
await this.pruneSessionIds(page.members);
|
|
25883
|
+
} catch (err) {
|
|
25884
|
+
this.warnSessionPruneFailed(err);
|
|
25885
|
+
} finally {
|
|
25886
|
+
this.sessionSweepRunning = false;
|
|
25887
|
+
}
|
|
25888
|
+
}
|
|
25889
|
+
/** Returns the ids removed. Without permission to run the script, removes none and warns. */
|
|
25890
|
+
async pruneSessionIds(ids) {
|
|
25891
|
+
const removed = [];
|
|
25892
|
+
for (let start = 0; start < ids.length; start += SESSION_ID_BATCH) {
|
|
25893
|
+
const batch = ids.slice(start, start + SESSION_ID_BATCH);
|
|
25894
|
+
try {
|
|
25895
|
+
const result = await this.client.eval(PRUNE_SESSION_IDS_SCRIPT, {
|
|
25896
|
+
keys: [
|
|
25897
|
+
this.sessionIdsKey(),
|
|
25898
|
+
...batch.flatMap((id) => [this.sessionKey(id), this.sessionMetaKey(id)])
|
|
25899
|
+
],
|
|
25900
|
+
arguments: batch
|
|
25901
|
+
});
|
|
25902
|
+
removed.push(...result);
|
|
25903
|
+
} catch (err) {
|
|
25904
|
+
if (!errorMessage(err).includes("NOPERM")) throw err;
|
|
25905
|
+
this.warnSessionPruneFailed(err);
|
|
25906
|
+
return removed;
|
|
25907
|
+
}
|
|
25908
|
+
}
|
|
25909
|
+
return removed;
|
|
25910
|
+
}
|
|
25911
|
+
warnSessionPruneFailed(err) {
|
|
25912
|
+
if (!this.isConnected) return;
|
|
25913
|
+
warnDiagnosticOnce(
|
|
25914
|
+
"redis_session_ids_prune_failed",
|
|
25915
|
+
`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.`,
|
|
25916
|
+
void 0,
|
|
25917
|
+
"docs/api-reference.md#redisstore-options"
|
|
25918
|
+
);
|
|
25553
25919
|
}
|
|
25554
25920
|
// ── Streaming events (for state.persist: 'streaming') ──────────────────
|
|
25555
25921
|
async appendStreamingEvents(executionId, events) {
|
|
@@ -25590,30 +25956,33 @@ var RedisStore = class _RedisStore {
|
|
|
25590
25956
|
* better-sqlite3's `close()` are both no-ops the second time; node-redis
|
|
25591
25957
|
* instead rejects with "The client is closed", so absorb that here.
|
|
25592
25958
|
*
|
|
25593
|
-
* Bounded, so a shutdown can't hang on Redis.
|
|
25594
|
-
*
|
|
25595
|
-
*
|
|
25596
|
-
*
|
|
25959
|
+
* Bounded, so a shutdown can't hang on Redis. Replies arrive in order, so a
|
|
25960
|
+
* reply to this PING means every earlier command has finished.
|
|
25961
|
+
*
|
|
25962
|
+
* Not `quit()`: it marks the client closed before the server answers, and
|
|
25963
|
+
* `destroy()` then throws without touching the socket, so a stalled server's
|
|
25964
|
+
* socket could never be closed. There's no other public way to close it. A
|
|
25965
|
+
* closed socket is all Redis needs to clean up, so nothing is lost.
|
|
25597
25966
|
*/
|
|
25598
25967
|
async close() {
|
|
25968
|
+
this.closing = true;
|
|
25969
|
+
this.stopResponseWatch?.();
|
|
25599
25970
|
if (this.connection.isOpen === false) return;
|
|
25600
|
-
if (this.connection.isReady
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
|
|
25605
|
-
|
|
25606
|
-
|
|
25607
|
-
|
|
25608
|
-
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
abandonConnection(this.connection);
|
|
25614
|
-
} finally {
|
|
25615
|
-
clearTimeout(timer);
|
|
25971
|
+
if (this.connection.isReady !== false) {
|
|
25972
|
+
let timer;
|
|
25973
|
+
try {
|
|
25974
|
+
await Promise.race([
|
|
25975
|
+
this.connection.ping(),
|
|
25976
|
+
new Promise((_resolve, reject) => {
|
|
25977
|
+
timer = setTimeout(() => reject(new Error("PING timed out")), CLOSE_TIMEOUT_MS);
|
|
25978
|
+
})
|
|
25979
|
+
]);
|
|
25980
|
+
} catch {
|
|
25981
|
+
} finally {
|
|
25982
|
+
clearTimeout(timer);
|
|
25983
|
+
}
|
|
25616
25984
|
}
|
|
25985
|
+
abandonConnection(this.connection);
|
|
25617
25986
|
}
|
|
25618
25987
|
async deleteCheckpoints(executionId) {
|
|
25619
25988
|
await this.client.del(this.checkpointKey(executionId));
|