返回 oh-my-ppt
resource-lock.ts
根目录 / src / main / agent-runtime / lock / resource-lock.ts
1 export type ResourceClaims = {
2 read?: readonly string[]
3 write?: readonly string[]
4 }
5
6 export type NormalizedResourceClaims = {
7 read: readonly string[]
8 write: readonly string[]
9 }
10
11 export type ResourceLockWaitPolicy = 'block' | 'fail'
12
13 export type ResourceLockAcquireOptions = {
14 ownerToken: symbol
15 signal?: AbortSignal
16 wait: ResourceLockWaitPolicy
17 }
18
19 export type ReleaseFunc = () => void
20
21 type Waiter = {
22 claims: NormalizedResourceClaims
23 ownerToken: symbol
24 resolve: (release: ReleaseFunc) => void
25 reject: (error: Error) => void
26 removeAbortListener: () => void
27 }
28
29 const normalizeKeys = (keys: readonly string[] | undefined): string[] => {
30 const normalized = new Set<string>()
31 for (const key of keys || []) {
32 const value = key.trim()
33 if (!value) throw new Error('Resource claim key must not be empty')
34 normalized.add(value)
35 }
36 return [...normalized]
37 }
38
39 export const normalizeResourceClaims = (claims: ResourceClaims): NormalizedResourceClaims => {
40 const write = normalizeKeys(claims.write)
41 const writeKeys = new Set(write)
42 const read = normalizeKeys(claims.read).filter((key) => !writeKeys.has(key))
43 return { read, write }
44 }
45
46 const intersects = (left: readonly string[], right: readonly string[]): boolean => {
47 if (left.length === 0 || right.length === 0) return false
48 const rightKeys = new Set(right)
49 return left.some((key) => rightKeys.has(key))
50 }
51
52 export const resourceClaimsConflict = (left: ResourceClaims, right: ResourceClaims): boolean => {
53 const normalizedLeft = normalizeResourceClaims(left)
54 const normalizedRight = normalizeResourceClaims(right)
55 return (
56 intersects(normalizedLeft.write, normalizedRight.write) ||
57 intersects(normalizedLeft.write, normalizedRight.read) ||
58 intersects(normalizedLeft.read, normalizedRight.write)
59 )
60 }
61
62 export const createAbortError = (): Error => {
63 const error = new Error('The operation was aborted')
64 error.name = 'AbortError'
65 return error
66 }
67
68 /**
69 * Reader/writer lock for complete resource claim sets. A waiter either receives every requested
70 * key or none of them; there is intentionally no public partial-acquire API.
71 */
72 export class ResourceLock {
73 private readonly readersByKey = new Map<string, Set<symbol>>()
74 private readonly writerByKey = new Map<string, symbol>()
75 private readonly ownerStates = new Map<symbol, 'waiting' | 'active'>()
76 private readonly waiters: Waiter[] = []
77
78 async acquire(
79 claims: ResourceClaims,
80 options: ResourceLockAcquireOptions
81 ): Promise<ReleaseFunc | null> {
82 const normalizedClaims = normalizeResourceClaims(claims)
83 if (options.signal?.aborted) throw createAbortError()
84 if (this.ownerStates.has(options.ownerToken)) {
85 throw new Error('ResourceLock does not support nested claims for the same owner')
86 }
87
88 if (options.wait === 'fail') {
89 if (this.hasActiveConflict(normalizedClaims) || this.hasWaitingConflict(normalizedClaims)) {
90 return null
91 }
92 return this.grant(normalizedClaims, options.ownerToken)
93 }
94
95 return new Promise<ReleaseFunc>((resolve, reject) => {
96 let waiter: Waiter
97 const removeAbortListener = (): void => {
98 options.signal?.removeEventListener('abort', onAbort)
99 }
100 const onAbort = (): void => {
101 const waiterIndex = this.waiters.indexOf(waiter)
102 if (waiterIndex === -1) return
103 this.waiters.splice(waiterIndex, 1)
104 this.ownerStates.delete(options.ownerToken)
105 removeAbortListener()
106 reject(createAbortError())
107 this.drain()
108 }
109
110 waiter = {
111 claims: normalizedClaims,
112 ownerToken: options.ownerToken,
113 resolve,
114 reject,
115 removeAbortListener
116 }
117 this.ownerStates.set(options.ownerToken, 'waiting')
118 this.waiters.push(waiter)
119 options.signal?.addEventListener('abort', onAbort, { once: true })
120 this.drain()
121 })
122 }
123
124 /**
125 * Immediate all-or-nothing acquisition used by the compatibility adapter
126 * while legacy IPC handlers still expose synchronous busy responses.
127 */
128 tryAcquire(
129 claims: ResourceClaims,
130 options: Omit<ResourceLockAcquireOptions, 'wait'>
131 ): ReleaseFunc | null {
132 const normalizedClaims = normalizeResourceClaims(claims)
133 if (options.signal?.aborted) throw createAbortError()
134 if (this.ownerStates.has(options.ownerToken)) {
135 throw new Error('ResourceLock does not support nested claims for the same owner')
136 }
137 if (this.hasActiveConflict(normalizedClaims) || this.hasWaitingConflict(normalizedClaims)) {
138 return null
139 }
140 return this.grant(normalizedClaims, options.ownerToken)
141 }
142
143 private hasActiveConflict(claims: NormalizedResourceClaims): boolean {
144 return (
145 claims.write.some(
146 (key) => this.writerByKey.has(key) || (this.readersByKey.get(key)?.size || 0) > 0
147 ) || claims.read.some((key) => this.writerByKey.has(key))
148 )
149 }
150
151 private hasWaitingConflict(claims: NormalizedResourceClaims): boolean {
152 return this.waiters.some((waiter) => resourceClaimsConflict(claims, waiter.claims))
153 }
154
155 private drain(): void {
156 const earlierBlockedClaims: NormalizedResourceClaims[] = []
157
158 for (let index = 0; index < this.waiters.length; ) {
159 const waiter = this.waiters[index]
160 const blockedByActive = this.hasActiveConflict(waiter.claims)
161 const blockedByEarlierWaiter = earlierBlockedClaims.some((claims) =>
162 resourceClaimsConflict(waiter.claims, claims)
163 )
164
165 if (blockedByActive || blockedByEarlierWaiter) {
166 earlierBlockedClaims.push(waiter.claims)
167 index += 1
168 continue
169 }
170
171 this.waiters.splice(index, 1)
172 waiter.removeAbortListener()
173 waiter.resolve(this.grant(waiter.claims, waiter.ownerToken))
174 }
175 }
176
177 private grant(claims: NormalizedResourceClaims, ownerToken: symbol): ReleaseFunc {
178 this.ownerStates.set(ownerToken, 'active')
179 for (const key of claims.write) {
180 this.writerByKey.set(key, ownerToken)
181 }
182 for (const key of claims.read) {
183 const readers = this.readersByKey.get(key) || new Set<symbol>()
184 readers.add(ownerToken)
185 this.readersByKey.set(key, readers)
186 }
187
188 let released = false
189 return () => {
190 if (released) return
191 released = true
192
193 for (const key of claims.write) {
194 if (this.writerByKey.get(key) === ownerToken) this.writerByKey.delete(key)
195 }
196 for (const key of claims.read) {
197 const readers = this.readersByKey.get(key)
198 if (!readers) continue
199 readers.delete(ownerToken)
200 if (readers.size === 0) this.readersByKey.delete(key)
201 }
202 this.ownerStates.delete(ownerToken)
203 this.drain()
204 }
205 }
206 }
207
207 lines TYPESCRIPT