@ixo/matrix 1.1.0
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 +396 -0
- package/dist/src/checkpointer/checkpointer.d.ts +56 -0
- package/dist/src/checkpointer/checkpointer.d.ts.map +1 -0
- package/dist/src/checkpointer/checkpointer.js +669 -0
- package/dist/src/checkpointer/checkpointer.js.map +1 -0
- package/dist/src/checkpointer/index.d.ts +3 -0
- package/dist/src/checkpointer/index.d.ts.map +1 -0
- package/dist/src/checkpointer/index.js +3 -0
- package/dist/src/checkpointer/index.js.map +1 -0
- package/dist/src/checkpointer/types.d.ts +66 -0
- package/dist/src/checkpointer/types.d.ts.map +1 -0
- package/dist/src/checkpointer/types.js +2 -0
- package/dist/src/checkpointer/types.js.map +1 -0
- package/dist/src/checkpointer/utils.d.ts +2 -0
- package/dist/src/checkpointer/utils.d.ts.map +1 -0
- package/dist/src/checkpointer/utils.js +15 -0
- package/dist/src/checkpointer/utils.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/matrix-manager.d.ts +61 -0
- package/dist/src/matrix-manager.d.ts.map +1 -0
- package/dist/src/matrix-manager.js +337 -0
- package/dist/src/matrix-manager.js.map +1 -0
- package/dist/src/matrix-state-manager/index.d.ts +2 -0
- package/dist/src/matrix-state-manager/index.d.ts.map +1 -0
- package/dist/src/matrix-state-manager/index.js +2 -0
- package/dist/src/matrix-state-manager/index.js.map +1 -0
- package/dist/src/matrix-state-manager/matrix-state-manager.d.ts +22 -0
- package/dist/src/matrix-state-manager/matrix-state-manager.d.ts.map +1 -0
- package/dist/src/matrix-state-manager/matrix-state-manager.js +172 -0
- package/dist/src/matrix-state-manager/matrix-state-manager.js.map +1 -0
- package/dist/src/types/matrix.d.ts +27 -0
- package/dist/src/types/matrix.d.ts.map +1 -0
- package/dist/src/types/matrix.js +2 -0
- package/dist/src/types/matrix.js.map +1 -0
- package/dist/src/types.d.ts +64 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +55 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/cache.d.ts +13 -0
- package/dist/src/utils/cache.d.ts.map +1 -0
- package/dist/src/utils/cache.js +44 -0
- package/dist/src/utils/cache.js.map +1 -0
- package/dist/src/utils/create-simple-matrix-client.d.ts +45 -0
- package/dist/src/utils/create-simple-matrix-client.d.ts.map +1 -0
- package/dist/src/utils/create-simple-matrix-client.js +269 -0
- package/dist/src/utils/create-simple-matrix-client.js.map +1 -0
- package/dist/src/utils/format-msg.d.ts +13 -0
- package/dist/src/utils/format-msg.d.ts.map +1 -0
- package/dist/src/utils/format-msg.js +14 -0
- package/dist/src/utils/format-msg.js.map +1 -0
- package/dist/src/utils/login.d.ts +3 -0
- package/dist/src/utils/login.d.ts.map +1 -0
- package/dist/src/utils/login.js +10 -0
- package/dist/src/utils/login.js.map +1 -0
- package/dist/src/utils/mx.d.ts +7 -0
- package/dist/src/utils/mx.d.ts.map +1 -0
- package/dist/src/utils/mx.js +58 -0
- package/dist/src/utils/mx.js.map +1 -0
- package/dist/src/utils/secretStorageKeys.d.ts +10 -0
- package/dist/src/utils/secretStorageKeys.d.ts.map +1 -0
- package/dist/src/utils/secretStorageKeys.js +33 -0
- package/dist/src/utils/secretStorageKeys.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { BaseCheckpointSaver, TASKS, WRITES_IDX_MAP, copyCheckpoint, maxChannelVersion, } from '@langchain/langgraph-checkpoint';
|
|
3
|
+
import { Logger } from '@ixo/logger';
|
|
4
|
+
import { matrixStateManager } from '../matrix-state-manager/matrix-state-manager.js';
|
|
5
|
+
class LRUCache {
|
|
6
|
+
cache = new Map();
|
|
7
|
+
max;
|
|
8
|
+
ttl;
|
|
9
|
+
constructor(max = 5000, ttl = 30000) {
|
|
10
|
+
this.max = max;
|
|
11
|
+
this.ttl = ttl;
|
|
12
|
+
}
|
|
13
|
+
get(key) {
|
|
14
|
+
const entry = this.cache.get(key);
|
|
15
|
+
if (!entry)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (Date.now() - entry.timestamp > this.ttl) {
|
|
18
|
+
this.cache.delete(key);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
this.cache.delete(key);
|
|
22
|
+
this.cache.set(key, entry);
|
|
23
|
+
return entry.value;
|
|
24
|
+
}
|
|
25
|
+
set(key, value) {
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
if (this.cache.size >= this.max) {
|
|
28
|
+
const firstKey = this.cache.keys().next().value;
|
|
29
|
+
if (firstKey)
|
|
30
|
+
this.cache.delete(firstKey);
|
|
31
|
+
}
|
|
32
|
+
this.cache.set(key, { value, timestamp: Date.now() });
|
|
33
|
+
}
|
|
34
|
+
delete(key) {
|
|
35
|
+
this.cache.delete(key);
|
|
36
|
+
}
|
|
37
|
+
clear() {
|
|
38
|
+
this.cache.clear();
|
|
39
|
+
}
|
|
40
|
+
deletePattern(pattern) {
|
|
41
|
+
const keysToDelete = [];
|
|
42
|
+
for (const key of this.cache.keys()) {
|
|
43
|
+
if (key.includes(pattern)) {
|
|
44
|
+
keysToDelete.push(key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const key of keysToDelete) {
|
|
48
|
+
this.cache.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class MatrixCheckpointSaver extends BaseCheckpointSaver {
|
|
53
|
+
stateManager = matrixStateManager;
|
|
54
|
+
checkpointCache;
|
|
55
|
+
writesCache;
|
|
56
|
+
latestCache;
|
|
57
|
+
indexCache;
|
|
58
|
+
metrics = {
|
|
59
|
+
hits: 0,
|
|
60
|
+
misses: 0,
|
|
61
|
+
indexRebuilds: 0,
|
|
62
|
+
filteredEvents: 0,
|
|
63
|
+
duplicateEvents: 0,
|
|
64
|
+
};
|
|
65
|
+
cacheTTL;
|
|
66
|
+
cacheMax;
|
|
67
|
+
maxCheckpointSizeBytes;
|
|
68
|
+
constructor(serde) {
|
|
69
|
+
super(serde);
|
|
70
|
+
this.cacheTTL = parseInt(process.env.MATRIX_CP_CACHE_TTL_MS || '300000', 10);
|
|
71
|
+
this.cacheMax = parseInt(process.env.MATRIX_CP_CACHE_MAX || '50000', 10);
|
|
72
|
+
this.maxCheckpointSizeBytes = parseInt(process.env.MATRIX_CP_MAX_SIZE_BYTES || '10485760', 10);
|
|
73
|
+
this.checkpointCache = new LRUCache(this.cacheMax, this.cacheTTL);
|
|
74
|
+
this.writesCache = new LRUCache(this.cacheMax, this.cacheTTL);
|
|
75
|
+
this.latestCache = new LRUCache(this.cacheMax, this.cacheTTL);
|
|
76
|
+
this.indexCache = new LRUCache(this.cacheMax, this.cacheTTL);
|
|
77
|
+
}
|
|
78
|
+
sanitizeOracleDid(oracleDid) {
|
|
79
|
+
return oracleDid.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
80
|
+
}
|
|
81
|
+
getCheckpointKey(oracleDid, threadId, checkpointNs, checkpointId) {
|
|
82
|
+
const safe = this.sanitizeOracleDid(oracleDid);
|
|
83
|
+
return `${safe}_ckpt_${threadId}_${checkpointNs}_${checkpointId}`;
|
|
84
|
+
}
|
|
85
|
+
getWritesKey(oracleDid, threadId, checkpointNs, checkpointId) {
|
|
86
|
+
const safe = this.sanitizeOracleDid(oracleDid);
|
|
87
|
+
return `${safe}_writes_${threadId}_${checkpointNs}_${checkpointId}`;
|
|
88
|
+
}
|
|
89
|
+
getLatestCheckpointKey(oracleDid, threadId, checkpointNs) {
|
|
90
|
+
const safe = this.sanitizeOracleDid(oracleDid);
|
|
91
|
+
return `${safe}_latest_${threadId}_${checkpointNs}`;
|
|
92
|
+
}
|
|
93
|
+
getThreadMapKey(oracleDid) {
|
|
94
|
+
const safe = this.sanitizeOracleDid(oracleDid);
|
|
95
|
+
return `${safe}_thread_map`;
|
|
96
|
+
}
|
|
97
|
+
getCacheWritesKey(roomId, oracleDid, threadId, checkpointNs, checkpointId) {
|
|
98
|
+
return `${roomId}:${oracleDid}:${threadId}:${checkpointNs}:${checkpointId}:writes`;
|
|
99
|
+
}
|
|
100
|
+
getCacheLatestKey(roomId, oracleDid, threadId, checkpointNs) {
|
|
101
|
+
return `${roomId}:${oracleDid}:${threadId}:${checkpointNs}:latest`;
|
|
102
|
+
}
|
|
103
|
+
getCacheCheckpointKey(roomId, oracleDid, threadId, checkpointNs, checkpointId) {
|
|
104
|
+
return `${roomId}:${oracleDid}:${threadId}:${checkpointNs}:${checkpointId}:checkpoint`;
|
|
105
|
+
}
|
|
106
|
+
async getThreadMap(roomId, oracleDid) {
|
|
107
|
+
const cacheKey = `thread_map:${roomId}:${oracleDid}`;
|
|
108
|
+
const cached = this.indexCache.get(cacheKey);
|
|
109
|
+
if (cached) {
|
|
110
|
+
this.metrics.hits++;
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
this.metrics.misses++;
|
|
114
|
+
try {
|
|
115
|
+
const threadMapKey = this.getThreadMapKey(oracleDid);
|
|
116
|
+
const threadMap = await this.stateManager.getState(roomId, threadMapKey);
|
|
117
|
+
if (threadMap && !threadMap.deleted) {
|
|
118
|
+
this.indexCache.set(cacheKey, threadMap);
|
|
119
|
+
return threadMap;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
}
|
|
124
|
+
const emptyMap = {};
|
|
125
|
+
this.indexCache.set(cacheKey, emptyMap);
|
|
126
|
+
return emptyMap;
|
|
127
|
+
}
|
|
128
|
+
async updateThreadMap(roomId, oracleDid, threadId, checkpointId) {
|
|
129
|
+
const threadMap = await this.getThreadMap(roomId, oracleDid);
|
|
130
|
+
threadMap[threadId] = checkpointId;
|
|
131
|
+
const threadMapKey = this.getThreadMapKey(oracleDid);
|
|
132
|
+
await this.stateManager.setState({
|
|
133
|
+
roomId,
|
|
134
|
+
stateKey: threadMapKey,
|
|
135
|
+
data: threadMap,
|
|
136
|
+
});
|
|
137
|
+
const cacheKey = `thread_map:${roomId}:${oracleDid}`;
|
|
138
|
+
this.indexCache.set(cacheKey, threadMap);
|
|
139
|
+
Logger.debug(`Updated thread mapping: ${threadId} -> ${checkpointId}`);
|
|
140
|
+
}
|
|
141
|
+
async storeCheckpoint(roomId, oracleDid, storedCheckpoint) {
|
|
142
|
+
const key = this.getCheckpointKey(oracleDid, storedCheckpoint.thread_id, storedCheckpoint.checkpoint_ns, storedCheckpoint.checkpoint_id);
|
|
143
|
+
storedCheckpoint.lastUpdatedAt = Date.now();
|
|
144
|
+
const serialized = JSON.stringify(storedCheckpoint);
|
|
145
|
+
storedCheckpoint.sizeBytes = serialized.length;
|
|
146
|
+
if (serialized.length > this.maxCheckpointSizeBytes) {
|
|
147
|
+
Logger.warn(`Checkpoint ${storedCheckpoint.checkpoint_id} exceeds size limit: ${serialized.length} bytes`, {
|
|
148
|
+
threadId: storedCheckpoint.thread_id,
|
|
149
|
+
checkpointNs: storedCheckpoint.checkpoint_ns,
|
|
150
|
+
maxSize: this.maxCheckpointSizeBytes,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
await this.stateManager.setState({
|
|
154
|
+
roomId,
|
|
155
|
+
stateKey: key,
|
|
156
|
+
data: storedCheckpoint,
|
|
157
|
+
});
|
|
158
|
+
await this.updateThreadMap(roomId, oracleDid, storedCheckpoint.thread_id, storedCheckpoint.checkpoint_id);
|
|
159
|
+
const latestKey = this.getLatestCheckpointKey(oracleDid, storedCheckpoint.thread_id, storedCheckpoint.checkpoint_ns);
|
|
160
|
+
await this.stateManager.setState({
|
|
161
|
+
roomId,
|
|
162
|
+
stateKey: latestKey,
|
|
163
|
+
data: storedCheckpoint,
|
|
164
|
+
});
|
|
165
|
+
const latestCacheKey = this.getCacheLatestKey(roomId, oracleDid, storedCheckpoint.thread_id, storedCheckpoint.checkpoint_ns);
|
|
166
|
+
this.latestCache.set(latestCacheKey, {
|
|
167
|
+
checkpointId: storedCheckpoint.checkpoint_id,
|
|
168
|
+
lastUpdatedAt: Date.now(),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async storeWrites(roomId, oracleDid, writes) {
|
|
172
|
+
if (!writes || writes.length === 0 || !writes[0])
|
|
173
|
+
return;
|
|
174
|
+
const key = this.getWritesKey(oracleDid, writes[0].thread_id, writes[0].checkpoint_ns, writes[0].checkpoint_id);
|
|
175
|
+
const existingWrites = await this.getStoredWrites(roomId, oracleDid, writes[0].thread_id, writes[0].checkpoint_ns, writes[0].checkpoint_id);
|
|
176
|
+
const writeMap = new Map();
|
|
177
|
+
for (const write of existingWrites) {
|
|
178
|
+
const compositeKey = `${write.task_id}:${write.idx}`;
|
|
179
|
+
writeMap.set(compositeKey, write);
|
|
180
|
+
}
|
|
181
|
+
let replacedCount = 0;
|
|
182
|
+
let appendedCount = 0;
|
|
183
|
+
for (const write of writes) {
|
|
184
|
+
const compositeKey = `${write.task_id}:${write.idx}`;
|
|
185
|
+
if (writeMap.has(compositeKey)) {
|
|
186
|
+
replacedCount++;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
appendedCount++;
|
|
190
|
+
}
|
|
191
|
+
writeMap.set(compositeKey, write);
|
|
192
|
+
}
|
|
193
|
+
const allWrites = Array.from(writeMap.values()).sort((a, b) => a.idx - b.idx);
|
|
194
|
+
await this.stateManager.setState({
|
|
195
|
+
roomId,
|
|
196
|
+
stateKey: key,
|
|
197
|
+
data: allWrites,
|
|
198
|
+
});
|
|
199
|
+
const cacheKey = this.getCacheWritesKey(roomId, oracleDid, writes[0].thread_id, writes[0].checkpoint_ns, writes[0].checkpoint_id);
|
|
200
|
+
this.writesCache.set(cacheKey, allWrites);
|
|
201
|
+
Logger.debug(`Stored writes for checkpoint ${writes[0].checkpoint_id}: ${replacedCount} replaced, ${appendedCount} appended`);
|
|
202
|
+
}
|
|
203
|
+
async getStoredCheckpoint(roomId, oracleDid, threadId, checkpointNs, checkpointId) {
|
|
204
|
+
try {
|
|
205
|
+
if (!checkpointId) {
|
|
206
|
+
const threadMap = await this.getThreadMap(roomId, oracleDid);
|
|
207
|
+
const latestCheckpointId = threadMap[threadId];
|
|
208
|
+
if (!latestCheckpointId) {
|
|
209
|
+
Logger.debug(`No checkpoint found for thread ${threadId}`);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
checkpointId = latestCheckpointId;
|
|
213
|
+
}
|
|
214
|
+
const checkpointCacheKey = this.getCacheCheckpointKey(roomId, oracleDid, threadId, checkpointNs, checkpointId);
|
|
215
|
+
const cachedCheckpoint = this.checkpointCache.get(checkpointCacheKey);
|
|
216
|
+
if (cachedCheckpoint) {
|
|
217
|
+
this.metrics.hits++;
|
|
218
|
+
Logger.debug(`Checkpoint cache HIT for ${checkpointId}`, {
|
|
219
|
+
threadId,
|
|
220
|
+
checkpointNs,
|
|
221
|
+
checkpointId,
|
|
222
|
+
});
|
|
223
|
+
return cachedCheckpoint;
|
|
224
|
+
}
|
|
225
|
+
this.metrics.misses++;
|
|
226
|
+
const key = this.getCheckpointKey(oracleDid, threadId, checkpointNs, checkpointId);
|
|
227
|
+
const stored = await this.stateManager.getState(roomId, key);
|
|
228
|
+
if (!stored ||
|
|
229
|
+
stored.deleted ||
|
|
230
|
+
!this.isValidCheckpoint(stored)) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
this.checkpointCache.set(checkpointCacheKey, stored);
|
|
234
|
+
return stored;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
Logger.debug(`Failed to get checkpoint: ${checkpointId || 'latest'}`, error);
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
isValidCheckpoint(checkpoint) {
|
|
242
|
+
return (checkpoint &&
|
|
243
|
+
typeof checkpoint === 'object' &&
|
|
244
|
+
typeof checkpoint.thread_id === 'string' &&
|
|
245
|
+
typeof checkpoint.checkpoint_id === 'string' &&
|
|
246
|
+
typeof checkpoint.checkpoint_ns === 'string' &&
|
|
247
|
+
typeof checkpoint.type === 'string' &&
|
|
248
|
+
typeof checkpoint.checkpoint === 'string' &&
|
|
249
|
+
typeof checkpoint.metadata === 'string');
|
|
250
|
+
}
|
|
251
|
+
async getStoredWrites(roomId, oracleDid, threadId, checkpointNs, checkpointId) {
|
|
252
|
+
const cacheKey = this.getCacheWritesKey(roomId, oracleDid, threadId, checkpointNs, checkpointId);
|
|
253
|
+
const cached = this.writesCache.get(cacheKey);
|
|
254
|
+
if (cached) {
|
|
255
|
+
this.metrics.hits++;
|
|
256
|
+
Logger.debug(`Writes cache HIT for checkpoint ${checkpointId}`, {
|
|
257
|
+
checkpointId,
|
|
258
|
+
cacheKey,
|
|
259
|
+
});
|
|
260
|
+
return cached;
|
|
261
|
+
}
|
|
262
|
+
this.metrics.misses++;
|
|
263
|
+
try {
|
|
264
|
+
const key = this.getWritesKey(oracleDid, threadId, checkpointNs, checkpointId);
|
|
265
|
+
const writes = await this.stateManager.getState(roomId, key);
|
|
266
|
+
if (!writes || writes.deleted) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
const sorted = (writes || []).sort((a, b) => a.idx - b.idx);
|
|
270
|
+
this.writesCache.set(cacheKey, sorted);
|
|
271
|
+
return sorted;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (error?.errcode === 'M_NOT_FOUND') {
|
|
275
|
+
Logger.debug(`No writes found for checkpoint ${checkpointId} (expected for checkpoints with writesCount=0)`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
Logger.debug(`Failed to get writes for checkpoint ${checkpointId}`, error);
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async migratePendingSends(mutableCheckpoint, roomId, oracleDid, threadId, checkpointNs, parentCheckpointId) {
|
|
284
|
+
const parentWrites = await this.getStoredWrites(roomId, oracleDid, threadId, checkpointNs, parentCheckpointId);
|
|
285
|
+
const taskWrites = parentWrites
|
|
286
|
+
.filter((write) => write.channel === TASKS)
|
|
287
|
+
.sort((a, b) => a.idx - b.idx);
|
|
288
|
+
const pendingSends = await Promise.all(taskWrites.map(async (write) => {
|
|
289
|
+
return await this.serde.loadsTyped(write.type, write.value);
|
|
290
|
+
}));
|
|
291
|
+
mutableCheckpoint.channel_values ??= {};
|
|
292
|
+
mutableCheckpoint.channel_values[TASKS] = pendingSends;
|
|
293
|
+
mutableCheckpoint.channel_versions ??= {};
|
|
294
|
+
mutableCheckpoint.channel_versions[TASKS] =
|
|
295
|
+
Object.keys(mutableCheckpoint.channel_versions).length > 0
|
|
296
|
+
? maxChannelVersion(...Object.values(mutableCheckpoint.channel_versions))
|
|
297
|
+
: this.getNextVersion(undefined);
|
|
298
|
+
}
|
|
299
|
+
async getTuple(config) {
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
const { thread_id: threadId, checkpoint_ns: checkpointNs = '', checkpoint_id: checkpointId, } = config.configurable ?? {};
|
|
302
|
+
if (!threadId) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
const configs = config.configurable?.configs;
|
|
306
|
+
if (!configs) {
|
|
307
|
+
Logger.error('Missing Matrix configs in configurable', {
|
|
308
|
+
threadId,
|
|
309
|
+
checkpointNs,
|
|
310
|
+
checkpointId,
|
|
311
|
+
});
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const { matrix } = configs;
|
|
315
|
+
const storedCheckpoint = await this.getStoredCheckpoint(matrix.roomId, matrix.oracleDid, threadId, checkpointNs, checkpointId);
|
|
316
|
+
if (!storedCheckpoint) {
|
|
317
|
+
Logger.debug('Checkpoint not found', {
|
|
318
|
+
roomId: matrix.roomId,
|
|
319
|
+
threadId,
|
|
320
|
+
checkpointNs,
|
|
321
|
+
checkpointId: checkpointId || 'latest',
|
|
322
|
+
});
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
const checkpoint = (await this.serde.loadsTyped(storedCheckpoint.type, storedCheckpoint.checkpoint));
|
|
326
|
+
const metadata = (await this.serde.loadsTyped(storedCheckpoint.type, storedCheckpoint.metadata));
|
|
327
|
+
if (checkpoint.v < 4 && storedCheckpoint.parent_checkpoint_id != null) {
|
|
328
|
+
Logger.debug('Migrating v<4 checkpoint', {
|
|
329
|
+
threadId,
|
|
330
|
+
checkpointId: storedCheckpoint.checkpoint_id,
|
|
331
|
+
parentCheckpointId: storedCheckpoint.parent_checkpoint_id,
|
|
332
|
+
});
|
|
333
|
+
await this.migratePendingSends(checkpoint, matrix.roomId, matrix.oracleDid, storedCheckpoint.thread_id, storedCheckpoint.checkpoint_ns, storedCheckpoint.parent_checkpoint_id);
|
|
334
|
+
}
|
|
335
|
+
const storedWrites = await this.getStoredWrites(matrix.roomId, matrix.oracleDid, storedCheckpoint.thread_id, storedCheckpoint.checkpoint_ns, storedCheckpoint.checkpoint_id);
|
|
336
|
+
const pendingWrites = await Promise.all(storedWrites.map(async (write) => {
|
|
337
|
+
const value = await this.serde.loadsTyped(write.type, write.value);
|
|
338
|
+
return [write.task_id, write.channel, value];
|
|
339
|
+
}));
|
|
340
|
+
const finalConfig = {
|
|
341
|
+
configurable: {
|
|
342
|
+
thread_id: storedCheckpoint.thread_id,
|
|
343
|
+
checkpoint_ns: storedCheckpoint.checkpoint_ns,
|
|
344
|
+
checkpoint_id: storedCheckpoint.checkpoint_id,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
const duration = Date.now() - startTime;
|
|
348
|
+
Logger.debug(`getTuple completed in ${duration}ms`, {
|
|
349
|
+
threadId,
|
|
350
|
+
checkpointId: storedCheckpoint.checkpoint_id,
|
|
351
|
+
writesCount: pendingWrites.length,
|
|
352
|
+
});
|
|
353
|
+
return {
|
|
354
|
+
config: finalConfig,
|
|
355
|
+
checkpoint,
|
|
356
|
+
metadata,
|
|
357
|
+
parentConfig: storedCheckpoint.parent_checkpoint_id
|
|
358
|
+
? {
|
|
359
|
+
configurable: {
|
|
360
|
+
thread_id: storedCheckpoint.thread_id,
|
|
361
|
+
checkpoint_ns: storedCheckpoint.checkpoint_ns,
|
|
362
|
+
checkpoint_id: storedCheckpoint.parent_checkpoint_id,
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
: undefined,
|
|
366
|
+
pendingWrites,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
async *list(config, options) {
|
|
370
|
+
const startTime = Date.now();
|
|
371
|
+
const { thread_id: threadId, checkpoint_ns: checkpointNs = '' } = config.configurable ?? {};
|
|
372
|
+
if (!threadId) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const configs = config.configurable?.configs;
|
|
376
|
+
if (!configs) {
|
|
377
|
+
Logger.error('Missing Matrix configs in list', { threadId });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const { matrix } = configs;
|
|
381
|
+
const { filter, before, limit } = options ?? {};
|
|
382
|
+
const threadMap = await this.getThreadMap(matrix.roomId, matrix.oracleDid);
|
|
383
|
+
const latestCheckpointId = threadMap[threadId];
|
|
384
|
+
if (!latestCheckpointId) {
|
|
385
|
+
Logger.debug('No checkpoints found for thread (empty thread map)', {
|
|
386
|
+
threadId,
|
|
387
|
+
checkpointNs,
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
Logger.debug(`Listing checkpoints for thread ${threadId}`, {
|
|
392
|
+
threadId,
|
|
393
|
+
checkpointNs,
|
|
394
|
+
latestCheckpointId,
|
|
395
|
+
});
|
|
396
|
+
let currentId = latestCheckpointId;
|
|
397
|
+
let yieldedCount = 0;
|
|
398
|
+
while (currentId && (limit === undefined || yieldedCount < limit)) {
|
|
399
|
+
try {
|
|
400
|
+
if (before?.configurable?.checkpoint_id &&
|
|
401
|
+
currentId >= before.configurable.checkpoint_id) {
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
const storedCheckpoint = await this.getStoredCheckpoint(matrix.roomId, matrix.oracleDid, threadId, checkpointNs, currentId);
|
|
405
|
+
if (!storedCheckpoint) {
|
|
406
|
+
Logger.warn('Checkpoint in chain but not found in storage', {
|
|
407
|
+
threadId,
|
|
408
|
+
checkpointId: currentId,
|
|
409
|
+
});
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
const checkpoint = (await this.serde.loadsTyped(storedCheckpoint.type, storedCheckpoint.checkpoint));
|
|
413
|
+
const metadata = (await this.serde.loadsTyped(storedCheckpoint.type, storedCheckpoint.metadata));
|
|
414
|
+
if (filter && Object.keys(filter).length > 0) {
|
|
415
|
+
let matches = true;
|
|
416
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
417
|
+
if (metadata[key] !== value) {
|
|
418
|
+
matches = false;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (!matches) {
|
|
423
|
+
currentId = storedCheckpoint.parent_checkpoint_id;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (checkpoint.v < 4 && storedCheckpoint.parent_checkpoint_id != null) {
|
|
428
|
+
await this.migratePendingSends(checkpoint, matrix.roomId, matrix.oracleDid, storedCheckpoint.thread_id, storedCheckpoint.checkpoint_ns, storedCheckpoint.parent_checkpoint_id);
|
|
429
|
+
}
|
|
430
|
+
yield {
|
|
431
|
+
config: {
|
|
432
|
+
configurable: {
|
|
433
|
+
thread_id: storedCheckpoint.thread_id,
|
|
434
|
+
checkpoint_ns: storedCheckpoint.checkpoint_ns,
|
|
435
|
+
checkpoint_id: storedCheckpoint.checkpoint_id,
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
checkpoint,
|
|
439
|
+
metadata,
|
|
440
|
+
parentConfig: storedCheckpoint.parent_checkpoint_id
|
|
441
|
+
? {
|
|
442
|
+
configurable: {
|
|
443
|
+
thread_id: storedCheckpoint.thread_id,
|
|
444
|
+
checkpoint_ns: storedCheckpoint.checkpoint_ns,
|
|
445
|
+
checkpoint_id: storedCheckpoint.parent_checkpoint_id,
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
: undefined,
|
|
449
|
+
};
|
|
450
|
+
yieldedCount++;
|
|
451
|
+
currentId = storedCheckpoint.parent_checkpoint_id;
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
Logger.warn('Skipping corrupted checkpoint in chain', {
|
|
455
|
+
checkpointId: currentId,
|
|
456
|
+
error: error instanceof Error ? error.message : String(error),
|
|
457
|
+
});
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const duration = Date.now() - startTime;
|
|
462
|
+
Logger.debug(`list completed in ${duration}ms`, {
|
|
463
|
+
threadId,
|
|
464
|
+
yieldedCount,
|
|
465
|
+
cacheHits: this.metrics.hits,
|
|
466
|
+
cacheMisses: this.metrics.misses,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
async put(config, checkpoint, metadata) {
|
|
470
|
+
const { thread_id: threadId, checkpoint_ns: checkpointNs = '', checkpoint_id: parentCheckpointId, } = config.configurable ?? {};
|
|
471
|
+
if (!threadId) {
|
|
472
|
+
throw new Error('Missing thread_id in config.configurable');
|
|
473
|
+
}
|
|
474
|
+
const configs = config.configurable?.configs;
|
|
475
|
+
if (!configs) {
|
|
476
|
+
throw new Error('Missing Matrix configs in configurable');
|
|
477
|
+
}
|
|
478
|
+
const { matrix } = configs;
|
|
479
|
+
const preparedCheckpoint = copyCheckpoint(checkpoint);
|
|
480
|
+
const [serializationType, serializedCheckpoint] = await this.serde.dumpsTyped(preparedCheckpoint);
|
|
481
|
+
const [, serializedMetadata] = await this.serde.dumpsTyped(metadata);
|
|
482
|
+
const storedCheckpoint = {
|
|
483
|
+
thread_id: threadId,
|
|
484
|
+
checkpoint_id: checkpoint.id,
|
|
485
|
+
checkpoint_ns: checkpointNs,
|
|
486
|
+
parent_checkpoint_id: parentCheckpointId,
|
|
487
|
+
type: serializationType,
|
|
488
|
+
checkpoint: new TextDecoder().decode(serializedCheckpoint),
|
|
489
|
+
metadata: new TextDecoder().decode(serializedMetadata),
|
|
490
|
+
};
|
|
491
|
+
await this.storeCheckpoint(matrix.roomId, matrix.oracleDid, storedCheckpoint);
|
|
492
|
+
return {
|
|
493
|
+
configurable: {
|
|
494
|
+
thread_id: threadId,
|
|
495
|
+
checkpoint_ns: checkpointNs,
|
|
496
|
+
checkpoint_id: checkpoint.id,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
async putWrites(config, writes, taskId) {
|
|
501
|
+
const { thread_id: threadId, checkpoint_ns: checkpointNs = '', checkpoint_id: checkpointId, } = config.configurable ?? {};
|
|
502
|
+
if (!threadId || !checkpointId) {
|
|
503
|
+
Logger.error('Missing thread_id or checkpoint_id in putWrites', {
|
|
504
|
+
threadId,
|
|
505
|
+
checkpointId,
|
|
506
|
+
});
|
|
507
|
+
throw new Error('Missing thread_id or checkpoint_id in config.configurable');
|
|
508
|
+
}
|
|
509
|
+
const configs = config.configurable?.configs;
|
|
510
|
+
if (!configs) {
|
|
511
|
+
Logger.error('Missing Matrix configs in putWrites', {
|
|
512
|
+
threadId,
|
|
513
|
+
checkpointId,
|
|
514
|
+
});
|
|
515
|
+
throw new Error('Missing Matrix configs in configurable');
|
|
516
|
+
}
|
|
517
|
+
const { matrix } = configs;
|
|
518
|
+
const storedWrites = await Promise.all(writes.map(async ([channel, value], originalIdx) => {
|
|
519
|
+
const [type, serializedValue] = await this.serde.dumpsTyped(value);
|
|
520
|
+
const idx = WRITES_IDX_MAP[channel] ?? originalIdx;
|
|
521
|
+
return {
|
|
522
|
+
thread_id: threadId,
|
|
523
|
+
checkpoint_id: checkpointId,
|
|
524
|
+
checkpoint_ns: checkpointNs,
|
|
525
|
+
task_id: taskId,
|
|
526
|
+
idx,
|
|
527
|
+
channel,
|
|
528
|
+
type,
|
|
529
|
+
value: new TextDecoder().decode(serializedValue),
|
|
530
|
+
};
|
|
531
|
+
}));
|
|
532
|
+
await this.storeWrites(matrix.roomId, matrix.oracleDid, storedWrites);
|
|
533
|
+
Logger.debug(`putWrites completed`, {
|
|
534
|
+
threadId,
|
|
535
|
+
checkpointId,
|
|
536
|
+
taskId,
|
|
537
|
+
writesCount: storedWrites.length,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
async deleteThread(threadId) {
|
|
541
|
+
Logger.info(`Deleting thread ${threadId}`);
|
|
542
|
+
const checkpointNs = '';
|
|
543
|
+
throw new Error(`deleteThread not fully implemented - requires roomId and oracleDid. ` +
|
|
544
|
+
`Thread ID: ${threadId}, namespace: ${checkpointNs}. ` +
|
|
545
|
+
`Consider implementing a version that accepts full config.`);
|
|
546
|
+
}
|
|
547
|
+
async deleteThreadWithContext(roomId, oracleDid, threadId, checkpointNs = '') {
|
|
548
|
+
Logger.info(`Deleting thread ${threadId} in room ${roomId}`, {
|
|
549
|
+
oracleDid,
|
|
550
|
+
checkpointNs,
|
|
551
|
+
});
|
|
552
|
+
try {
|
|
553
|
+
const threadMap = await this.getThreadMap(roomId, oracleDid);
|
|
554
|
+
const latestCheckpointId = threadMap[threadId];
|
|
555
|
+
if (!latestCheckpointId) {
|
|
556
|
+
Logger.debug(`Thread ${threadId} not found in thread map`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
let currentId = latestCheckpointId;
|
|
560
|
+
let deletedCount = 0;
|
|
561
|
+
while (currentId) {
|
|
562
|
+
const checkpoint = await this.getStoredCheckpoint(roomId, oracleDid, threadId, checkpointNs, currentId);
|
|
563
|
+
if (!checkpoint)
|
|
564
|
+
break;
|
|
565
|
+
const checkpointKey = this.getCheckpointKey(oracleDid, threadId, checkpointNs, currentId);
|
|
566
|
+
await this.stateManager.setState({
|
|
567
|
+
roomId,
|
|
568
|
+
stateKey: checkpointKey,
|
|
569
|
+
data: null,
|
|
570
|
+
});
|
|
571
|
+
const writesKey = this.getWritesKey(oracleDid, threadId, checkpointNs, currentId);
|
|
572
|
+
await this.stateManager.setState({
|
|
573
|
+
roomId,
|
|
574
|
+
stateKey: writesKey,
|
|
575
|
+
data: null,
|
|
576
|
+
});
|
|
577
|
+
deletedCount++;
|
|
578
|
+
currentId = checkpoint.parent_checkpoint_id;
|
|
579
|
+
}
|
|
580
|
+
delete threadMap[threadId];
|
|
581
|
+
const threadMapKey = this.getThreadMapKey(oracleDid);
|
|
582
|
+
await this.stateManager.setState({
|
|
583
|
+
roomId,
|
|
584
|
+
stateKey: threadMapKey,
|
|
585
|
+
data: threadMap,
|
|
586
|
+
});
|
|
587
|
+
const latestKey = this.getLatestCheckpointKey(oracleDid, threadId, checkpointNs);
|
|
588
|
+
await this.stateManager.setState({
|
|
589
|
+
roomId,
|
|
590
|
+
stateKey: latestKey,
|
|
591
|
+
data: null,
|
|
592
|
+
});
|
|
593
|
+
const threadPattern = `${roomId}:${oracleDid}:${threadId}:${checkpointNs}`;
|
|
594
|
+
this.checkpointCache.deletePattern(threadPattern);
|
|
595
|
+
this.writesCache.deletePattern(threadPattern);
|
|
596
|
+
this.latestCache.deletePattern(threadPattern);
|
|
597
|
+
const cacheKey = `thread_map:${roomId}:${oracleDid}`;
|
|
598
|
+
this.indexCache.delete(cacheKey);
|
|
599
|
+
Logger.info(`Deleted thread ${threadId}`, {
|
|
600
|
+
roomId,
|
|
601
|
+
deletedCheckpoints: deletedCount,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
Logger.error(`Failed to delete thread ${threadId}`, error);
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async rebuildThreadMap(roomId, oracleDid) {
|
|
610
|
+
Logger.warn(`Manual thread map rebuild requested for room ${roomId}`, {
|
|
611
|
+
oracleDid,
|
|
612
|
+
});
|
|
613
|
+
try {
|
|
614
|
+
const stateEvents = await this.stateManager.listStateEvents(roomId);
|
|
615
|
+
const threadMap = {};
|
|
616
|
+
let processedCount = 0;
|
|
617
|
+
for (const event of stateEvents) {
|
|
618
|
+
if (event &&
|
|
619
|
+
'thread_id' in event &&
|
|
620
|
+
'checkpoint_id' in event &&
|
|
621
|
+
'checkpoint_ns' in event &&
|
|
622
|
+
event.checkpoint_ns === '') {
|
|
623
|
+
const threadId = event.thread_id;
|
|
624
|
+
const checkpointId = event.checkpoint_id;
|
|
625
|
+
if (!threadMap[threadId] || checkpointId > threadMap[threadId]) {
|
|
626
|
+
threadMap[threadId] = checkpointId;
|
|
627
|
+
}
|
|
628
|
+
processedCount++;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const threadMapKey = this.getThreadMapKey(oracleDid);
|
|
632
|
+
await this.stateManager.setState({
|
|
633
|
+
roomId,
|
|
634
|
+
stateKey: threadMapKey,
|
|
635
|
+
data: threadMap,
|
|
636
|
+
});
|
|
637
|
+
const cacheKey = `thread_map:${roomId}:${oracleDid}`;
|
|
638
|
+
this.indexCache.set(cacheKey, threadMap);
|
|
639
|
+
Logger.info(`Successfully rebuilt thread map: ${Object.keys(threadMap).length} threads, ${processedCount} checkpoints processed`, {
|
|
640
|
+
roomId,
|
|
641
|
+
oracleDid,
|
|
642
|
+
threadCount: Object.keys(threadMap).length,
|
|
643
|
+
processedCheckpoints: processedCount,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
Logger.error(`Failed to rebuild thread map for room ${roomId}`, error);
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async end() {
|
|
652
|
+
Logger.debug('end() called (no-op)');
|
|
653
|
+
}
|
|
654
|
+
getCacheMetrics() {
|
|
655
|
+
return { ...this.metrics };
|
|
656
|
+
}
|
|
657
|
+
getFilteringMetrics() {
|
|
658
|
+
const avgEventsPerRebuild = this.metrics.indexRebuilds > 0
|
|
659
|
+
? this.metrics.filteredEvents / this.metrics.indexRebuilds
|
|
660
|
+
: 0;
|
|
661
|
+
return {
|
|
662
|
+
filteredEvents: this.metrics.filteredEvents,
|
|
663
|
+
duplicateEvents: this.metrics.duplicateEvents,
|
|
664
|
+
indexRebuilds: this.metrics.indexRebuilds,
|
|
665
|
+
avgEventsPerRebuild: Math.round(avgEventsPerRebuild),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
//# sourceMappingURL=checkpointer.js.map
|