Last active
March 20, 2026 07:37
-
-
Save Sleavely/d5260984187221319950af5b86695489 to your computer and use it in GitHub Desktop.
Singleton promises
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { describe, it, expect, vi } from 'vitest' | |
| import { Thenable } from './Thenable.js' | |
| describe('Thenable', () => { | |
| class ThenableImplementation extends Thenable { | |
| execute = vi.fn().mockResolvedValue('result') | |
| } | |
| it('resolves when awaited', async () => { | |
| const t = new ThenableImplementation() | |
| const result = await t | |
| expect(result).toBe('result') | |
| }) | |
| it('only calls execute once across multiple awaits', async () => { | |
| const t = new ThenableImplementation() | |
| await t | |
| await t | |
| await t | |
| expect(t.execute).toHaveBeenCalledTimes(1) | |
| }) | |
| it('reuses the same promise across multiple awaits', async () => { | |
| const t = new ThenableImplementation() | |
| const [r1, r2] = await Promise.all([t, t]) | |
| expect(r1).toBe(r2) | |
| expect(t.execute).toHaveBeenCalledTimes(1) | |
| }) | |
| it('propagates rejection from execute', async () => { | |
| class FailingThenable extends Thenable { | |
| execute = vi.fn().mockRejectedValue(new Error('boom')) | |
| } | |
| const t = new FailingThenable() | |
| await expect(t).rejects.toThrow('boom') | |
| }) | |
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * A base class that only executes once, once you `await` it. | |
| * You could call it a Lazy Singleton Promise. | |
| */ | |
| export abstract class Thenable { | |
| #promise: Promise<unknown> | null = null; | |
| abstract execute(): Promise<unknown>; | |
| async then( | |
| resolve: PromiseConstructor['resolve'], | |
| reject: PromiseConstructor['reject'] | |
| ) { | |
| try { | |
| if (!this.#promise) { | |
| this.#promise = this.execute() | |
| } | |
| resolve(this.#promise); | |
| } catch(error) { | |
| reject(error); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment