UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

165 lines 6.27 kB
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