Skip to content

Commit fe8e041

Browse files
committed
Merge branch 'main' into dqbd/util-typed-node
2 parents 1849e81 + 07a8594 commit fe8e041

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2151
-228
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ dist-cjs
2222
tmp/
2323
__pycache__
2424
.DS_Store
25+
tsconfig.vitest-temp.json

libs/checkpoint/jest.config.cjs

Lines changed: 0 additions & 20 deletions
This file was deleted.

libs/checkpoint/jest.env.cjs

Lines changed: 0 additions & 12 deletions
This file was deleted.

libs/checkpoint/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
"lint": "yarn lint:eslint && yarn lint:dpdm",
2222
"lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm",
2323
"prepack": "yarn build",
24-
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts --testTimeout 30000 --maxWorkers=50%",
25-
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch --testPathIgnorePatterns=\\.int\\.test.ts",
26-
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.cjs --testTimeout 100000",
27-
"test:int": "NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=\\.int\\.test.ts --testTimeout 100000 --maxWorkers=50%",
24+
"test": "vitest",
25+
"test:watch": "vitest watch",
26+
"test:single": "vitest run",
27+
"test:int": "vitest run --mode int",
2828
"format": "prettier --config .prettierrc --write \"src\"",
2929
"format:check": "prettier --config .prettierrc --check \"src\""
3030
},
@@ -61,7 +61,8 @@
6161
"rollup": "^4.37.0",
6262
"ts-jest": "^29.1.0",
6363
"tsx": "^4.19.3",
64-
"typescript": "^4.9.5 || ^5.4.5"
64+
"typescript": "^4.9.5 || ^5.4.5",
65+
"vitest": "^3.1.2"
6566
},
6667
"publishConfig": {
6768
"access": "public",

libs/checkpoint/src/cache/base.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { SerializerProtocol } from "../serde/base.js";
2+
import { JsonPlusSerializer } from "../serde/jsonplus.js";
3+
4+
export type CacheNamespace = string[];
5+
export type CacheFullKey = [namespace: CacheNamespace, key: string];
6+
7+
export abstract class BaseCache<V = unknown> {
8+
serde: SerializerProtocol = new JsonPlusSerializer();
9+
10+
/**
11+
* Initialize the cache with a serializer.
12+
*
13+
* @param serde - The serializer to use.
14+
*/
15+
constructor(serde?: SerializerProtocol) {
16+
this.serde = serde || this.serde;
17+
}
18+
19+
/**
20+
* Get the cached values for the given keys.
21+
*
22+
* @param keys - The keys to get.
23+
*/
24+
abstract get(
25+
keys: CacheFullKey[]
26+
): Promise<{ key: CacheFullKey; value: V }[]>;
27+
28+
/**
29+
* Set the cached values for the given keys and TTLs.
30+
*
31+
* @param pairs - The pairs to set.
32+
*/
33+
abstract set(
34+
pairs: { key: CacheFullKey; value: V; ttl?: number }[]
35+
): Promise<void>;
36+
37+
/**
38+
* Delete the cached values for the given namespaces.
39+
* If no namespaces are provided, clear all cached values.
40+
*
41+
* @param namespaces - The namespaces to clear.
42+
*/
43+
abstract clear(namespaces: CacheNamespace[]): Promise<void>;
44+
}

libs/checkpoint/src/cache/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./base.js";
2+
export * from "./memory.js";

libs/checkpoint/src/cache/memory.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { BaseCache, type CacheFullKey, type CacheNamespace } from "./base.js";
2+
3+
export class InMemoryCache<V = unknown> extends BaseCache<V> {
4+
private cache: {
5+
[namespace: string]: {
6+
[key: string]: {
7+
enc: string;
8+
val: Uint8Array | string;
9+
exp: number | null;
10+
};
11+
};
12+
} = {};
13+
14+
async get(keys: CacheFullKey[]): Promise<{ key: CacheFullKey; value: V }[]> {
15+
if (!keys.length) return [];
16+
const now = Date.now();
17+
return (
18+
await Promise.all(
19+
keys.map(
20+
async (fullKey): Promise<{ key: CacheFullKey; value: V }[]> => {
21+
const [namespace, key] = fullKey;
22+
const strNamespace = namespace.join(",");
23+
24+
if (strNamespace in this.cache && key in this.cache[strNamespace]) {
25+
const cached = this.cache[strNamespace][key];
26+
if (cached.exp == null || now < cached.exp) {
27+
const value = await this.serde.loadsTyped(
28+
cached.enc,
29+
cached.val
30+
);
31+
return [{ key: fullKey, value }];
32+
} else {
33+
delete this.cache[strNamespace][key];
34+
}
35+
}
36+
37+
return [];
38+
}
39+
)
40+
)
41+
).flat();
42+
}
43+
44+
async set(
45+
pairs: { key: CacheFullKey; value: V; ttl?: number }[]
46+
): Promise<void> {
47+
const now = Date.now();
48+
for (const { key: fullKey, value, ttl } of pairs) {
49+
const [namespace, key] = fullKey;
50+
const strNamespace = namespace.join(",");
51+
const [enc, val] = await this.serde.dumpsTyped(value);
52+
const exp = ttl != null ? ttl * 1000 + now : null;
53+
54+
this.cache[strNamespace] ??= {};
55+
this.cache[strNamespace][key] = { enc, val, exp };
56+
}
57+
}
58+
59+
async clear(namespaces: CacheNamespace[]): Promise<void> {
60+
if (!namespaces.length) {
61+
this.cache = {};
62+
return;
63+
}
64+
65+
for (const namespace of namespaces) {
66+
const strNamespace = namespace.join(",");
67+
if (strNamespace in this.cache) delete this.cache[strNamespace];
68+
}
69+
}
70+
}

libs/checkpoint/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from "./types.js";
55
export * from "./serde/base.js";
66
export * from "./serde/types.js";
77
export * from "./store/index.js";
8+
export * from "./cache/index.js";

libs/checkpoint/src/serde/tests/jsonplus.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { it, expect } from "@jest/globals";
1+
import { it, expect } from "vitest";
22
import { AIMessage, HumanMessage } from "@langchain/core/messages";
33
import { uuid6 } from "../../id.js";
44
import { JsonPlusSerializer } from "../jsonplus.js";
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import { InMemoryCache } from "../cache/memory.js";
3+
4+
describe("InMemoryCache", () => {
5+
let cache: InMemoryCache<string>;
6+
7+
beforeEach(() => {
8+
cache = new InMemoryCache<string>();
9+
});
10+
11+
describe("get and set", () => {
12+
it("should set and get a single value", async () => {
13+
const key: [string[], string] = [["test"], "key1"];
14+
await cache.set([{ key, value: "value1", ttl: 1000 }]);
15+
expect(await cache.get([key])).toEqual([{ key, value: "value1" }]);
16+
});
17+
18+
it("should handle multiple values", async () => {
19+
const pairs = [
20+
{
21+
key: [["test"], "key1"] as [string[], string],
22+
value: "value1",
23+
ttl: 1000,
24+
},
25+
{
26+
key: [["test"], "key2"] as [string[], string],
27+
value: "value2",
28+
ttl: 1000,
29+
},
30+
];
31+
32+
await cache.set(pairs);
33+
const result = await cache.get(pairs.map((p) => p.key));
34+
35+
expect(result).toEqual([
36+
{ key: [["test"], "key1"], value: "value1" },
37+
{ key: [["test"], "key2"], value: "value2" },
38+
]);
39+
});
40+
41+
it("should return empty array for non-existent keys", async () => {
42+
const result = await cache.get([[["test"], "nonexistent"]]);
43+
expect(result).toHaveLength(0);
44+
});
45+
});
46+
47+
describe("TTL behavior", () => {
48+
it("should expire values after TTL", async () => {
49+
const key: [string[], string] = [["test"], "key1"];
50+
await cache.set([{ key, value: "value1", ttl: 0.1 }]); // 100ms TTL
51+
52+
// Wait for TTL to expire
53+
await new Promise((resolve) => {
54+
setTimeout(resolve, 150);
55+
});
56+
57+
expect(await cache.get([key])).toEqual([]);
58+
});
59+
60+
it("should not expire values before TTL", async () => {
61+
const key: [string[], string] = [["test"], "key1"];
62+
await cache.set([{ key, value: "value1", ttl: 0.2 }]); // 200ms TTL
63+
64+
// Check before TTL expires
65+
await new Promise((resolve) => {
66+
setTimeout(resolve, 100);
67+
});
68+
69+
const result = await cache.get([key]);
70+
expect(result).toHaveLength(1);
71+
expect(result[0].value).toBe("value1");
72+
});
73+
});
74+
75+
describe("namespace handling", () => {
76+
it("should handle different namespaces separately", async () => {
77+
const pairs = [
78+
{
79+
key: [["ns1"], "key1"] as [string[], string],
80+
value: "value1",
81+
ttl: 1000,
82+
},
83+
{
84+
key: [["ns2"], "key1"] as [string[], string],
85+
value: "value2",
86+
ttl: 1000,
87+
},
88+
];
89+
90+
await cache.set(pairs);
91+
const result = await cache.get(pairs.map((p) => p.key));
92+
93+
expect(result).toEqual([
94+
{ key: [["ns1"], "key1"], value: "value1" },
95+
{ key: [["ns2"], "key1"], value: "value2" },
96+
]);
97+
});
98+
99+
it("should handle nested namespaces", async () => {
100+
const key: [string[], string] = [["ns1", "subns"], "key1"];
101+
await cache.set([{ key, value: "value1", ttl: 1.0 }]);
102+
103+
expect(await cache.get([key])).toEqual([{ key, value: "value1" }]);
104+
});
105+
});
106+
107+
describe("clear operations", () => {
108+
it("should clear specific namespace", async () => {
109+
const pairs = [
110+
{
111+
key: [["ns1"], "key1"] as [string[], string],
112+
value: "value1",
113+
ttl: 1.0,
114+
},
115+
{
116+
key: [["ns2"], "key1"] as [string[], string],
117+
value: "value2",
118+
ttl: 1.0,
119+
},
120+
];
121+
122+
await cache.set(pairs);
123+
await cache.clear([["ns1"]]);
124+
125+
expect(await cache.get(pairs.map((p) => p.key))).toEqual([
126+
{ key: [["ns2"], "key1"], value: "value2" },
127+
]);
128+
});
129+
130+
it("should clear all namespaces when no namespace specified", async () => {
131+
const pairs = [
132+
{
133+
key: [["ns1"], "key1"] as [string[], string],
134+
value: "value1",
135+
ttl: 1.0,
136+
},
137+
{
138+
key: [["ns2"], "key1"] as [string[], string],
139+
value: "value2",
140+
ttl: 1.0,
141+
},
142+
];
143+
144+
await cache.set(pairs);
145+
await cache.clear([]);
146+
147+
expect(await cache.get(pairs.map((p) => p.key))).toEqual([]);
148+
});
149+
});
150+
151+
describe("edge cases", () => {
152+
it("should handle empty key arrays", async () => {
153+
expect(await cache.get([])).toEqual([]);
154+
});
155+
156+
it("should handle empty namespace arrays", async () => {
157+
expect(await cache.clear([])).toBeUndefined();
158+
});
159+
160+
it("should handle setting empty pairs", async () => {
161+
expect(await cache.set([])).toBeUndefined();
162+
});
163+
});
164+
});

libs/checkpoint/src/tests/checkpoints.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from "@jest/globals";
1+
import { describe, it, expect } from "vitest";
22
import {
33
Checkpoint,
44
CheckpointTuple,

libs/checkpoint/src/tests/namespace.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, beforeEach } from "@jest/globals";
1+
import { describe, it, expect, beforeEach } from "vitest";
22
import { InMemoryStore } from "../store/memory.js";
33
import { InvalidNamespaceError } from "../store/base.js";
44

0 commit comments

Comments
 (0)