chapter-external-update-coordinator.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. import { normalizePath } from "@/lib/path-utils"
  2. interface ChapterExternalUpdateCoordinator {
  3. runExternalUpdate(path: string, write: () => Promise<void>): Promise<number>
  4. flushBeforeLeave(path: string, write: () => Promise<void>): Promise<boolean>
  5. markEditorSession(path: string, version?: number): void
  6. }
  7. export function createChapterExternalUpdateCoordinator(): ChapterExternalUpdateCoordinator {
  8. const pathVersions = new Map<string, number>()
  9. const editorSessionVersions = new Map<string, number>()
  10. const activeExternalVersions = new Map<string, number>()
  11. const writeChains = new Map<string, Promise<void>>()
  12. function keyOf(path: string): string {
  13. return normalizePath(path)
  14. }
  15. function currentVersion(key: string): number {
  16. return pathVersions.get(key) ?? 0
  17. }
  18. function enqueue<T>(key: string, operation: () => Promise<T>): Promise<T> {
  19. const previous = writeChains.get(key) ?? Promise.resolve()
  20. const run = previous.catch(() => undefined).then(operation)
  21. const tail = run.then(() => undefined, () => undefined)
  22. writeChains.set(key, tail)
  23. void tail.finally(() => {
  24. if (writeChains.get(key) === tail) writeChains.delete(key)
  25. })
  26. return run
  27. }
  28. async function runExternalUpdate(path: string, write: () => Promise<void>): Promise<number> {
  29. const key = keyOf(path)
  30. const previousVersion = currentVersion(key)
  31. const version = previousVersion + 1
  32. pathVersions.set(key, version)
  33. activeExternalVersions.set(key, version)
  34. try {
  35. await enqueue(key, write)
  36. return version
  37. } catch (error) {
  38. if (pathVersions.get(key) === version) pathVersions.set(key, previousVersion)
  39. throw error
  40. } finally {
  41. if (activeExternalVersions.get(key) === version) activeExternalVersions.delete(key)
  42. }
  43. }
  44. function flushBeforeLeave(path: string, write: () => Promise<void>): Promise<boolean> {
  45. const key = keyOf(path)
  46. const expectedVersion = editorSessionVersions.get(key) ?? currentVersion(key)
  47. return enqueue(key, async () => {
  48. if (activeExternalVersions.has(key)) return false
  49. if (currentVersion(key) !== expectedVersion) return false
  50. await write()
  51. return true
  52. })
  53. }
  54. function markEditorSession(path: string, version = currentVersion(keyOf(path))): void {
  55. editorSessionVersions.set(keyOf(path), version)
  56. }
  57. return { runExternalUpdate, flushBeforeLeave, markEditorSession }
  58. }