Last active
November 6, 2023 14:38
-
-
Save reconbot/95c08df8da8c787a9a80f4876e496ee7 to your computer and use it in GitHub Desktop.
An example of using Symbol.asyncDispose to easily cleanup remote resources when an execption occures.
This file contains 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 test, { describe } from 'node:test' | |
import { AsyncDisposableTransaction } from './AsyncDisposableTransaction' | |
import assert, { deepEqual, equal, rejects } from 'node:assert' | |
describe('AsyncDisposableTransaction', () => { | |
test('should rollback when not committed', async () => { | |
let rollbackCalled = false | |
await (async () => { | |
await using transaction = new AsyncDisposableTransaction() | |
transaction.rollback(() => { | |
rollbackCalled = true | |
}) | |
})() | |
assert(rollbackCalled) | |
}) | |
test('should not rollback when committed', async () => { | |
let rollbackCalled = false | |
await (async () => { | |
await using transaction = new AsyncDisposableTransaction() | |
transaction.rollback(() => { | |
rollbackCalled = true | |
}) | |
transaction.commit() | |
})() | |
assert(!rollbackCalled) | |
}) | |
test('should rollback in reverse order', async () => { | |
const rollbackOrder: number[] = [] | |
await (async () => { | |
await using transaction = new AsyncDisposableTransaction() | |
transaction.rollback(() => { | |
rollbackOrder.push(1) | |
}) | |
transaction.rollback(() => { | |
rollbackOrder.push(2) | |
}) | |
transaction.rollback(() => { | |
rollbackOrder.push(3) | |
}) | |
})() | |
deepEqual(rollbackOrder, [3, 2, 1]) | |
}) | |
test('should throw an error when rollback fails', async () => { | |
let rollbackCalled = false | |
await rejects((async () => { | |
await using transaction = new AsyncDisposableTransaction() | |
transaction.rollback(() => { | |
rollbackCalled = true | |
throw new Error('rollback error') | |
}) | |
})(), { | |
message: 'AsyncDisposableTransaction: 1 errors occurred during rollback', | |
}) | |
assert(rollbackCalled) | |
}) | |
test('should throw an error when multiple rollbacks fail', async () => { | |
let rollbackCalled = false | |
await rejects((async () => { | |
await using transaction = new AsyncDisposableTransaction() | |
transaction.rollback(() => { | |
rollbackCalled = true | |
throw new Error('rollback error') | |
}) | |
transaction.rollback(() => { | |
rollbackCalled = true | |
throw new Error('rollback error') | |
}) | |
})(), { | |
message: 'AsyncDisposableTransaction: 2 errors occurred during rollback', | |
}) | |
assert(rollbackCalled) | |
}) | |
}) |
This file contains 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
type RollbackFn = () => (Promise<unknown> | unknown) | |
export class AsyncDisposableTransaction { | |
private _rollback: boolean | |
rollbackFns: RollbackFn[] | |
constructor() { | |
this._rollback = true | |
this.rollbackFns = [] | |
} | |
commit() { | |
this._rollback = false | |
} | |
rollback(rollbackFn: RollbackFn) { | |
this.rollbackFns.unshift(rollbackFn) | |
} | |
async [Symbol.asyncDispose]() { | |
if (!this._rollback) { | |
return | |
} | |
const errors: Error[] = [] | |
for (const rollbackFn of this.rollbackFns) { | |
try { | |
await rollbackFn() | |
} catch (error) { | |
errors.push(error) | |
} | |
} | |
if (errors.length > 0) { | |
throw new AggregateError(errors, `AsyncDisposableTransaction: ${errors.length} errors occurred during rollback`) | |
} | |
} | |
} |
This file contains 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 { DBModels } from '../ddb/createDB' | |
import { createSubscribeToken, deleteSubscribeToken } from './createSubscribeToken' | |
import { AsyncDisposableTransaction } from '../AsyncDisposableTransaction' | |
export async function joinRoom({ | |
roomId, | |
connectionId, | |
db, | |
}: { | |
roomId: string | |
connectionId: string | |
db: DBModels | |
}) { | |
const room = await db.room.GET({ id: roomId }) | |
await using transaction = new AsyncDisposableTransaction() | |
await db.room.increment({ key: room, field: 'currentViewerCount', maxValue: room.viewerLimit, value: 1 }) | |
transaction.rollback(() => db.room.increment({ key: room, field: 'currentViewerCount', maxValue: room.viewerLimit, value: -1 })) | |
const { token, id } = await createSubscribeToken({ roomId }) | |
transaction.rollback(() => deleteSubscribeToken({ id })) | |
await db.roomMembership.create({ | |
connectionId, | |
roomId, | |
subscribeToken: token, | |
subscribeTokenId: id, | |
}) | |
transaction.rollback(() => db.roomMembership.delete({ connectionId, roomId })) | |
transaction.commit() | |
return { | |
roomId: room.id, | |
subscriberToken: token, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment