@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
JavaScript
;
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;