@river-build/sdk
Version:
For more details, visit the following resources:
165 lines • 6.27 kB
JavaScript
import { check, dlog } from '@river-build/dlog';
import Dexie from 'dexie';
const log = dlog('csb:dataStore');
export var LoadPriority;
(function (LoadPriority) {
LoadPriority["high"] = "high";
LoadPriority["low"] = "low";
})(LoadPriority || (LoadPriority = {}));
class TransactionBundler {
name;
constructor(name) {
this.name = name;
}
isWrite = false;
tableNames = [];
dbOps = [];
effects = [];
onCommitted = [];
}
class TransactionGroup {
name;
high;
low;
constructor(name) {
this.name = name;
this.high = new TransactionBundler('high');
this.low = new TransactionBundler('low');
}
get bundles() {
return [this.high, this.low];
}
get hasOps() {
return this.bundles.some((b) => b.dbOps.length > 0);
}
}
function makeSchema(classes) {
const schema = {};
for (const cls of classes) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
check(cls.tableName !== undefined, 'missing tableName, decorate with @persistedObservable');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
check(schema[cls.tableName] === undefined, `duplicate table name: ${cls.tableName}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
schema[cls.tableName] = 'id';
}
return schema;
}
export class Store {
db;
transactionGroup;
isLoadedMap = {};
constructor(name, version, classes) {
const schema = makeSchema(classes);
log('new Store', name, version, schema);
this.db = new Dexie(name);
this.db.version(version).stores(schema);
}
checkTableName(tableName) {
check(this.db._dbSchema[tableName] !== undefined, `table "${tableName}" not registered`);
}
newTransactionGroup(name) {
// log(`newTransactionGroup "${name}"`)
check(this.transactionGroup === undefined, `transaction already in progress named: ${this.transactionGroup?.name} while trying to start ${name}`);
this.transactionGroup = new TransactionGroup(name);
}
async commitTransaction() {
const time = Date.now();
check(this.transactionGroup !== undefined, 'transaction not started');
// save off the group
const tGroup = this.transactionGroup;
// clear before await so that any new ops are queued
this.transactionGroup = undefined;
// if no ops, return
if (!tGroup.hasOps) {
// log(`commitTransaction "${tGroup.name}" skipped (empty)`)
return;
}
log(`commitTransaction "${tGroup.name}"`, 'tables:', tGroup.bundles.map((b) => ({ [b.name]: b.tableNames })));
// iterate over InitialLoadPriority values
for (const bundle of tGroup.bundles) {
if (bundle.tableNames.length === 0) {
continue;
}
const mode = bundle.isWrite ? 'rw!' : 'r!';
await this.db.transaction(mode, bundle.tableNames, async () => {
for (const fn of bundle.dbOps) {
await fn();
}
});
if (bundle.effects.length > 0 || bundle.onCommitted.length > 0) {
this.withTransaction(`${tGroup.name}>effects_${bundle.name}`, () => {
bundle.effects.forEach((fn) => fn());
bundle.onCommitted.map((fn) => fn());
});
}
}
log(`commitTransaction "${tGroup.name}" done`, 'elapsedMs:', Date.now() - time);
}
withTransaction(name, fn) {
if (this.transactionGroup !== undefined) {
return fn();
}
else {
this.newTransactionGroup(name);
const result = fn();
this.commitTransaction().catch((e) => {
log(`uncaught commitTransaction error in group ${name}`, e);
throw e;
});
return result;
}
}
load(tableName, id, loadPriority, onLoad, onError, onCommitted) {
log('+enqueue load', tableName, id, loadPriority);
this.checkTableName(tableName);
// make sure that we're loading in a transaction started with either withTransaction or newTransactionGroup
check(this.transactionGroup !== undefined, 'transaction not started');
// there should only be one model loaded per row, to prevent things like concurrent writes and out of sync data
check(!this.isLoaded(tableName, id), `model already loaded ${tableName}:${id}`);
this.setIsLoaded(tableName, id);
const bundler = this.transactionGroup[loadPriority];
bundler.tableNames.push(tableName);
const dbOp = async () => {
try {
const data = await this.db.table(tableName).get(id);
bundler.effects.push(() => onLoad(data));
}
catch (e) {
bundler.effects.push(() => onError(e));
}
};
bundler.dbOps.push(dbOp);
bundler.onCommitted.push(onCommitted);
}
save(tableName, data, onSaved, onError, onCommitted) {
log('+enqueue save', tableName, data.id);
this.checkTableName(tableName);
check(this.transactionGroup !== undefined, 'transaction not started');
const bundler = this.transactionGroup.low;
bundler.tableNames.push(tableName);
bundler.isWrite = true;
const dbOp = async () => {
try {
const id = await this.db.table(tableName).put(data);
check(id === data.id, 'id mismatch???');
bundler.effects.push(() => onSaved());
}
catch (e) {
bundler.effects.push(() => onError(e));
}
};
bundler.dbOps.push(dbOp);
bundler.onCommitted.push(onCommitted);
}
isLoaded(tableName, id) {
return this.isLoadedMap[tableName]?.has(id) ?? false;
}
setIsLoaded(tableName, id) {
if (this.isLoadedMap[tableName] === undefined) {
this.isLoadedMap[tableName] = new Set();
}
this.isLoadedMap[tableName].add(id);
}
}
//# sourceMappingURL=store.js.map