indinis
Version:
A storage library using LSM trees for storage and B-trees for indices with MVCC support
134 lines (111 loc) • 5.94 kB
text/typescript
// tssrc/test/atomic_increment.test.ts
import { Indinis, increment, AtomicIncrementOperation } from '../index';
import * as fs from 'fs';
import * as path from 'path';
import { rimraf } from 'rimraf';
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const TEST_DATA_DIR_BASE = path.resolve(__dirname, '..', '..', '.test-data', 'indinis-atomic-increment');
interface StatsDoc {
id?: string;
name: string;
views: number;
likes: number;
version?: number;
lastUpdater?: string;
}
describe('Indinis Atomic Increment Functionality', () => {
let db: Indinis;
let testDataDir: string;
// --- START OF FIX ---
// The store path must have an odd number of segments.
const statsStorePath = 'stats'; // This is the collection/store.
const postItemId = 'post123'; // This is the document ID.
// The full key is constructed automatically by the API.
// --- END OF FIX ---
jest.setTimeout(30000);
beforeAll(async () => {
await fs.promises.mkdir(TEST_DATA_DIR_BASE, { recursive: true });
});
beforeEach(async () => {
const randomSuffix = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
testDataDir = path.join(TEST_DATA_DIR_BASE, `test-${randomSuffix}`);
await fs.promises.mkdir(testDataDir, { recursive: true });
db = new Indinis(testDataDir);
console.log(`\n[ATOMIC TEST START] Using data directory: ${testDataDir}`);
// --- FIX: Use the corrected paths here ---
const initialDoc: StatsDoc = { name: 'My First Post', views: 0, likes: 10 };
await db.store<StatsDoc>(statsStorePath).item(postItemId).make(initialDoc);
});
afterEach(async () => {
if (db) await db.close();
if (fs.existsSync(testDataDir)) await rimraf(testDataDir);
});
afterAll(async () => {
if (fs.existsSync(TEST_DATA_DIR_BASE)) await rimraf(TEST_DATA_DIR_BASE);
});
// --- All test `it` blocks are updated to use the corrected paths ---
it('should correctly increment a numeric field by a positive value', async () => {
const post = db.store<StatsDoc>(statsStorePath).item(postItemId);
await post.modify({ views: increment(1) });
const updatedDoc = await post.one();
expect(updatedDoc?.views).toBe(1);
});
it('should correctly increment and decrement numeric fields in the same operation', async () => {
const post = db.store<StatsDoc>(statsStorePath).item(postItemId);
await post.modify({
views: increment(5),
likes: increment(-2)
});
const updatedDoc = await post.one();
expect(updatedDoc?.views).toBe(5);
expect(updatedDoc?.likes).toBe(8);
});
it('should handle concurrent increments atomically without race conditions', async () => {
const post = db.store<StatsDoc>(statsStorePath).item(postItemId);
const numConcurrentUpdates = 25;
const incrementsPerPromise = 5;
console.log(`Spawning ${numConcurrentUpdates} concurrent writers, each incrementing 'views' by ${incrementsPerPromise}...`);
const promises: Promise<void>[] = [];
for (let i = 0; i < numConcurrentUpdates; i++) {
const promise = post.modify({ views: increment(incrementsPerPromise) });
promises.push(promise);
}
await Promise.all(promises);
const finalDoc = await post.one();
const expectedViews = numConcurrentUpdates * incrementsPerPromise;
console.log(`Final document state after concurrent increments:`, finalDoc);
expect(finalDoc?.views).toBe(expectedViews);
});
it('should correctly handle mixed atomic increments and simple merge updates in one modify call', async () => {
const post = db.store<StatsDoc>(statsStorePath).item(postItemId);
await post.modify({
views: increment(10),
lastUpdater: 'mixed_op_test'
});
const updatedDoc = await post.one();
expect(updatedDoc?.views).toBe(10);
expect(updatedDoc?.lastUpdater).toBe('mixed_op_test');
expect(updatedDoc?.likes).toBe(10);
});
it('should throw an error when trying to increment a non-existent field', async () => {
const post = db.store<StatsDoc>(statsStorePath).item(postItemId);
const modifyPromise = post.modify({ nonExistentCounter: increment(1) } as any);
await expect(modifyPromise).rejects.toThrow(/Cannot increment field 'nonExistentCounter'.*field does not exist or is not a number/);
});
it('should throw an error when trying to increment a non-numeric field', async () => {
const post = db.store<StatsDoc>(statsStorePath).item(postItemId);
await post.modify({ name: 'A Post With A Stringy Number Field', likes: 'five' as any });
const modifyPromise = post.modify({ likes: increment(1) });
await expect(modifyPromise).rejects.toThrow(/Cannot increment field 'likes'.*field does not exist or is not a number/);
});
it('should throw an error when trying to modify a non-existent document', async () => {
const nonExistentPost = db.store<StatsDoc>(statsStorePath).item('nonexistent123');
const modifyPromise = nonExistentPost.modify({ views: increment(1) });
await expect(modifyPromise).rejects.toThrow(/Cannot apply atomic update: document with key 'stats\/nonexistent123' does not exist/);
});
it('should throw a TypeError if increment() is called with a non-numeric value', () => {
expect(() => { increment('not a number' as any); }).toThrow(TypeError);
expect(() => { increment(NaN); }).toThrow(TypeError);
expect(() => { increment(Infinity); }).toThrow(TypeError);
});
});