openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
396 lines (395 loc) • 14.1 kB
JavaScript
import { c as isRecord } from "./utils-CCC-BEJH.js";
import "./record-shared-BXlUig5f.js";
import { t as getMatrixRuntime } from "./runtime-CN4Os2vf.js";
import { O as resolveMatrixSqliteStateEnv } from "./crypto-state-store-Dh3bqPTW.js";
import { t as claimCurrentTokenStorageState } from "./storage-6UUh27xf.js";
import { t as createAsyncLock } from "./async-lock-BcLS4KOc.js";
import { n as LogService } from "./logger-LOBoLzE6.js";
import path from "node:path";
import fs from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import { Category, MemoryStore, SyncAccumulator } from "matrix-js-sdk/lib/matrix.js";
//#region extensions/matrix/src/matrix/client/file-sync-store.ts
const STORE_VERSION = 1;
const PERSIST_DEBOUNCE_MS = 250;
const SYNC_CACHE_NAMESPACE = "sync-cache";
const SYNC_CACHE_MAX_ENTRIES = 2e4;
const SYNC_CACHE_MAX_CHUNKS = Math.floor((SYNC_CACHE_MAX_ENTRIES - 1) / 2);
const SYNC_CACHE_STATE_KEY = "current";
const SYNC_CACHE_CHUNK_BYTES = 24e3;
function normalizeRoomsData(value) {
if (!isRecord(value)) return null;
return {
[Category.Join]: isRecord(value[Category.Join]) ? value[Category.Join] : {},
[Category.Invite]: isRecord(value[Category.Invite]) ? value[Category.Invite] : {},
[Category.Leave]: isRecord(value[Category.Leave]) ? value[Category.Leave] : {},
[Category.Knock]: isRecord(value[Category.Knock]) ? value[Category.Knock] : {}
};
}
function toPersistedSyncData(value) {
if (!isRecord(value)) return null;
if (typeof value.nextBatch === "string" && value.nextBatch.trim()) {
const roomsData = normalizeRoomsData(value.roomsData);
if (!Array.isArray(value.accountData) || !roomsData) return null;
return {
nextBatch: value.nextBatch,
accountData: value.accountData,
roomsData
};
}
if (typeof value.next_batch === "string" && value.next_batch.trim()) {
const roomsData = normalizeRoomsData(value.rooms);
if (!roomsData) return null;
return {
nextBatch: value.next_batch,
accountData: isRecord(value.account_data) && Array.isArray(value.account_data.events) ? value.account_data.events : [],
roomsData
};
}
return null;
}
function normalizePersistedStore(value) {
if (!isRecord(value) || value.version !== STORE_VERSION) return null;
return {
version: STORE_VERSION,
savedSync: toPersistedSyncData(value.savedSync),
clientOptions: isRecord(value.clientOptions) ? value.clientOptions : void 0,
cleanShutdown: value.cleanShutdown === true
};
}
function normalizeLegacyPersistedStore(value) {
const persisted = normalizePersistedStore(value);
if (persisted) return persisted;
return {
version: STORE_VERSION,
savedSync: toPersistedSyncData(value),
cleanShutdown: false
};
}
function cloneJson(value) {
return structuredClone(value);
}
function syncDataToSyncResponse(syncData) {
return {
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
account_data: { events: syncData.accountData }
};
}
var SqliteBackedMatrixSyncStore = class extends MemoryStore {
constructor(storageRootDir) {
super();
this.storageRootDir = storageRootDir;
this.persistLock = createAsyncLock();
this.accumulator = new SyncAccumulator();
this.savedSync = null;
this.cleanShutdown = false;
this.dirty = false;
this.persistTimer = null;
this.persistPromise = null;
this.stateKey = resolveSyncCacheStateKey(storageRootDir);
let restoredSavedSync = null;
let restoredClientOptions;
let restoredCleanShutdown = false;
let syncCacheStore = createNoopMatrixSyncCacheStore();
let syncCacheStoreUnavailableError;
try {
syncCacheStore = openMatrixSyncCacheStore(storageRootDir);
const persisted = readPersistedStoreFromSyncStore(syncCacheStore, this.stateKey);
if (persisted) {
restoredSavedSync = persisted.savedSync;
restoredClientOptions = persisted.clientOptions;
restoredCleanShutdown = persisted.cleanShutdown === true;
}
} catch (err) {
syncCacheStoreUnavailableError = err;
LogService.warn("MatrixSyncCacheStore", "Failed to load Matrix sync cache:", err);
}
this.store = syncCacheStore;
this.storeUnavailableError = syncCacheStoreUnavailableError;
this.savedSync = restoredSavedSync;
this.savedClientOptions = restoredClientOptions;
this.hadSavedSyncOnLoad = restoredSavedSync !== null;
this.hadCleanShutdownOnLoad = this.hadSavedSyncOnLoad && restoredCleanShutdown;
this.cleanShutdown = this.hadCleanShutdownOnLoad;
if (this.savedSync) {
this.accumulator.accumulate(syncDataToSyncResponse(this.savedSync), true);
super.setSyncToken(this.savedSync.nextBatch);
}
if (this.savedClientOptions) super.storeClientOptions(this.savedClientOptions);
}
hasSavedSync() {
return this.hadSavedSyncOnLoad;
}
hasSavedSyncFromCleanShutdown() {
return this.hadCleanShutdownOnLoad;
}
getSavedSync() {
return Promise.resolve(this.savedSync ? cloneJson(this.savedSync) : null);
}
getSavedSyncToken() {
return Promise.resolve(this.savedSync?.nextBatch ?? null);
}
setSyncData(syncData) {
this.accumulator.accumulate(syncData);
this.savedSync = this.accumulator.getJSON();
this.markDirtyAndSchedulePersist();
return Promise.resolve();
}
getClientOptions() {
return Promise.resolve(this.savedClientOptions ? cloneJson(this.savedClientOptions) : void 0);
}
storeClientOptions(options) {
this.savedClientOptions = cloneJson(options);
super.storeClientOptions(options);
this.markDirtyAndSchedulePersist();
return Promise.resolve();
}
save(force = false) {
if (force) return this.flush();
return Promise.resolve();
}
wantsSave() {
return false;
}
async deleteAllData() {
this.assertStoreAvailable();
if (this.persistTimer) {
clearTimeout(this.persistTimer);
this.persistTimer = null;
}
this.dirty = false;
await this.persistPromise?.catch(() => void 0);
await super.deleteAllData();
this.savedSync = null;
this.savedClientOptions = void 0;
this.cleanShutdown = false;
this.store.delete(metaKey(this.stateKey));
for (const row of this.store.entries()) if (row.key.startsWith(chunkKeyPrefix(this.stateKey))) this.store.delete(row.key);
await fs.rm(resolveLegacySyncCachePath(this.storageRootDir), { force: true }).catch(() => void 0);
}
markCleanShutdown() {
this.cleanShutdown = true;
this.dirty = true;
}
async flush() {
if (this.persistTimer) {
clearTimeout(this.persistTimer);
this.persistTimer = null;
}
while (this.dirty || this.persistPromise) {
if (this.dirty && !this.persistPromise) this.persistPromise = this.persist().finally(() => {
this.persistPromise = null;
});
await this.persistPromise;
}
}
markDirtyAndSchedulePersist() {
this.cleanShutdown = false;
this.dirty = true;
if (this.persistTimer) return;
this.persistTimer = setTimeout(() => {
this.persistTimer = null;
this.flush().catch((err) => {
LogService.warn("MatrixSyncCacheStore", "Failed to persist Matrix sync store:", err);
});
}, PERSIST_DEBOUNCE_MS);
this.persistTimer.unref?.();
}
async persist() {
this.assertStoreAvailable();
this.dirty = false;
const payload = {
version: STORE_VERSION,
savedSync: this.savedSync ? cloneJson(this.savedSync) : null,
cleanShutdown: this.cleanShutdown,
...this.savedClientOptions ? { clientOptions: cloneJson(this.savedClientOptions) } : {}
};
try {
await this.persistLock(async () => {
this.writePersistedStore(payload);
claimCurrentTokenStorageState({ rootDir: this.storageRootDir });
});
} catch (err) {
this.dirty = true;
throw err;
}
}
writePersistedStore(payload) {
const rows = buildSyncCacheRows(this.stateKey, payload);
for (const row of rows.chunks) this.store.register(row.key, row.value);
this.store.register(rows.meta.key, rows.meta.value);
for (const row of this.store.entries()) if (row.key.startsWith(chunkKeyPrefix(this.stateKey)) && !rows.nextChunkKeys.has(row.key)) this.store.delete(row.key);
}
assertStoreAvailable() {
if (this.storeUnavailableError == null) return;
throw new Error("Matrix sync cache SQLite store is unavailable; cannot persist sync state", { cause: this.storeUnavailableError });
}
};
function createNoopMatrixSyncCacheStore() {
return {
register: () => {},
registerIfAbsent: () => false,
lookup: () => void 0,
consume: () => void 0,
delete: () => false,
entries: () => [],
clear: () => {}
};
}
function readPersistedStoreFromSyncStore(store, stateKey) {
const meta = store.lookup(metaKey(stateKey));
if (!isSyncCacheMeta(meta)) return null;
const chunks = [];
for (let index = 0; index < meta.chunkCount; index += 1) {
const chunk = store.lookup(chunkKey(stateKey, meta.generation, index));
if (!isSyncCacheChunk(chunk) || chunk.index !== index) return normalizePersistedStore({
version: STORE_VERSION,
savedSync: null,
clientOptions: meta.clientOptions,
cleanShutdown: false
});
chunks.push(chunk.data);
}
let savedSync = null;
if (chunks.length > 0) {
const syncJson = chunks.join("");
if (meta.syncDigest !== digestText(syncJson)) return normalizePersistedStore({
version: STORE_VERSION,
savedSync: null,
clientOptions: meta.clientOptions,
cleanShutdown: false
});
try {
savedSync = toPersistedSyncData(JSON.parse(syncJson));
} catch {
savedSync = null;
}
}
return normalizePersistedStore({
version: STORE_VERSION,
savedSync,
clientOptions: meta.clientOptions,
cleanShutdown: meta.cleanShutdown
});
}
function openMatrixSyncCacheStore(storageRootDir) {
return getMatrixRuntime().state.openSyncKeyedStore(openMatrixSyncCacheStoreOptions(storageRootDir));
}
function resolveSyncCacheStateKey(_storageRootDir) {
return SYNC_CACHE_STATE_KEY;
}
function metaKey(stateKey) {
return `${stateKey}:meta`;
}
function chunkKeyPrefix(stateKey) {
return `${stateKey}:sync:`;
}
function chunkKey(stateKey, generation, index) {
return `${chunkKeyPrefix(stateKey)}${generation}:${index}`;
}
function resolveLegacySyncCachePath(storageRootDir) {
return path.join(storageRootDir, "bot-storage.json");
}
function digestText(value) {
return createHash("sha256").update(value, "utf8").digest("hex");
}
function isSyncCacheMeta(value) {
return isRecord(value) && value.kind === "meta" && value.version === STORE_VERSION && typeof value.generation === "string" && value.generation.trim() !== "" && typeof value.chunkCount === "number" && Number.isSafeInteger(value.chunkCount) && value.chunkCount >= 0 && value.chunkCount <= SYNC_CACHE_MAX_CHUNKS;
}
function isSyncCacheChunk(value) {
return isRecord(value) && value.kind === "sync-chunk" && typeof value.index === "number" && Number.isSafeInteger(value.index) && value.index >= 0 && typeof value.data === "string";
}
function chunkSyncCacheJson(value) {
const chunks = [];
const pushChunk = (chunk) => {
if (chunks.length >= SYNC_CACHE_MAX_CHUNKS) throw new Error("Matrix sync cache exceeds SQLite chunk limit");
chunks.push(chunk);
};
let current = "";
let currentBytes = 0;
for (const char of value) {
const charBytes = Buffer.byteLength(char, "utf8");
if (current && currentBytes + charBytes > SYNC_CACHE_CHUNK_BYTES) {
pushChunk(current);
current = "";
currentBytes = 0;
}
current += char;
currentBytes += charBytes;
}
if (current) pushChunk(current);
return chunks;
}
function buildSyncCacheRows(stateKey, payload) {
const generation = randomUUID().replaceAll("-", "");
const syncJson = payload.savedSync ? JSON.stringify(payload.savedSync) : "";
const chunks = (syncJson ? chunkSyncCacheJson(syncJson) : []).map((data, index) => ({
key: chunkKey(stateKey, generation, index),
value: {
kind: "sync-chunk",
index,
data
}
}));
return {
chunks,
nextChunkKeys: new Set(chunks.map((chunk) => chunk.key)),
meta: {
key: metaKey(stateKey),
value: {
kind: "meta",
version: STORE_VERSION,
generation,
chunkCount: chunks.length,
...syncJson ? { syncDigest: digestText(syncJson) } : {},
...payload.clientOptions ? { clientOptions: payload.clientOptions } : {},
cleanShutdown: payload.cleanShutdown === true
}
}
};
}
async function readLegacyMatrixSyncCacheState(storageRootDir) {
try {
const raw = await fs.readFile(resolveLegacySyncCachePath(storageRootDir), "utf8");
const persisted = normalizeLegacyPersistedStore(JSON.parse(raw));
if (!persisted?.savedSync && !persisted?.clientOptions) return null;
return persisted;
} catch {
return null;
}
}
async function hasMatrixSyncCacheStateInStore(params) {
const stateKey = resolveSyncCacheStateKey(params.storageRootDir);
const meta = await params.store.lookup(metaKey(stateKey));
if (!isSyncCacheMeta(meta) || meta.chunkCount <= 0) return false;
const chunks = [];
for (let index = 0; index < meta.chunkCount; index += 1) {
const chunk = await params.store.lookup(chunkKey(stateKey, meta.generation, index));
if (!isSyncCacheChunk(chunk) || chunk.index !== index) return false;
chunks.push(chunk.data);
}
const syncJson = chunks.join("");
if (meta.syncDigest !== digestText(syncJson)) return false;
try {
return toPersistedSyncData(JSON.parse(syncJson)) !== null;
} catch {
return false;
}
}
async function writeMatrixSyncCacheStateToStore(params) {
const stateKey = resolveSyncCacheStateKey(params.storageRootDir);
const rows = buildSyncCacheRows(stateKey, params.payload);
for (const row of rows.chunks) await params.store.register(row.key, row.value);
await params.store.register(rows.meta.key, rows.meta.value);
for (const row of await params.store.entries()) if (row.key.startsWith(chunkKeyPrefix(stateKey)) && !rows.nextChunkKeys.has(row.key)) await params.store.delete(row.key);
}
function openMatrixSyncCacheStoreOptions(storageRootDir) {
return {
namespace: SYNC_CACHE_NAMESPACE,
maxEntries: SYNC_CACHE_MAX_ENTRIES,
env: resolveMatrixSqliteStateEnv({ stateDir: storageRootDir })
};
}
//#endregion
export { writeMatrixSyncCacheStateToStore as a, readLegacyMatrixSyncCacheState as i, hasMatrixSyncCacheStateInStore as n, openMatrixSyncCacheStoreOptions as r, SqliteBackedMatrixSyncStore as t };