Add nonblocking/asyn I/O operations

This commit is contained in:
KS Jannette
2026-08-01 01:06:19 -04:00
parent 90035b3568
commit ee6fa9e576
16 changed files with 711 additions and 137 deletions

View File

@@ -1,14 +1,23 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Readable } from 'node:stream';
const { mockQuery } = vi.hoisted(() => ({
const { mockQuery, mockConnect } = vi.hoisted(() => ({
mockQuery: vi.fn(),
mockConnect: vi.fn(),
}));
vi.mock('../db/index.js', () => ({
query: mockQuery,
getPool: () => ({ connect: mockConnect }),
}));
import { getAllNotes, getNoteById, createNote, createNotes } from '../db/notes.dao.js';
import {
getAllNotes,
streamAllNotes,
getNoteById,
createNote,
createNotes,
} from '../db/notes.dao.js';
const MOCK_ROWS = [
{ id: 'note_001', text: 'Login flow feels confusing', x: 193, y: 191, author: 'user_5', color: 'yellow' },
@@ -48,6 +57,58 @@ describe('notes.dao', () => {
});
});
describe('streamAllNotes', () => {
const mockClient = (rows) => {
const release = vi.fn();
const client = {
release,
query: vi.fn(() => Readable.from(rows, { objectMode: true })),
};
mockConnect.mockResolvedValue(client);
return { client, release };
};
it('should stream rows without buffering them into an array', async () => {
mockClient(MOCK_ROWS);
const stream = await streamAllNotes();
const received = [];
for await (const row of stream) received.push(row);
expect(received).toEqual(MOCK_ROWS);
});
it('should release the pooled client once the stream ends', async () => {
const { release } = mockClient(MOCK_ROWS);
const stream = await streamAllNotes();
for await (const _row of stream) { /* drain */ }
expect(release).toHaveBeenCalledOnce();
});
it('should release the pooled client when a consumer destroys the stream early', async () => {
const { release } = mockClient(MOCK_ROWS);
const stream = await streamAllNotes();
stream.destroy();
await new Promise((resolve) => stream.once('close', resolve));
expect(release).toHaveBeenCalledOnce();
});
it('should release the pooled client when starting the query throws', async () => {
const release = vi.fn();
mockConnect.mockResolvedValue({
release,
query: vi.fn(() => { throw new Error('cursor failed'); }),
});
await expect(streamAllNotes()).rejects.toThrow('cursor failed');
expect(release).toHaveBeenCalledOnce();
});
});
describe('getNoteById', () => {
it('should return a single note when found', async () => {
mockQuery.mockResolvedValue({ rows: [MOCK_ROWS[0]] });
@@ -109,5 +170,31 @@ describe('notes.dao', () => {
expect(sql).toContain('INSERT INTO notes');
expect(params).toHaveLength(12);
});
it('should return an empty array without querying when given no notes', async () => {
const result = await createNotes([]);
expect(result).toEqual([]);
expect(mockQuery).not.toHaveBeenCalled();
});
it('should split large inputs into multiple statements under the bind-parameter limit', async () => {
const many = Array.from({ length: 2500 }, (_, i) => ({
id: `note_${i}`,
text: `text ${i}`,
author: 'user_1',
}));
mockQuery.mockImplementation(async (_sql, params) => ({
rows: new Array(params.length / 6).fill(null).map((_, i) => ({ i })),
}));
const result = await createNotes(many);
expect(mockQuery).toHaveBeenCalledTimes(3);
expect(result).toHaveLength(2500);
for (const [, params] of mockQuery.mock.calls) {
expect(params.length).toBeLessThan(65535);
}
});
});
});