lock.ts 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import z from "zod"
  2. import { Instance } from "../project/instance"
  3. import { Log } from "../util/log"
  4. import { NamedError } from "../util/error"
  5. export namespace SessionLock {
  6. const log = Log.create({ service: "session.lock" })
  7. export const LockedError = NamedError.create(
  8. "SessionLockedError",
  9. z.object({
  10. sessionID: z.string(),
  11. message: z.string(),
  12. }),
  13. )
  14. type LockState = {
  15. controller: AbortController
  16. created: number
  17. }
  18. const state = Instance.state(
  19. () => {
  20. const locks = new Map<string, LockState>()
  21. return {
  22. locks,
  23. }
  24. },
  25. async (current) => {
  26. for (const [sessionID, lock] of current.locks) {
  27. log.info("force abort", { sessionID })
  28. lock.controller.abort()
  29. }
  30. current.locks.clear()
  31. },
  32. )
  33. function get(sessionID: string) {
  34. return state().locks.get(sessionID)
  35. }
  36. function unset(input: { sessionID: string; controller: AbortController }) {
  37. const lock = get(input.sessionID)
  38. if (!lock) return false
  39. if (lock.controller !== input.controller) return false
  40. state().locks.delete(input.sessionID)
  41. return true
  42. }
  43. export function acquire(input: { sessionID: string }) {
  44. const lock = get(input.sessionID)
  45. if (lock) {
  46. throw new LockedError({ sessionID: input.sessionID, message: `Session ${input.sessionID} is locked` })
  47. }
  48. const controller = new AbortController()
  49. state().locks.set(input.sessionID, {
  50. controller,
  51. created: Date.now(),
  52. })
  53. log.info("locked", { sessionID: input.sessionID })
  54. return {
  55. signal: controller.signal,
  56. abort() {
  57. controller.abort()
  58. unset({ sessionID: input.sessionID, controller })
  59. },
  60. async [Symbol.dispose]() {
  61. const removed = unset({ sessionID: input.sessionID, controller })
  62. if (removed) {
  63. log.info("unlocked", { sessionID: input.sessionID })
  64. }
  65. },
  66. }
  67. }
  68. export function abort(sessionID: string) {
  69. const lock = get(sessionID)
  70. if (!lock) return false
  71. log.info("abort", { sessionID })
  72. lock.controller.abort()
  73. state().locks.delete(sessionID)
  74. return true
  75. }
  76. export function isLocked(sessionID: string) {
  77. return get(sessionID) !== undefined
  78. }
  79. export function assertUnlocked(sessionID: string) {
  80. const lock = get(sessionID)
  81. if (!lock) return
  82. throw new LockedError({ sessionID, message: `Session ${sessionID} is locked` })
  83. }
  84. }