zeitlich 0.2.48 → 0.2.50
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/README.md +26 -23
- package/dist/{activities-DCaIPQBT.d.ts → activities-IuOIvPHO.d.ts} +6 -6
- package/dist/{activities-BlQR5gX4.d.cts → activities-cIlq1y1y.d.cts} +6 -6
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +3 -3
- package/dist/adapters/sandbox/daytona/index.d.ts +3 -3
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/daytona/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +45 -42
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +10 -10
- package/dist/adapters/thread/anthropic/index.d.ts +10 -10
- package/dist/adapters/thread/anthropic/index.js +45 -42
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +7 -7
- package/dist/adapters/thread/anthropic/workflow.d.ts +7 -7
- package/dist/adapters/thread/google-genai/index.cjs +117 -54
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +27 -23
- package/dist/adapters/thread/google-genai/index.d.ts +27 -23
- package/dist/adapters/thread/google-genai/index.js +117 -54
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +8 -8
- package/dist/adapters/thread/google-genai/workflow.d.ts +8 -8
- package/dist/adapters/thread/langchain/index.cjs +45 -42
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +10 -10
- package/dist/adapters/thread/langchain/index.d.ts +10 -10
- package/dist/adapters/thread/langchain/index.js +45 -42
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +7 -7
- package/dist/adapters/thread/langchain/workflow.d.ts +7 -7
- package/dist/{cold-store-UL13Sstw.d.cts → cold-store-C0uvYTSi.d.cts} +1 -1
- package/dist/{cold-store-aD4TSKlU.d.ts → cold-store-CCnZYWjx.d.ts} +1 -1
- package/dist/index.cjs +15063 -405
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -83
- package/dist/index.d.ts +79 -83
- package/dist/index.js +15064 -402
- package/dist/index.js.map +1 -1
- package/dist/{proxy-BAty3CWM.d.cts → proxy-BVznA2_p.d.cts} +1 -1
- package/dist/{proxy-mbnwBhHw.d.ts → proxy-C4J1pNUk.d.ts} +1 -1
- package/dist/{thread-manager-CICj68PI.d.ts → thread-manager-BqjzWsP7.d.ts} +4 -4
- package/dist/{thread-manager-R6c3lnJy.d.cts → thread-manager-CzIs47uG.d.cts} +4 -4
- package/dist/{thread-manager-DsXvJ5cJ.d.cts → thread-manager-Dzl1fHhV.d.cts} +4 -4
- package/dist/{thread-manager-DtEtbUkp.d.ts → thread-manager-SkSWRPRc.d.ts} +4 -4
- package/dist/{types-gVa5XCWD.d.ts → types-BQvXWcft.d.ts} +1 -1
- package/dist/{types-DF4wzWQG.d.ts → types-CbPnU4RM.d.ts} +3 -3
- package/dist/{types-CJ7tCdl6.d.cts → types-D8W5TnSa.d.cts} +3 -3
- package/dist/{types-CJ7tCdl6.d.ts → types-D8W5TnSa.d.ts} +3 -3
- package/dist/{types-DwBYd0ij.d.ts → types-DZnUqCAP.d.cts} +709 -686
- package/dist/{types-CjY93AWZ.d.cts → types-OEN1xrFg.d.cts} +1 -1
- package/dist/{types-DWeyCTYK.d.cts → types-YNesmGKV.d.ts} +709 -686
- package/dist/{types-DDLPnxBh.d.cts → types-d2RvEP6v.d.cts} +3 -3
- package/dist/{workflow-DdaU7_j4.d.ts → workflow-B3oTe2_D.d.cts} +34 -3
- package/dist/{workflow-DVNPR7eX.d.cts → workflow-Bkzg0cjB.d.ts} +34 -3
- package/dist/workflow.cjs +15021 -362
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +3 -3
- package/dist/workflow.d.ts +3 -3
- package/dist/workflow.js +15022 -359
- package/dist/workflow.js.map +1 -1
- package/package.json +10 -37
- package/src/adapters/thread/anthropic/activities.ts +1 -1
- package/src/adapters/thread/anthropic/fork-transform.test.ts +17 -11
- package/src/adapters/thread/anthropic/model-invoker.test.ts +4 -3
- package/src/adapters/thread/anthropic/model-invoker.ts +1 -1
- package/src/adapters/thread/anthropic/thread-manager.test.ts +2 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +1 -1
- package/src/adapters/thread/google-genai/activities.ts +1 -1
- package/src/adapters/thread/google-genai/fork-transform.test.ts +17 -11
- package/src/adapters/thread/google-genai/model-invoker.test.ts +337 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +107 -23
- package/src/adapters/thread/google-genai/thread-manager.test.ts +2 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +1 -1
- package/src/adapters/thread/langchain/activities.ts +1 -1
- package/src/adapters/thread/langchain/fork-transform.test.ts +17 -11
- package/src/adapters/thread/langchain/model-invoker.ts +1 -1
- package/src/adapters/thread/langchain/thread-manager.test.ts +2 -2
- package/src/adapters/thread/langchain/thread-manager.ts +1 -1
- package/src/index.ts +2 -2
- package/src/lib/sandbox/capability-types.test.ts +2 -2
- package/src/lib/sandbox/manager.ts +2 -6
- package/src/lib/sandbox/sandbox.test.ts +1 -1
- package/src/lib/sandbox/types.ts +2 -2
- package/src/lib/session/session.integration.test.ts +92 -0
- package/src/lib/session/session.ts +23 -0
- package/src/lib/subagent/handler.ts +23 -0
- package/src/lib/subagent/subagent.integration.test.ts +198 -0
- package/src/lib/thread/keys.test.ts +9 -9
- package/src/lib/thread/keys.ts +1 -1
- package/src/lib/thread/manager.test.ts +24 -14
- package/src/lib/thread/manager.ts +19 -23
- package/src/lib/thread/snapshot.test.ts +51 -43
- package/src/lib/thread/snapshot.ts +54 -32
- package/src/lib/thread/test-utils.ts +106 -59
- package/src/lib/thread/tiered.test.ts +1 -1
- package/src/lib/thread/types.ts +2 -2
- package/src/lib/tool-router/router.integration.test.ts +44 -0
- package/src/lib/tool-router/router.ts +149 -33
- package/src/lib/tool-router/types.ts +23 -0
- package/src/lib/workflow.ts +49 -0
- package/src/{adapters/sandbox/inmemory/proxy.ts → test-utils/in-memory-sandbox-proxy.ts} +5 -16
- package/src/{adapters/sandbox/inmemory/index.ts → test-utils/in-memory-sandbox.ts} +11 -3
- package/src/tools/bash/bash.test.ts +1 -1
- package/src/tools/edit/handler.test.ts +1 -1
- package/tsup.config.ts +2 -4
- package/dist/adapters/sandbox/inmemory/index.cjs +0 -214
- package/dist/adapters/sandbox/inmemory/index.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +0 -40
- package/dist/adapters/sandbox/inmemory/index.d.ts +0 -40
- package/dist/adapters/sandbox/inmemory/index.js +0 -211
- package/dist/adapters/sandbox/inmemory/index.js.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.cjs +0 -36
- package/dist/adapters/sandbox/inmemory/workflow.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.js +0 -34
- package/dist/adapters/sandbox/inmemory/workflow.js.map +0 -1
|
@@ -813,6 +813,204 @@ describe("createSubagentHandler", () => {
|
|
|
813
813
|
expect(workflowInput.thread).toBeUndefined();
|
|
814
814
|
});
|
|
815
815
|
|
|
816
|
+
// --- persistThreadState: parent slice flush before child loads ---
|
|
817
|
+
|
|
818
|
+
it("calls persistThreadState before executeChild when forking parent's thread", async () => {
|
|
819
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
820
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
821
|
+
const persistThreadState = vi.fn(async () => undefined);
|
|
822
|
+
|
|
823
|
+
const subagent: SubagentConfig = {
|
|
824
|
+
agentName: "parent-fork-persist",
|
|
825
|
+
description: "Forks parent thread",
|
|
826
|
+
workflow: mockWorkflow(),
|
|
827
|
+
thread: "fork",
|
|
828
|
+
newThreadSource: "from-parent",
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const { handler } = createSubagentHandler([subagent]);
|
|
832
|
+
|
|
833
|
+
await handler(
|
|
834
|
+
{
|
|
835
|
+
subagent: "parent-fork-persist",
|
|
836
|
+
description: "test",
|
|
837
|
+
prompt: "test",
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
threadId: "parent-t",
|
|
841
|
+
toolCallId: "tc",
|
|
842
|
+
toolName: "Subagent",
|
|
843
|
+
persistThreadState,
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
expect(persistThreadState).toHaveBeenCalledTimes(1);
|
|
848
|
+
const lastExecOrder =
|
|
849
|
+
execMock.mock.invocationCallOrder[
|
|
850
|
+
execMock.mock.invocationCallOrder.length - 1
|
|
851
|
+
] ?? Infinity;
|
|
852
|
+
expect(persistThreadState.mock.invocationCallOrder[0]).toBeLessThan(
|
|
853
|
+
lastExecOrder
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("calls persistThreadState before executeChild when continuing parent's thread", async () => {
|
|
858
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
859
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
860
|
+
const persistThreadState = vi.fn(async () => undefined);
|
|
861
|
+
|
|
862
|
+
const subagent: SubagentConfig = {
|
|
863
|
+
agentName: "parent-continue-persist",
|
|
864
|
+
description: "Continues parent thread",
|
|
865
|
+
workflow: mockWorkflow(),
|
|
866
|
+
thread: "continue",
|
|
867
|
+
newThreadSource: "from-parent",
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
const { handler } = createSubagentHandler([subagent]);
|
|
871
|
+
|
|
872
|
+
await handler(
|
|
873
|
+
{
|
|
874
|
+
subagent: "parent-continue-persist",
|
|
875
|
+
description: "test",
|
|
876
|
+
prompt: "test",
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
threadId: "parent-t",
|
|
880
|
+
toolCallId: "tc",
|
|
881
|
+
toolName: "Subagent",
|
|
882
|
+
persistThreadState,
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
expect(persistThreadState).toHaveBeenCalledTimes(1);
|
|
887
|
+
const lastExecOrder =
|
|
888
|
+
execMock.mock.invocationCallOrder[
|
|
889
|
+
execMock.mock.invocationCallOrder.length - 1
|
|
890
|
+
] ?? Infinity;
|
|
891
|
+
expect(persistThreadState.mock.invocationCallOrder[0]).toBeLessThan(
|
|
892
|
+
lastExecOrder
|
|
893
|
+
);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it("calls persistThreadState when args.threadId points at the parent's thread", async () => {
|
|
897
|
+
const persistThreadState = vi.fn(async () => undefined);
|
|
898
|
+
|
|
899
|
+
const subagent: SubagentConfig = {
|
|
900
|
+
agentName: "explicit-parent",
|
|
901
|
+
description: "Explicit parent threadId",
|
|
902
|
+
workflow: mockWorkflow(),
|
|
903
|
+
thread: "continue",
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
const { handler } = createSubagentHandler([subagent]);
|
|
907
|
+
|
|
908
|
+
await handler(
|
|
909
|
+
{
|
|
910
|
+
subagent: "explicit-parent",
|
|
911
|
+
description: "test",
|
|
912
|
+
prompt: "test",
|
|
913
|
+
threadId: "parent-t",
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
threadId: "parent-t",
|
|
917
|
+
toolCallId: "tc",
|
|
918
|
+
toolName: "Subagent",
|
|
919
|
+
persistThreadState,
|
|
920
|
+
}
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
expect(persistThreadState).toHaveBeenCalledTimes(1);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("does not call persistThreadState when child thread is independent of the parent", async () => {
|
|
927
|
+
const persistThreadState = vi.fn(async () => undefined);
|
|
928
|
+
|
|
929
|
+
const subagent: SubagentConfig = {
|
|
930
|
+
agentName: "independent-thread",
|
|
931
|
+
description: "Continues a sibling thread",
|
|
932
|
+
workflow: mockWorkflow(),
|
|
933
|
+
thread: "continue",
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
const { handler } = createSubagentHandler([subagent]);
|
|
937
|
+
|
|
938
|
+
await handler(
|
|
939
|
+
{
|
|
940
|
+
subagent: "independent-thread",
|
|
941
|
+
description: "test",
|
|
942
|
+
prompt: "test",
|
|
943
|
+
threadId: "sibling-thread",
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
threadId: "parent-t",
|
|
947
|
+
toolCallId: "tc",
|
|
948
|
+
toolName: "Subagent",
|
|
949
|
+
persistThreadState,
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
expect(persistThreadState).not.toHaveBeenCalled();
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("does not call persistThreadState when the child starts a fresh thread", async () => {
|
|
957
|
+
const persistThreadState = vi.fn(async () => undefined);
|
|
958
|
+
|
|
959
|
+
const subagent: SubagentConfig = {
|
|
960
|
+
agentName: "fresh-thread",
|
|
961
|
+
description: "Always starts fresh",
|
|
962
|
+
workflow: mockWorkflow(),
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const { handler } = createSubagentHandler([subagent]);
|
|
966
|
+
|
|
967
|
+
await handler(
|
|
968
|
+
{ subagent: "fresh-thread", description: "test", prompt: "test" },
|
|
969
|
+
{
|
|
970
|
+
threadId: "parent-t",
|
|
971
|
+
toolCallId: "tc",
|
|
972
|
+
toolName: "Subagent",
|
|
973
|
+
persistThreadState,
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
expect(persistThreadState).not.toHaveBeenCalled();
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("still spawns the child when persistThreadState throws", async () => {
|
|
981
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
982
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
983
|
+
const persistThreadState = vi.fn(async () => {
|
|
984
|
+
throw new Error("redis down");
|
|
985
|
+
});
|
|
986
|
+
const callsBefore = execMock.mock.calls.length;
|
|
987
|
+
|
|
988
|
+
const subagent: SubagentConfig = {
|
|
989
|
+
agentName: "persist-fails",
|
|
990
|
+
description: "Persist failure should not block child",
|
|
991
|
+
workflow: mockWorkflow(),
|
|
992
|
+
thread: "fork",
|
|
993
|
+
newThreadSource: "from-parent",
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const { handler } = createSubagentHandler([subagent]);
|
|
997
|
+
|
|
998
|
+
await expect(
|
|
999
|
+
handler(
|
|
1000
|
+
{ subagent: "persist-fails", description: "test", prompt: "test" },
|
|
1001
|
+
{
|
|
1002
|
+
threadId: "parent-t",
|
|
1003
|
+
toolCallId: "tc",
|
|
1004
|
+
toolName: "Subagent",
|
|
1005
|
+
persistThreadState,
|
|
1006
|
+
}
|
|
1007
|
+
)
|
|
1008
|
+
).resolves.toBeDefined();
|
|
1009
|
+
|
|
1010
|
+
expect(persistThreadState).toHaveBeenCalledTimes(1);
|
|
1011
|
+
expect(execMock.mock.calls.length).toBe(callsBefore + 1);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
816
1014
|
// --- Sandbox continuation ---
|
|
817
1015
|
|
|
818
1016
|
it("does not pass sandbox when thread is fork (own sandbox)", async () => {
|
|
@@ -35,29 +35,29 @@ describe("createThreadManager ↔ public key helpers round-trip", () => {
|
|
|
35
35
|
const redis = {
|
|
36
36
|
exists: vi.fn(async (k: string) => (meta.has(k) ? 1 : 0)),
|
|
37
37
|
set: vi.fn(
|
|
38
|
-
async (k: string, v: string,
|
|
38
|
+
async (k: string, v: string, options?: { EX?: number }) => {
|
|
39
39
|
meta.set(k, v);
|
|
40
|
-
writtenMetaExpires.set(k,
|
|
40
|
+
if (options?.EX !== undefined) writtenMetaExpires.set(k, options.EX);
|
|
41
41
|
return "OK";
|
|
42
42
|
}
|
|
43
43
|
),
|
|
44
|
-
del: vi.fn(async (
|
|
44
|
+
del: vi.fn(async (keys: string | string[]) => {
|
|
45
45
|
let n = 0;
|
|
46
|
-
for (const k of keys) {
|
|
46
|
+
for (const k of Array.isArray(keys) ? keys : [keys]) {
|
|
47
47
|
if (store.delete(k)) n++;
|
|
48
48
|
if (meta.delete(k)) n++;
|
|
49
49
|
}
|
|
50
50
|
return n;
|
|
51
51
|
}),
|
|
52
|
-
|
|
52
|
+
rPush: vi.fn(async (k: string, element: string | string[]) => {
|
|
53
53
|
const list = store.get(k) ?? [];
|
|
54
|
-
list.push(...
|
|
54
|
+
list.push(...(Array.isArray(element) ? element : [element]));
|
|
55
55
|
store.set(k, list);
|
|
56
56
|
return list.length;
|
|
57
57
|
}),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
lRange: vi.fn(async (k: string) => store.get(k) ?? []),
|
|
59
|
+
lLen: vi.fn(async (k: string) => (store.get(k) ?? []).length),
|
|
60
|
+
lTrim: vi.fn(async () => "OK"),
|
|
61
61
|
expire: vi.fn(async (k: string, ttl: number) => {
|
|
62
62
|
if (store.has(k)) writtenListExpires.set(k, ttl);
|
|
63
63
|
if (meta.has(k)) writtenMetaExpires.set(k, ttl);
|
package/src/lib/thread/keys.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const THREAD_TTL_SECONDS = 60 * 60 * 24 * 90;
|
|
|
38
38
|
* Build the Redis list key that holds a thread's serialized messages.
|
|
39
39
|
*
|
|
40
40
|
* Mirrors the exact key used internally by zeitlich's thread manager,
|
|
41
|
-
* so a consumer calling `redis.
|
|
41
|
+
* so a consumer calling `redis.lRange(getThreadListKey(key, id), 0, -1)`
|
|
42
42
|
* sees the same data the writer wrote.
|
|
43
43
|
*
|
|
44
44
|
* @param threadKey - Thread key (defaults to `"messages"` inside the
|
|
@@ -1,45 +1,53 @@
|
|
|
1
1
|
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
|
-
import type
|
|
2
|
+
import type { RedisClientType } from "redis";
|
|
3
3
|
import { createThreadManager } from "./manager";
|
|
4
4
|
import type { PersistedThreadState } from "../state/types";
|
|
5
5
|
|
|
6
|
+
type Keys = string | string[];
|
|
7
|
+
const toKeys = (keys: Keys): string[] => (Array.isArray(keys) ? keys : [keys]);
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
|
-
* Minimal in-memory
|
|
10
|
+
* Minimal in-memory node-redis stub exposing just the commands used by
|
|
8
11
|
* `createThreadManager`'s state methods (get/set/del/exists/expire) plus
|
|
9
|
-
* the list helpers needed for `initialize`.
|
|
12
|
+
* the list helpers needed for `initialize`/`fork`. Uses the node-redis
|
|
13
|
+
* (`redis`) camelCase API and array-or-variadic keys.
|
|
10
14
|
*/
|
|
11
|
-
function createFakeRedis():
|
|
15
|
+
function createFakeRedis(): RedisClientType {
|
|
12
16
|
const store = new Map<string, string>();
|
|
13
17
|
|
|
14
18
|
const redis = {
|
|
15
19
|
async get(key: string): Promise<string | null> {
|
|
16
20
|
return store.has(key) ? (store.get(key) as string) : null;
|
|
17
21
|
},
|
|
18
|
-
async set(
|
|
22
|
+
async set(
|
|
23
|
+
key: string,
|
|
24
|
+
value: string,
|
|
25
|
+
_options?: { EX?: number }
|
|
26
|
+
): Promise<"OK"> {
|
|
19
27
|
store.set(key, String(value));
|
|
20
28
|
return "OK";
|
|
21
29
|
},
|
|
22
|
-
async del(
|
|
30
|
+
async del(keys: Keys): Promise<number> {
|
|
23
31
|
let removed = 0;
|
|
24
|
-
for (const k of keys) {
|
|
32
|
+
for (const k of toKeys(keys)) {
|
|
25
33
|
if (store.delete(k)) removed++;
|
|
26
34
|
}
|
|
27
35
|
return removed;
|
|
28
36
|
},
|
|
29
|
-
async exists(
|
|
30
|
-
return keys.reduce((acc, k) => acc + (store.has(k) ? 1 : 0), 0);
|
|
37
|
+
async exists(keys: Keys): Promise<number> {
|
|
38
|
+
return toKeys(keys).reduce((acc, k) => acc + (store.has(k) ? 1 : 0), 0);
|
|
31
39
|
},
|
|
32
40
|
async expire(_key: string, _ttl: number): Promise<number> {
|
|
33
41
|
return 1;
|
|
34
42
|
},
|
|
35
|
-
async
|
|
43
|
+
async lRange(): Promise<string[]> {
|
|
36
44
|
return [];
|
|
37
45
|
},
|
|
38
|
-
async
|
|
46
|
+
async rPush(): Promise<number> {
|
|
39
47
|
return 0;
|
|
40
48
|
},
|
|
41
49
|
_store: store,
|
|
42
|
-
} as unknown as
|
|
50
|
+
} as unknown as RedisClientType & { _store: Map<string, string> };
|
|
43
51
|
|
|
44
52
|
return redis;
|
|
45
53
|
}
|
|
@@ -64,10 +72,12 @@ const baseSlice: PersistedThreadState = {
|
|
|
64
72
|
};
|
|
65
73
|
|
|
66
74
|
describe("createThreadManager state persistence", () => {
|
|
67
|
-
let redis:
|
|
75
|
+
let redis: RedisClientType & { _store: Map<string, string> };
|
|
68
76
|
|
|
69
77
|
beforeEach(() => {
|
|
70
|
-
redis = createFakeRedis() as
|
|
78
|
+
redis = createFakeRedis() as RedisClientType & {
|
|
79
|
+
_store: Map<string, string>;
|
|
80
|
+
};
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
async function initThread(threadId: string): Promise<void> {
|
|
@@ -60,12 +60,12 @@ export function createThreadManager<T>(
|
|
|
60
60
|
return {
|
|
61
61
|
async initialize(): Promise<void> {
|
|
62
62
|
await redis.del(redisKey);
|
|
63
|
-
await redis.set(metaKey, "1",
|
|
63
|
+
await redis.set(metaKey, "1", { EX: ttlSeconds });
|
|
64
64
|
},
|
|
65
65
|
|
|
66
66
|
async load(): Promise<T[]> {
|
|
67
67
|
await assertThreadExists();
|
|
68
|
-
const data = await redis.
|
|
68
|
+
const data = await redis.lRange(redisKey, 0, -1);
|
|
69
69
|
return data.map(deserialize);
|
|
70
70
|
},
|
|
71
71
|
|
|
@@ -75,23 +75,19 @@ export function createThreadManager<T>(
|
|
|
75
75
|
|
|
76
76
|
if (idOf) {
|
|
77
77
|
const dedupId = messages.map(idOf).join(":");
|
|
78
|
-
await redis.eval(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
redisKey,
|
|
83
|
-
String(ttlSeconds),
|
|
84
|
-
...messages.map(serialize)
|
|
85
|
-
);
|
|
78
|
+
await redis.eval(APPEND_IDEMPOTENT_SCRIPT, {
|
|
79
|
+
keys: [dedupKey(dedupId), redisKey],
|
|
80
|
+
arguments: [String(ttlSeconds), ...messages.map(serialize)],
|
|
81
|
+
});
|
|
86
82
|
} else {
|
|
87
|
-
await redis.
|
|
83
|
+
await redis.rPush(redisKey, messages.map(serialize));
|
|
88
84
|
await redis.expire(redisKey, ttlSeconds);
|
|
89
85
|
}
|
|
90
86
|
},
|
|
91
87
|
|
|
92
88
|
async fork(newThreadId: string): Promise<BaseThreadManager<T>> {
|
|
93
89
|
await assertThreadExists();
|
|
94
|
-
const data = await redis.
|
|
90
|
+
const data = await redis.lRange(redisKey, 0, -1);
|
|
95
91
|
const stateRaw = await redis.get(stateKey);
|
|
96
92
|
const forked = createThreadManager({
|
|
97
93
|
...config,
|
|
@@ -100,12 +96,12 @@ export function createThreadManager<T>(
|
|
|
100
96
|
await forked.initialize();
|
|
101
97
|
if (data.length > 0) {
|
|
102
98
|
const newKey = getThreadListKey(key, newThreadId);
|
|
103
|
-
await redis.
|
|
99
|
+
await redis.rPush(newKey, data);
|
|
104
100
|
await redis.expire(newKey, ttlSeconds);
|
|
105
101
|
}
|
|
106
102
|
if (stateRaw != null) {
|
|
107
103
|
const newStateKey = getThreadStateKey(key, newThreadId);
|
|
108
|
-
await redis.set(newStateKey, stateRaw,
|
|
104
|
+
await redis.set(newStateKey, stateRaw, { EX: ttlSeconds });
|
|
109
105
|
}
|
|
110
106
|
return forked;
|
|
111
107
|
},
|
|
@@ -117,23 +113,23 @@ export function createThreadManager<T>(
|
|
|
117
113
|
"replaceAll requires the thread manager to be configured with `idOf`"
|
|
118
114
|
);
|
|
119
115
|
}
|
|
120
|
-
const existing = await redis.
|
|
116
|
+
const existing = await redis.lRange(redisKey, 0, -1);
|
|
121
117
|
const existingIds = existing
|
|
122
118
|
.map((raw) => idOf(deserialize(raw)))
|
|
123
119
|
.filter((id): id is string => typeof id === "string");
|
|
124
120
|
await redis.del(redisKey);
|
|
125
121
|
if (existingIds.length > 0) {
|
|
126
|
-
await redis.del(
|
|
122
|
+
await redis.del(existingIds.map(dedupKey));
|
|
127
123
|
}
|
|
128
124
|
if (messages.length > 0) {
|
|
129
|
-
await redis.
|
|
125
|
+
await redis.rPush(redisKey, messages.map(serialize));
|
|
130
126
|
await redis.expire(redisKey, ttlSeconds);
|
|
131
127
|
}
|
|
132
128
|
await redis.expire(metaKey, ttlSeconds);
|
|
133
129
|
},
|
|
134
130
|
|
|
135
131
|
async delete(): Promise<void> {
|
|
136
|
-
await redis.del(redisKey, metaKey, stateKey);
|
|
132
|
+
await redis.del([redisKey, metaKey, stateKey]);
|
|
137
133
|
},
|
|
138
134
|
|
|
139
135
|
async loadState(): Promise<PersistedThreadState | null> {
|
|
@@ -144,7 +140,7 @@ export function createThreadManager<T>(
|
|
|
144
140
|
|
|
145
141
|
async saveState(state: PersistedThreadState): Promise<void> {
|
|
146
142
|
await assertThreadExists();
|
|
147
|
-
await redis.set(stateKey, JSON.stringify(state),
|
|
143
|
+
await redis.set(stateKey, JSON.stringify(state), { EX: ttlSeconds });
|
|
148
144
|
},
|
|
149
145
|
|
|
150
146
|
async deleteState(): Promise<void> {
|
|
@@ -153,7 +149,7 @@ export function createThreadManager<T>(
|
|
|
153
149
|
|
|
154
150
|
async length(): Promise<number> {
|
|
155
151
|
await assertThreadExists();
|
|
156
|
-
return redis.
|
|
152
|
+
return redis.lLen(redisKey);
|
|
157
153
|
},
|
|
158
154
|
|
|
159
155
|
async truncateFromId(messageId: string): Promise<void> {
|
|
@@ -163,7 +159,7 @@ export function createThreadManager<T>(
|
|
|
163
159
|
"truncateFromId requires the thread manager to be configured with `idOf`"
|
|
164
160
|
);
|
|
165
161
|
}
|
|
166
|
-
const data = await redis.
|
|
162
|
+
const data = await redis.lRange(redisKey, 0, -1);
|
|
167
163
|
let idx = -1;
|
|
168
164
|
const removedIds: string[] = [];
|
|
169
165
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -178,7 +174,7 @@ export function createThreadManager<T>(
|
|
|
178
174
|
await redis.del(redisKey);
|
|
179
175
|
await redis.expire(metaKey, ttlSeconds);
|
|
180
176
|
} else {
|
|
181
|
-
await redis.
|
|
177
|
+
await redis.lTrim(redisKey, 0, idx - 1);
|
|
182
178
|
await redis.expire(redisKey, ttlSeconds);
|
|
183
179
|
}
|
|
184
180
|
// Clear dedup markers for the removed messages so that a rewind
|
|
@@ -186,7 +182,7 @@ export function createThreadManager<T>(
|
|
|
186
182
|
// re-append without the idempotent-append Lua script treating it
|
|
187
183
|
// as a duplicate.
|
|
188
184
|
if (removedIds.length > 0) {
|
|
189
|
-
await redis.del(
|
|
185
|
+
await redis.del(removedIds.map(dedupKey));
|
|
190
186
|
}
|
|
191
187
|
},
|
|
192
188
|
};
|