alinea
Version:
Headless git-based CMS
136 lines (134 loc) • 4.48 kB
JavaScript
import "../../chunks/chunk-NZLE2WMY.js";
// src/core/source/IndexedDBSource.ts
import { ShaMismatchError } from "./ShaMismatchError.js";
import { ReadonlyTree } from "./Tree.js";
var IndexedDBSource = class {
#factory;
#name;
#connection;
constructor(indexedDB, name) {
this.#factory = indexedDB;
this.#name = name;
}
#createConnection() {
return new Promise((resolve, reject) => {
const request = this.#factory.open(this.#name);
request.onsuccess = () => {
const db = request.result;
db.onclose = () => {
console.info("IndexedDB connection closed");
this.#connection = void 0;
};
resolve(db);
};
request.onerror = () => reject(request.error);
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore("blobs");
db.createObjectStore("tree");
};
});
}
#connect() {
this.#connection ??= this.#createConnection();
return this.#connection;
}
#retryIfClosing(handle) {
return (error) => {
if (error instanceof Error && error.message.includes("closing")) {
this.#connection = void 0;
return handle();
}
throw error;
};
}
getTree() {
const handle = async () => {
const db = await this.#connect();
const transaction = db.transaction(["tree", "blobs"], "readonly");
const treeStore = transaction.objectStore("tree");
const blobsStore = transaction.objectStore("blobs");
const [tree, blobKeys] = await Promise.all([
new Promise((resolve, reject) => {
const request = treeStore.get("tree");
request.onsuccess = (event) => {
const entry = event.target.result;
resolve(entry ? new ReadonlyTree(entry) : ReadonlyTree.EMPTY);
};
request.onerror = (event) => reject(event.target.error);
}),
new Promise((resolve, reject) => {
const request = blobsStore.getAllKeys();
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
})
]);
for (const sha of tree.shas) {
if (!blobKeys.includes(sha)) {
console.warn(`Blob ${sha} in tree, but not found`);
return ReadonlyTree.EMPTY;
}
}
return tree;
};
return handle().catch(this.#retryIfClosing(handle));
}
async getTreeIfDifferent(sha) {
const current = await this.getTree();
return current.sha === sha ? void 0 : current;
}
async *getBlobs(shas) {
const db = await this.#connect();
const transaction = db.transaction(["blobs"], "readonly");
const store = transaction.objectStore("blobs");
for (const sha of shas) {
const request = store.get(sha);
yield new Promise((resolve, reject) => {
request.onsuccess = (event) => {
const entry = event.target.result;
if (entry) resolve([sha, entry]);
else reject(new Error(`Blob not found: ${sha}`));
};
request.onerror = (event) => reject(event.target.error);
});
}
}
async applyChanges(batch) {
const db = await this.#connect();
const current = await this.getTree();
if (batch.fromSha !== current.sha)
throw new ShaMismatchError(
current.sha,
batch.fromSha,
"Cannot apply changes locally due to SHA mismatch"
);
const updatedTree = current.clone();
updatedTree.applyChanges(batch);
const compiled = await updatedTree.compile();
const transaction = db.transaction(["blobs", "tree"], "readwrite");
const blobs = transaction.objectStore("blobs");
const tree = transaction.objectStore("tree");
tree.put(compiled.toJSON(), "tree");
for (const change of batch.changes)
switch (change.op) {
case "add":
blobs.put(change.contents, change.sha);
break;
}
const blobKeys = await new Promise((resolve, reject) => {
const request = blobs.getAllKeys();
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
for (const sha of blobKeys) {
if (!compiled.hasSha(sha)) blobs.delete(sha);
}
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = (event) => reject(event.target.error);
});
}
};
export {
IndexedDBSource
};