mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat(base): minimal async connection pool for duckdb reader pool
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { ConnectionPool } from './connection-pool';
|
||||
|
||||
describe('ConnectionPool', () => {
|
||||
it('hands out an available resource immediately', async () => {
|
||||
const pool = new ConnectionPool<string>();
|
||||
pool.init(['a', 'b']);
|
||||
expect(await pool.acquire()).toBe('b');
|
||||
expect(await pool.acquire()).toBe('a');
|
||||
});
|
||||
|
||||
it('a waiter is resolved by the next release', async () => {
|
||||
const pool = new ConnectionPool<string>();
|
||||
pool.init(['only']);
|
||||
const first = await pool.acquire();
|
||||
let resolved: string | null = null;
|
||||
const secondP = pool.acquire().then((v) => (resolved = v));
|
||||
expect(resolved).toBeNull();
|
||||
pool.release(first);
|
||||
await secondP;
|
||||
expect(resolved).toBe('only');
|
||||
});
|
||||
|
||||
it('FIFO among waiters (fair under contention)', async () => {
|
||||
const pool = new ConnectionPool<string>();
|
||||
pool.init(['only']);
|
||||
const held = await pool.acquire();
|
||||
|
||||
const order: number[] = [];
|
||||
const p1 = pool.acquire().then(() => order.push(1));
|
||||
const p2 = pool.acquire().then(() => order.push(2));
|
||||
const p3 = pool.acquire().then(() => order.push(3));
|
||||
|
||||
pool.release(held);
|
||||
await p1;
|
||||
pool.release('only'); // re-release the value that p1 got (simulated)
|
||||
await p2;
|
||||
pool.release('only');
|
||||
await p3;
|
||||
|
||||
expect(order).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('withResource acquires, invokes callback, and releases even on throw', async () => {
|
||||
const pool = new ConnectionPool<string>();
|
||||
pool.init(['one']);
|
||||
let called = false;
|
||||
await expect(
|
||||
pool.withResource(async (v) => {
|
||||
called = true;
|
||||
expect(v).toBe('one');
|
||||
throw new Error('boom');
|
||||
}),
|
||||
).rejects.toThrow('boom');
|
||||
expect(called).toBe(true);
|
||||
// resource should be back in the pool
|
||||
expect(await pool.acquire()).toBe('one');
|
||||
});
|
||||
|
||||
it('size() reports the initial count regardless of check-outs', () => {
|
||||
const pool = new ConnectionPool<string>();
|
||||
pool.init(['a', 'b', 'c']);
|
||||
expect(pool.size()).toBe(3);
|
||||
});
|
||||
|
||||
it('close() returns all held resources and rejects pending waiters', async () => {
|
||||
const pool = new ConnectionPool<string>();
|
||||
pool.init(['only']);
|
||||
const first = await pool.acquire();
|
||||
const pending = pool.acquire();
|
||||
pending.catch(() => {}); // Attach catch to prevent unhandled rejection
|
||||
const closed = pool.close();
|
||||
expect(closed).toEqual([]); // No free resources (one is checked out)
|
||||
await expect(pending).rejects.toThrow(/closed/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
type Waiter<T> = {
|
||||
resolve: (value: T) => void;
|
||||
reject: (err: Error) => void;
|
||||
};
|
||||
|
||||
/*
|
||||
* A minimal async resource pool. No external deps. Semantics:
|
||||
*
|
||||
* - `acquire()` returns an available resource immediately, or a Promise
|
||||
* that resolves when one is released.
|
||||
* - `release(r)` returns a resource. If there are pending waiters, hands
|
||||
* to the FIFO-first one. Otherwise returns to the free list.
|
||||
* - `withResource(fn)` acquires, invokes, and releases — releases even
|
||||
* if `fn` throws.
|
||||
* - `close()` rejects all pending waiters and returns the currently-free
|
||||
* resources so the owner can release them. Already-checked-out
|
||||
* resources are the caller's responsibility to finish with and re-release
|
||||
* (they'll get a no-op release, the pool being closed).
|
||||
*
|
||||
* Initial size is set via `init(resources)`. Resources must not be checked
|
||||
* out before `init` is called. `size()` reports the canonical count (does
|
||||
* not decrement on acquire).
|
||||
*/
|
||||
export class ConnectionPool<T> {
|
||||
private free: T[] = [];
|
||||
private waiters: Waiter<T>[] = [];
|
||||
private initialCount = 0;
|
||||
private closed = false;
|
||||
|
||||
init(resources: T[]): void {
|
||||
if (this.initialCount !== 0) {
|
||||
throw new Error('ConnectionPool already initialised');
|
||||
}
|
||||
this.free = [...resources];
|
||||
this.initialCount = resources.length;
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.initialCount;
|
||||
}
|
||||
|
||||
async acquire(): Promise<T> {
|
||||
if (this.closed) {
|
||||
throw new Error('ConnectionPool is closed');
|
||||
}
|
||||
if (this.free.length > 0) {
|
||||
return this.free.pop()!;
|
||||
}
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
this.waiters.push({ resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
release(resource: T): void {
|
||||
if (this.closed) {
|
||||
// Drop; caller expected this
|
||||
return;
|
||||
}
|
||||
const waiter = this.waiters.shift();
|
||||
if (waiter) {
|
||||
waiter.resolve(resource);
|
||||
} else {
|
||||
this.free.push(resource);
|
||||
}
|
||||
}
|
||||
|
||||
async withResource<R>(fn: (resource: T) => Promise<R>): Promise<R> {
|
||||
const resource = await this.acquire();
|
||||
try {
|
||||
return await fn(resource);
|
||||
} finally {
|
||||
this.release(resource);
|
||||
}
|
||||
}
|
||||
|
||||
close(): T[] {
|
||||
this.closed = true;
|
||||
for (const waiter of this.waiters) {
|
||||
waiter.reject(new Error('ConnectionPool is closed'));
|
||||
}
|
||||
this.waiters = [];
|
||||
const remaining = this.free;
|
||||
this.free = [];
|
||||
return remaining;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user