diff --git a/jest.config.js b/jest.config.js index 1eaaa78..5f9724f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,14 +1,20 @@ module.exports = { - preset: 'ts-jest', + preset: 'ts-jest/presets/js-with-ts', roots: ['src'], transformIgnorePatterns: ['/node_modules/.*\\.js', '/build/.*\\.js'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: { + target: 'es2022', + }, + }, + ], + }, testMatch: ['**/__test__/*\\.(ts|js|tsx|jsx)', '**/*\\.(spec|test)\\.(ts|js|tsx|jsx)'], collectCoverageFrom: ['src/**/*.(ts|tsx)', '!build/', '!**/node_modules', '!/coverage'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], coverageReporters: ['json', 'lcov', 'text', 'html'], - globals: { - 'ts-jest': { - isolatedModules: true, - }, - }, + setupFiles: ['core-js'], }; diff --git a/package.json b/package.json index 68088e4..4f5f9b7 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/node": "^18", "@types/react": "latest", "@types/react-dom": "latest", + "core-js": "^3.36.1", "eventemitter3": "^4", "fp-ts": "^2", "gts": "^5.2.0", diff --git a/src/concurrency/lazy-thenable.spec.ts b/src/concurrency/lazy-thenable.spec.ts index b58899f..41d6463 100644 --- a/src/concurrency/lazy-thenable.spec.ts +++ b/src/concurrency/lazy-thenable.spec.ts @@ -13,12 +13,13 @@ describe(lazyThenable, () => { await wait(0); expect(called).toEqual(1); expect(await converted).toEqual(1); + expect(await lazy1).toEqual(1); expect(called).toEqual(1); }); it('run actual action at most once', async () => { let called = 0; - const lazy2 = lazyThenable(async () => ++called); + const lazy2 = lazyThenable(() => ++called); expect(await lazy2).toEqual(1); for (let i = 0; i < 10; i++) { diff --git a/src/concurrency/lazy-thenable.ts b/src/concurrency/lazy-thenable.ts index 633c7e8..d1954ed 100644 --- a/src/concurrency/lazy-thenable.ts +++ b/src/concurrency/lazy-thenable.ts @@ -1,11 +1,16 @@ -export function lazyThenable(action: () => PromiseLike): PromiseLike { - let r: null | Promise = null; +/** + * create a lazy PromiseLike to run {@name io} at most once and only after being awaited + * @param io + * @return + */ +export function lazyThenable(io: () => T): PromiseLike> { + let r: null | Promise> = null; return { then( - onfulfilled?: ((value: T) => PromiseLike | TResult1) | undefined | null, + onfulfilled?: ((value: Awaited) => PromiseLike | TResult1) | undefined | null, onrejected?: ((reason: any) => PromiseLike | TResult2) | undefined | null, ): PromiseLike { - return (r ??= Promise.resolve(action())).then(onfulfilled, onrejected); + return (r ??= Promise.resolve(io())).then(onfulfilled, onrejected); }, }; } diff --git a/src/concurrency/lease.ts b/src/concurrency/lease.ts new file mode 100644 index 0000000..03e994a --- /dev/null +++ b/src/concurrency/lease.ts @@ -0,0 +1,8 @@ +/** + * An acquired lock or resource + */ +export interface Lease { + value: T; + dispose(): PromiseLike; + [Symbol.asyncDispose](): PromiseLike; +} diff --git a/src/concurrency/lockable.ts b/src/concurrency/lockable.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/concurrency/resource-pool.ts b/src/concurrency/resource-pool.ts index 14f6387..6bfcb6e 100644 --- a/src/concurrency/resource-pool.ts +++ b/src/concurrency/resource-pool.ts @@ -5,6 +5,8 @@ * - NOT supported: replace / refresh / timeout of tasks */ import { wait } from './timing'; +import { Lease } from './lease'; +import { lazyThenable } from './lazy-thenable'; export class ResourcePool { // can be used as a mutex @@ -37,17 +39,12 @@ export class ResourcePool { return this.consumers.length; } - async use(task: (res: T) => R): Promise { - const r = await this.borrow(); - try { - return await task(r); - } finally { - this.resources.push(r); - this.balance(); - } + async use(task: (res: T) => R): Promise> { + await using lease = await this.borrow(); + return /* must not omit 'await' here */ await task(lease.value); } - tryUse(task: (res: T | null) => R): R | Promise { + tryUse(task: (res: T | null) => R): R | Promise> { if (/** some resource is immediately available */ this.freeCount > 0) { return this.use(task); } else { @@ -88,13 +85,35 @@ export class ResourcePool { } } - private borrow(): Promise { + async borrow(): Promise> { + // TODO: implement timeout + const v = await this._borrow(); + + const _return = lazyThenable(() => this._return(v)); + + return { + value: v, + async dispose(): Promise { + return _return; + }, + [Symbol.asyncDispose]() { + return _return; + }, + }; + } + + private _borrow(): Promise { return new Promise((f) => { this.consumers.push(f); this.balance(); }); } + private _return(value: T): void { + this.resources.push(value); + this.balance(); + } + private balance(): void { while (this.resources.length && this.consumers.length) { const r = this.resources.shift()!; diff --git a/tsconfig.json b/tsconfig.json index f6bc429..0eb3c8e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "jsx": "preserve", "module": "commonjs", "moduleResolution": "node", - "target": "es2021", + "target": "es2022", "skipLibCheck": true, "rootDir": "src", "outDir": "lib" diff --git a/yarn.lock b/yarn.lock index 18d0c19..06e61c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1228,6 +1228,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +core-js@^3.36.1: + version "3.36.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578" + integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA== + create-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320"