lock.ts 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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({
  47. sessionID: input.sessionID,
  48. message: `Session ${input.sessionID} is locked`,
  49. })
  50. }
  51. const controller = new AbortController()
  52. state().locks.set(input.sessionID, {
  53. controller,
  54. created: Date.now(),
  55. })
  56. log.info("locked", { sessionID: input.sessionID })
  57. return {
  58. signal: controller.signal,
  59. abort() {
  60. controller.abort()
  61. unset({ sessionID: input.sessionID, controller })
  62. },
  63. async [Symbol.dispose]() {
  64. const removed = unset({ sessionID: input.sessionID, controller })
  65. if (removed) {
  66. log.info("unlocked", { sessionID: input.sessionID })
  67. }
  68. },
  69. }
  70. }
  71. export function abort(sessionID: string) {
  72. const lock = get(sessionID)
  73. if (!lock) return false
  74. log.info("abort", { sessionID })
  75. lock.controller.abort()
  76. state().locks.delete(sessionID)
  77. return true
  78. }
  79. export function isLocked(sessionID: string) {
  80. return get(sessionID) !== undefined
  81. }
  82. export function assertUnlocked(sessionID: string) {
  83. const lock = get(sessionID)
  84. if (!lock) return
  85. throw new LockedError({ sessionID, message: `Session ${sessionID} is locked` })
  86. }
  87. }