diffable-objects
Version:
A package for dynamic state tracking for Cloudflare's Durable Objects using SQLite
103 lines (101 loc) • 3.66 kB
JavaScript
import { replay } from "./tracker.js";
const INITIAL_QUERY = `
CREATE TABLE IF NOT EXISTS changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
state TEXT NOT NULL,
type TEXT NOT NULL,
key TEXT NOT NULL,
path TEXT NOT NULL,
valueType TEXT,
value TEXT,
oldValue TEXT
);
CREATE TABLE IF NOT EXISTS snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
state TEXT NOT NULL,
value TEXT NOT NULL,
changes_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`;
export class SqliteState {
#name;
#storage;
#db;
constructor(name, storage) {
this.#name = name;
this.#storage = storage;
this.#db = storage.sql;
this.#db.exec(INITIAL_QUERY);
}
resume(initialValue) {
const results = this.#db.exec("SELECT value, changes_id FROM snapshots WHERE state = ? ORDER BY created_at DESC LIMIT 1", this.#name);
const result = results.next();
const mapChanges = (changes) => changes.map((change) => ({
type: "change",
atomicChange: {
type: change.type,
key: change.key,
path: change.path,
valueType: change.valueType,
value: change.value ? JSON.parse(change.value) : undefined,
oldValue: change.oldValue
? JSON.parse(change.oldValue)
: undefined,
},
}));
if (!result.done) {
const { value, changes_id } = result.value;
const changes = this.#db
.exec("SELECT * FROM changes WHERE id > ? AND state = ?", changes_id, this.#name)
.toArray();
const actions = [
{
type: "snapshot",
value: JSON.parse(value),
},
...mapChanges(changes),
];
return replay(actions, initialValue);
}
const changes = this.#db
.exec("SELECT * FROM changes WHERE state = ?", this.#name)
.toArray();
return replay(mapChanges(changes), initialValue);
}
appendChanges(changes) {
if (changes.length === 0) {
return;
}
this.#storage.transactionSync(() => {
for (const { type, key, path, valueType, value, oldValue } of changes) {
this.#db.exec("INSERT INTO changes (state, type, key, path, valueType, value, oldValue) VALUES (?, ?, ?, ?, ?, ?, ?)", this.#name, type, key, path, valueType, value ? JSON.stringify(value) : null, oldValue ? JSON.stringify(oldValue) : null);
}
});
}
latestChange() {
const result = this.#db
.exec("SELECT id FROM changes WHERE state = ? ORDER BY id DESC LIMIT 1", this.#name)
.next();
if (result.done) {
return null;
}
return { id: result.value.id };
}
latestSnapshot() {
const result = this.#db
.exec("SELECT created_at FROM snapshots WHERE state = ? ORDER BY created_at DESC LIMIT 1", this.#name)
.next();
if (result.done) {
return null;
}
return new Date(result.value.created_at);
}
snapshot(snapshot) {
const { id } = this.#db
.exec("SELECT id FROM changes WHERE state = ? ORDER BY id DESC LIMIT 1", this.#name)
.one();
// TODO: assert there are changes in the DB before we can snapshot.
this.#db.exec("INSERT INTO snapshots (state, value, changes_id) VALUES (?, ?, ?)", this.#name, JSON.stringify(snapshot), id);
}
}