UNPKG

@zenfs/core

Version:

A filesystem, anywhere

266 lines (265 loc) 8.58 kB
// SPDX-License-Identifier: LGPL-3.0-or-later import { withErrno } from 'kerium'; import { warn } from 'kerium/log'; import { Resource } from 'utilium/cache'; import '../../polyfills.js'; /** * A transaction for a store. * @category Stores and Transactions */ export class Transaction { store; constructor(store) { this.store = store; } } /** * Transaction that implements asynchronous operations with synchronous ones * @category Stores and Transactions */ export class SyncTransaction extends Transaction { /* eslint-disable @typescript-eslint/require-await */ async get(id, offset, end) { return this.getSync(id, offset, end); } async set(id, data, offset) { return this.setSync(id, data, offset); } async remove(id) { return this.removeSync(id); } } /** * Transaction that implements synchronous operations with a cache * Implementors: You *must* update the cache and wait for `store.asyncDone` in your asynchronous methods. * @todo Make sure we handle abortions correctly, especially since the cache is shared between transactions. * @category Stores and Transactions */ export class AsyncTransaction extends Transaction { asyncDone = Promise.resolve(); /** * Run a asynchronous operation from a sync context. Not magic and subject to (race) conditions. * @internal */ async(promise) { this.asyncDone = this.asyncDone.then(() => promise); } /** * Gets a cache resource * If `info` is set and the resource doesn't exist, it will be created * @internal */ _cached(id, info) { this.store.cache ??= new Map(); const resource = this.store.cache.get(id); if (!resource) return !info ? undefined : new Resource(id, info.size, {}, this.store.cache); if (info) resource.size = info.size; return resource; } getSync(id, offset, end) { const resource = this._cached(id); if (!resource) return; end ??= resource.size; const missing = resource.missing(offset, end); for (const { start, end } of missing) { this.async(this.get(id, start, end)); } if (missing.length) throw withErrno('EAGAIN'); const region = resource.regionAt(offset); if (!region) { warn('Missing cache region for ' + id); return; } return region.data.subarray(offset - region.offset, end - region.offset); } setSync(id, data, offset) { this.async(this.set(id, data, offset)); } removeSync(id) { this.async(this.remove(id)); this.store.cache?.delete(id); } } /** * Wraps a transaction with the ability to roll-back changes, among other things. * This is used by `StoreFS` * @category Stores and Transactions * @internal @hidden */ export class WrappedTransaction { raw; fs; /** * Whether the transaction was committed or aborted */ done = false; flag(flag) { return this.raw.store.flags?.includes(flag) ?? false; } constructor(raw, fs) { this.raw = raw; this.fs = fs; } /** * Stores data in the keys we modify prior to modifying them. * Allows us to roll back commits. */ originalData = new Map(); /**TransactionEntry * List of keys modified in this transaction, if any. */ modifiedKeys = new Set(); keys() { return this.raw.keys(); } async get(id, offset = 0, end) { const data = await this.raw.get(id, offset, end); this.stash(id); return data; } getSync(id, offset = 0, end) { const data = this.raw.getSync(id, offset, end); this.stash(id); return data; } async set(id, view, offset = 0) { await this.markModified(id, offset, view.byteLength); const buffer = view instanceof Uint8Array ? view : new Uint8Array(view.buffer, view.byteOffset, view.byteLength); await this.raw.set(id, buffer, offset); } setSync(id, view, offset = 0) { this.markModifiedSync(id, offset, view.byteLength); const buffer = view instanceof Uint8Array ? view : new Uint8Array(view.buffer, view.byteOffset, view.byteLength); this.raw.setSync(id, buffer, offset); } async remove(id) { await this.markModified(id, 0, undefined); await this.raw.remove(id); } removeSync(id) { this.markModifiedSync(id, 0, undefined); this.raw.removeSync(id); } commit() { this.done = true; return Promise.resolve(); } commitSync() { this.done = true; } async abort() { if (this.done) return; // Rollback old values. for (const [id, entries] of this.originalData) { if (!this.modifiedKeys.has(id)) continue; // Key didn't exist. if (entries.some(ent => !ent.data)) { await this.raw.remove(id); this.fs._remove(id); continue; } for (const entry of entries.reverse()) { await this.raw.set(id, entry.data, entry.offset); } } this.done = true; } abortSync() { if (this.done) return; // Rollback old values. for (const [id, entries] of this.originalData) { if (!this.modifiedKeys.has(id)) continue; // Key didn't exist. if (entries.some(ent => !ent.data)) { this.raw.removeSync(id); this.fs._remove(id); continue; } for (const entry of entries.reverse()) { this.raw.setSync(id, entry.data, entry.offset); } } this.done = true; } async [Symbol.asyncDispose]() { if (this.done) return; await this.abort(); } [Symbol.dispose]() { if (this.done) return; this.abortSync(); } /** * Stashes given key value pair into `originalData` if it doesn't already exist. * Allows us to stash values the program is requesting anyway to * prevent needless `get` requests if the program modifies the data later * on during the transaction. */ stash(id, data, offset = 0) { if (!this.originalData.has(id)) this.originalData.set(id, []); this.originalData.get(id).push({ data, offset }); } /** * Marks an id as modified, and stashes its value if it has not been stashed already. */ async markModified(id, offset, length) { this.modifiedKeys.add(id); const end = length ? offset + length : undefined; try { this.stash(id, await this.raw.get(id, offset, end), offset); } catch (e) { if (!(this.raw instanceof AsyncTransaction)) throw e; /* async transaction has a quirk: setting the buffer to a larger size doesn't work correctly due to cache ranges so, we cache the existing sub-ranges */ const tx = this.raw; const resource = tx._cached(id); if (!resource) throw e; for (const range of resource.cached(offset, end ?? offset)) { this.stash(id, await this.raw.get(id, range.start, range.end), range.start); } } } /** * Marks an id as modified, and stashes its value if it has not been stashed already. */ markModifiedSync(id, offset, length) { this.modifiedKeys.add(id); const end = length ? offset + length : undefined; try { this.stash(id, this.raw.getSync(id, offset, end), offset); } catch (e) { if (!(this.raw instanceof AsyncTransaction)) throw e; /* async transaction has a quirk: setting the buffer to a larger size doesn't work correctly due to cache ranges so, we cache the existing sub-ranges */ const tx = this.raw; const resource = tx._cached(id); if (!resource) throw e; for (const range of resource.cached(offset, end ?? offset)) { this.stash(id, this.raw.getSync(id, range.start, range.end), range.start); } } } }