mono/packages/acl/src/data/MemoryBackend.ts
2026-02-17 22:32:32 +01:00

148 lines
4.6 KiB
TypeScript

/**
* @polymech/acl — In-memory backend
*
* Transaction = array of deferred mutations, executed on `end()`.
* All reads are synchronous (wrapped as async for the interface).
*/
import type { IBackend, Value, Values } from '../interfaces.js';
type Transaction = (() => void)[];
type BucketStore = Record<string, Record<string, string[]>>;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function toArray(v: Values): (string | number)[] {
return Array.isArray(v) ? v : [v];
}
function setUnion(a: string[], b: string[]): string[] {
return [...new Set([...a, ...b])];
}
function setDifference(a: string[], b: string[]): string[] {
const set = new Set(b);
return a.filter((x) => !set.has(x));
}
// ---------------------------------------------------------------------------
// MemoryBackend
// ---------------------------------------------------------------------------
export class MemoryBackend implements IBackend<Transaction> {
#buckets: BucketStore = {};
/** Expose raw data (used by FileBackend for serialisation). */
get buckets(): BucketStore {
return this.#buckets;
}
set buckets(data: BucketStore) {
this.#buckets = data;
}
// -- Transaction lifecycle ------------------------------------------------
begin(): Transaction {
return [];
}
async end(transaction: Transaction): Promise<void> {
for (const fn of transaction) {
fn();
}
}
async clean(): Promise<void> {
this.#buckets = {};
}
// -- Reads ----------------------------------------------------------------
async get(bucket: string, key: Value): Promise<string[]> {
return this.#buckets[bucket]?.[key] ?? [];
}
async union(bucket: string, keys: Value[]): Promise<string[]> {
const b = this.#findBucket(bucket);
if (!b) return [];
const result: string[] = [];
for (const key of keys) {
const vals = b[String(key)];
if (vals) result.push(...vals);
}
return [...new Set(result)];
}
async unions(buckets: string[], keys: Value[]): Promise<Record<string, string[]>> {
const result: Record<string, string[]> = {};
for (const bucket of buckets) {
const b = this.#buckets[bucket];
if (!b) {
result[bucket] = [];
continue;
}
const merged: string[] = [];
for (const key of keys) {
const vals = b[String(key)];
if (vals) merged.push(...vals);
}
result[bucket] = [...new Set(merged)];
}
return result;
}
// -- Writes (queued in transaction) ---------------------------------------
add(transaction: Transaction, bucket: string, key: Value, values: Values): void {
const vals = toArray(values).map(String);
transaction.push(() => {
this.#buckets[bucket] ??= {};
const existing = this.#buckets[bucket]![String(key)] ?? [];
this.#buckets[bucket]![String(key)] = setUnion(existing, vals);
});
}
del(transaction: Transaction, bucket: string, keys: Values): void {
const keysArr = toArray(keys).map(String);
transaction.push(() => {
const b = this.#buckets[bucket];
if (!b) return;
for (const k of keysArr) {
delete b[k];
}
});
}
remove(transaction: Transaction, bucket: string, key: Value, values: Values): void {
const vals = toArray(values).map(String);
transaction.push(() => {
const b = this.#buckets[bucket];
if (!b) return;
const old = b[String(key)];
if (old) {
b[String(key)] = setDifference(old, vals);
}
});
}
// -- Internal -------------------------------------------------------------
/**
* Find a bucket by exact name or by regex match (legacy compat).
*/
#findBucket(name: string): Record<string, string[]> | undefined {
if (this.#buckets[name]) return this.#buckets[name];
for (const key of Object.keys(this.#buckets)) {
if (new RegExp(`^${key}$`).test(name)) {
return this.#buckets[key];
}
}
return undefined;
}
}