feat(stdlib): new modules + eslint/tsconfig migration

- Add array/async/etc. modules and type tests; migrate to eslint flat config
  and composite tsconfig (vitest typecheck enabled).
- Fix PubSub.emit to snapshot listeners before iterating (stable EventEmitter
  semantics; avoids invoking listeners added during the same emit).
This commit is contained in:
2026-06-07 16:29:08 +07:00
parent 008d85a8fd
commit 96f4cba4a8
118 changed files with 3511 additions and 240 deletions
+18 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SyncMutex } from '.';
describe('SyncMutex', () => {
@@ -91,4 +91,21 @@ describe('SyncMutex', () => {
expect(mutex.isLocked).toBe(false);
});
it('releases the lock and rethrows when the callback throws synchronously', async () => {
await expect(mutex.execute(() => {
throw new Error('boom');
})).rejects.toThrow('boom');
expect(mutex.isLocked).toBe(false);
// The mutex must still be usable afterwards.
await expect(mutex.execute(() => Promise.resolve('ok'))).resolves.toBe('ok');
});
it('releases the lock and rejects when the callback returns a rejecting promise', async () => {
await expect(mutex.execute(() => Promise.reject(new Error('async boom')))).rejects.toThrow('async boom');
expect(mutex.isLocked).toBe(false);
});
});
+8 -3
View File
@@ -37,9 +37,14 @@ export class SyncMutex {
return;
this.lock();
const result = await callback();
this.unlock();
return result;
// try/finally guarantees the lock is released even if the callback throws or
// rejects — otherwise a single failure would wedge the guarded section forever.
try {
return await callback();
}
finally {
this.unlock();
}
}
}