UNPKG

@darlean/fs-persistence-suite

Version:

File System Persistence Suite that uses a physical or shared file system to persist data.

166 lines (165 loc) 6.36 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FsPersistenceActor = void 0; const base_1 = require("@darlean/base"); const utils_1 = require("@darlean/utils"); const threads_1 = require("threads"); class FsPersistenceActor { constructor(basePath, nrReaders, deser) { this.nrReaders = nrReaders; this.deser = deser; this.lastConnIdx = 0; this.basePath = basePath; this.connections = []; } async activate() { const promises = []; // Ensure writer creates the folder and database before the readers try to read it. const writableConn = await this.openDatabase('writable'); for (let idx = 1; idx < this.nrReaders + 1; idx++) { promises.push(this.openDatabase('readonly')); } this.connections = [writableConn, ...(await Promise.all(promises))]; } async deactivate() { await this.closeDatabase(); } async touch() { // Do nothing } async store(options) { return this.storeBatchImpl({ items: [{ ...options, identifier: undefined }] }); } async storeBatchBuffer(options) { return this.storeBatchImpl(this.deser.deserializeTyped(options)); } async load(options) { const conn = this.getConnection('readonly'); if (!conn) { throw new Error('No connection'); } conn.mutex.tryAcquire() || (await conn.mutex.acquire()); try { conn.busy = true; const loadResult = await conn.worker.load(options); // Because of thread boundaries, the Buffer value in loadResult is replaced // with a byte-array. So, create a new Buffer around this. return { version: loadResult.version, value: loadResult.value ? Buffer.from(await loadResult.value.arrayBuffer()) : undefined }; } finally { conn.busy = false; conn.mutex.release(); } } async queryBuffer(options) { const conn = this.getConnection('readonly'); if (!conn) { throw new Error('No connection'); } conn.mutex.tryAcquire() || (await conn.mutex.acquire()); try { conn.busy = true; const queryResults = await conn.worker.query(options); const result = { items: [], continuationToken: queryResults.continuationToken }; for (const item of queryResults.items) { result.items.push({ sortKey: item.sortKey, value: item.value ? Buffer.from(await item.value.arrayBuffer()) : undefined }); } return this.deser.serialize(result); } finally { conn.busy = false; conn.mutex.release(); } } async storeBatchImpl(options) { // TODO: Combine multiple batches internally const conn = this.getConnection('writable'); if (!conn) { throw new Error('No connection'); } conn.mutex.tryAcquire() || (await conn.mutex.acquire()); try { conn.busy = true; // TODO: Check assumption that worker performs internal synchronization (only one task at a time) const options2 = { items: options.items.map((item) => ({ identifier: item.identifier, partitionKey: item.partitionKey, version: item.version, sortKey: item.sortKey, specifier: item.specifier, value: item.value ? new Blob([item.value]) : undefined })) }; return (await conn.worker.storeBatch(options2)); } finally { conn.busy = false; conn.mutex.release(); } } async openDatabase(mode) { const filepath = this.basePath; const worker = new threads_1.Worker('./worker.js'); const spawned = await (0, threads_1.spawn)(worker); await spawned.open(filepath, mode); return { worker: spawned, mutex: new utils_1.Mutex(), busy: false }; } async closeDatabase() { for (const conn of this.connections) { await conn.worker.close(); await threads_1.Thread.terminate(conn.worker); } } getConnection(mode) { if (mode === 'writable') { return this.connections[0]; } else { let idx = 0; for (let offset = 0; offset < this.connections.length - 1; offset++) { this.lastConnIdx++; idx = 1 + (this.lastConnIdx % (this.connections.length - 1)); if (!this.connections[idx].busy) { break; } } return this.connections[idx]; } } } __decorate([ (0, base_1.action)() ], FsPersistenceActor.prototype, "touch", null); __decorate([ (0, base_1.action)({ locking: 'shared' }) ], FsPersistenceActor.prototype, "store", null); __decorate([ (0, base_1.action)({ locking: 'shared' }) ], FsPersistenceActor.prototype, "storeBatchBuffer", null); __decorate([ (0, base_1.action)({ locking: 'shared' }) ], FsPersistenceActor.prototype, "load", null); __decorate([ (0, base_1.action)({ locking: 'shared' }) ], FsPersistenceActor.prototype, "queryBuffer", null); exports.FsPersistenceActor = FsPersistenceActor;