Skip to content

Instantly share code, notes, and snippets.

@reconbot
Last active November 6, 2023 14:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save reconbot/95c08df8da8c787a9a80f4876e496ee7 to your computer and use it in GitHub Desktop.
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.
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)
})
})
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`)
}
}
}
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