jackson/npm/src/db/mem.ts

234 lines
6.3 KiB
TypeScript

// This is an in-memory implementation to be used with testing and prototyping only
import { DatabaseDriver, DatabaseOption, Index, Encrypted, Records, SortOrder } from '../typings';
import * as dbutils from './utils';
class Mem implements DatabaseDriver {
private options: DatabaseOption;
private store: any;
private indexes: any;
private cleanup: any;
private ttlStore: any;
private ttlCleanup: any;
private timerId: any;
constructor(options: DatabaseOption) {
this.options = options;
}
async init(): Promise<Mem> {
this.store = {}; // map of key, value
this.indexes = {}; // map of key, Set
this.cleanup = {}; // map of indexes for cleanup when store key is deleted
this.ttlStore = {}; // map of key to ttl
if (this.options.ttl) {
this.ttlCleanup = async () => {
const now = Date.now();
for (const k in this.ttlStore) {
if (this.ttlStore[k].expiresAt < now) {
await this.delete(this.ttlStore[k].namespace, this.ttlStore[k].key);
}
}
if (this.options.ttl) {
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
}
};
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
}
return this;
}
async get(namespace: string, key: string): Promise<any> {
const res = this.store[dbutils.key(namespace, key)];
if (res) {
return res;
}
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getAll(
namespace: string,
pageOffset?: number,
pageLimit?: number,
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const returnValue: string[] = [];
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
let take = limit;
let count = 0;
take! += skip!;
if (namespace) {
const index = dbutils.keyFromParts(dbutils.createdAtPrefix, namespace);
if (this.indexes[index] === undefined) {
return { data: [] };
}
const val: string[] = Array.from(this.indexes[index]);
const iterator: IterableIterator<string> = sortOrder === 'ASC' ? val.values() : val.reverse().values();
for (const value of iterator) {
if (count >= take!) {
break;
}
if (count >= skip!) {
returnValue.push(this.store[dbutils.keyFromParts(namespace, value)]);
}
count++;
}
}
return { data: returnValue || [] };
}
async getByIndex(
namespace: string,
idx: Index,
pageOffset?: number,
pageLimit?: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
let take = limit;
let count = 0;
take! += skip!;
const dbKeys = Array.from((await this.indexes[dbutils.keyForIndex(namespace, idx)]) || []) as string[];
const iterator: IterableIterator<string> =
sortOrder === 'ASC' ? dbKeys.values() : dbKeys.reverse().values();
const ret: string[] = [];
for (const dbKey of iterator || []) {
if (count >= take!) {
break;
}
if (count >= skip!) {
ret.push(await this.get(namespace, dbKey));
}
count++;
}
return { data: ret };
}
async put(namespace: string, key: string, val: Encrypted, ttl = 0, ...indexes: any[]): Promise<any> {
const k = dbutils.key(namespace, key);
this.store[k] = val;
if (ttl) {
this.ttlStore[k] = {
namespace,
key,
expiresAt: Date.now() + ttl * 1000,
};
}
// no ttl support for secondary indexes
for (const idx of indexes || []) {
const idxKey = dbutils.keyForIndex(namespace, idx);
let set = this.indexes[idxKey];
if (!set) {
set = new Set();
this.indexes[idxKey] = set;
}
set.add(key);
const cleanupKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
let cleanup = this.cleanup[cleanupKey];
if (!cleanup) {
cleanup = new Set();
this.cleanup[cleanupKey] = cleanup;
}
cleanup.add(idxKey);
}
let createdAtSet = this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)];
if (!createdAtSet) {
createdAtSet = new Set();
this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)] = createdAtSet;
this.store['createdAt'] = new Date().toISOString();
createdAtSet.add(key);
} else {
if (!this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)].has(key)) {
createdAtSet.add(key);
this.store['createdAt'] = new Date().toISOString();
}
}
let modifiedAtSet = this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)];
if (!modifiedAtSet) {
modifiedAtSet = new Set();
this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)] = modifiedAtSet;
}
modifiedAtSet.add(key);
this.store['modifiedAt'] = new Date().toISOString();
}
async delete(namespace: string, key: string): Promise<any> {
const k = dbutils.key(namespace, key);
delete this.store[k];
const idxKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
// delete secondary indexes and then the mapping of the seconary indexes
const dbKeys = this.cleanup[idxKey];
for (const dbKey of dbKeys || []) {
this.indexes[dbKey] && this.indexes[dbKey].delete(key);
}
if (this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)]) {
this.indexes[dbutils.keyFromParts(dbutils.createdAtPrefix, namespace)].delete(key);
}
if (this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)]) {
this.indexes[dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace)].delete(key);
}
delete this.cleanup[idxKey];
delete this.ttlStore[k];
}
async deleteMany(namespace: string, keys: string[]): Promise<void> {
if (keys.length === 0) {
return;
}
for (const key of keys) {
await this.delete(namespace, key);
}
}
async close(): Promise<void> {
// no-op
}
}
export default {
new: async (options: DatabaseOption) => {
return await new Mem(options).init();
},
};