@planet-matrix/mobius-model 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +123 -36
- package/dist/index.js +45 -4
- package/dist/index.js.map +183 -11
- package/oxlint.config.ts +6 -0
- package/package.json +16 -10
- package/src/abort/README.md +92 -0
- package/src/abort/abort-manager.ts +278 -0
- package/src/abort/abort-signal-listener-manager.ts +81 -0
- package/src/abort/index.ts +2 -0
- package/src/basic/README.md +69 -118
- package/src/basic/function.ts +81 -62
- package/src/basic/is.ts +152 -71
- package/src/basic/promise.ts +29 -8
- package/src/basic/string.ts +2 -33
- package/src/color/README.md +105 -0
- package/src/color/index.ts +3 -0
- package/src/color/internal.ts +42 -0
- package/src/color/rgb/analyze.ts +236 -0
- package/src/color/rgb/construct.ts +130 -0
- package/src/color/rgb/convert.ts +227 -0
- package/src/color/rgb/derive.ts +303 -0
- package/src/color/rgb/index.ts +6 -0
- package/src/color/rgb/internal.ts +208 -0
- package/src/color/rgb/parse.ts +302 -0
- package/src/color/rgb/serialize.ts +144 -0
- package/src/color/types.ts +57 -0
- package/src/color/xyz/analyze.ts +80 -0
- package/src/color/xyz/construct.ts +19 -0
- package/src/color/xyz/convert.ts +71 -0
- package/src/color/xyz/index.ts +3 -0
- package/src/color/xyz/internal.ts +23 -0
- package/src/css/README.md +93 -0
- package/src/css/class.ts +559 -0
- package/src/css/index.ts +1 -0
- package/src/encoding/README.md +66 -79
- package/src/encoding/base64.ts +13 -4
- package/src/environment/README.md +97 -0
- package/src/environment/basic.ts +26 -0
- package/src/environment/device.ts +311 -0
- package/src/environment/feature.ts +285 -0
- package/src/environment/geo.ts +337 -0
- package/src/environment/index.ts +7 -0
- package/src/environment/runtime.ts +400 -0
- package/src/environment/snapshot.ts +60 -0
- package/src/environment/variable.ts +239 -0
- package/src/event/README.md +90 -0
- package/src/event/class-event-proxy.ts +228 -0
- package/src/event/common.ts +19 -0
- package/src/event/event-manager.ts +203 -0
- package/src/event/index.ts +4 -0
- package/src/event/instance-event-proxy.ts +186 -0
- package/src/event/internal.ts +24 -0
- package/src/exception/README.md +96 -0
- package/src/exception/browser.ts +219 -0
- package/src/exception/index.ts +4 -0
- package/src/exception/nodejs.ts +169 -0
- package/src/exception/normalize.ts +106 -0
- package/src/exception/types.ts +99 -0
- package/src/identifier/README.md +92 -0
- package/src/identifier/id.ts +119 -0
- package/src/identifier/index.ts +2 -0
- package/src/identifier/uuid.ts +187 -0
- package/src/index.ts +16 -1
- package/src/log/README.md +79 -0
- package/src/log/index.ts +5 -0
- package/src/log/log-emitter.ts +72 -0
- package/src/log/log-record.ts +10 -0
- package/src/log/log-scheduler.ts +74 -0
- package/src/log/log-type.ts +8 -0
- package/src/log/logger.ts +543 -0
- package/src/orchestration/README.md +89 -0
- package/src/orchestration/coordination/barrier.ts +214 -0
- package/src/orchestration/coordination/count-down-latch.ts +215 -0
- package/src/orchestration/coordination/errors.ts +98 -0
- package/src/orchestration/coordination/index.ts +16 -0
- package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
- package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
- package/src/orchestration/coordination/keyed-lock.ts +168 -0
- package/src/orchestration/coordination/mutex.ts +257 -0
- package/src/orchestration/coordination/permit.ts +127 -0
- package/src/orchestration/coordination/read-write-lock.ts +444 -0
- package/src/orchestration/coordination/semaphore.ts +280 -0
- package/src/orchestration/index.ts +1 -0
- package/src/random/README.md +55 -86
- package/src/random/index.ts +1 -1
- package/src/random/string.ts +35 -0
- package/src/reactor/README.md +4 -0
- package/src/reactor/reactor-core/primitive.ts +9 -9
- package/src/reactor/reactor-core/reactive-system.ts +5 -5
- package/src/singleton/README.md +79 -0
- package/src/singleton/factory.ts +55 -0
- package/src/singleton/index.ts +2 -0
- package/src/singleton/manager.ts +204 -0
- package/src/storage/README.md +107 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/table.ts +449 -0
- package/src/timer/README.md +86 -0
- package/src/timer/expiration/expiration-manager.ts +594 -0
- package/src/timer/expiration/index.ts +3 -0
- package/src/timer/expiration/min-heap.ts +208 -0
- package/src/timer/expiration/remaining-manager.ts +241 -0
- package/src/timer/index.ts +1 -0
- package/src/type/README.md +54 -307
- package/src/type/class.ts +2 -2
- package/src/type/index.ts +14 -14
- package/src/type/is.ts +265 -2
- package/src/type/object.ts +37 -0
- package/src/type/string.ts +7 -2
- package/src/type/tuple.ts +6 -6
- package/src/type/union.ts +16 -0
- package/src/web/README.md +77 -0
- package/src/web/capture.ts +35 -0
- package/src/web/clipboard.ts +97 -0
- package/src/web/dom.ts +117 -0
- package/src/web/download.ts +16 -0
- package/src/web/event.ts +46 -0
- package/src/web/index.ts +10 -0
- package/src/web/local-storage.ts +113 -0
- package/src/web/location.ts +28 -0
- package/src/web/permission.ts +172 -0
- package/src/web/script-loader.ts +432 -0
- package/tests/unit/abort/abort-manager.spec.ts +225 -0
- package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
- package/tests/unit/basic/array.spec.ts +1 -1
- package/tests/unit/basic/stream.spec.ts +1 -1
- package/tests/unit/basic/string.spec.ts +0 -9
- package/tests/unit/color/rgb/analyze.spec.ts +110 -0
- package/tests/unit/color/rgb/construct.spec.ts +56 -0
- package/tests/unit/color/rgb/convert.spec.ts +60 -0
- package/tests/unit/color/rgb/derive.spec.ts +103 -0
- package/tests/unit/color/rgb/parse.spec.ts +66 -0
- package/tests/unit/color/rgb/serialize.spec.ts +46 -0
- package/tests/unit/color/xyz/analyze.spec.ts +33 -0
- package/tests/unit/color/xyz/construct.spec.ts +10 -0
- package/tests/unit/color/xyz/convert.spec.ts +18 -0
- package/tests/unit/css/class.spec.ts +157 -0
- package/tests/unit/environment/basic.spec.ts +20 -0
- package/tests/unit/environment/device.spec.ts +146 -0
- package/tests/unit/environment/feature.spec.ts +388 -0
- package/tests/unit/environment/geo.spec.ts +111 -0
- package/tests/unit/environment/runtime.spec.ts +364 -0
- package/tests/unit/environment/snapshot.spec.ts +4 -0
- package/tests/unit/environment/variable.spec.ts +190 -0
- package/tests/unit/event/class-event-proxy.spec.ts +225 -0
- package/tests/unit/event/event-manager.spec.ts +246 -0
- package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
- package/tests/unit/exception/browser.spec.ts +213 -0
- package/tests/unit/exception/nodejs.spec.ts +144 -0
- package/tests/unit/exception/normalize.spec.ts +57 -0
- package/tests/unit/identifier/id.spec.ts +71 -0
- package/tests/unit/identifier/uuid.spec.ts +85 -0
- package/tests/unit/log/log-emitter.spec.ts +33 -0
- package/tests/unit/log/log-scheduler.spec.ts +40 -0
- package/tests/unit/log/log-type.spec.ts +7 -0
- package/tests/unit/log/logger.spec.ts +222 -0
- package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
- package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
- package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
- package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
- package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
- package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
- package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
- package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
- package/tests/unit/random/string.spec.ts +11 -0
- package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
- package/tests/unit/reactor/preact-signal.spec.ts +1 -2
- package/tests/unit/singleton/singleton.spec.ts +49 -0
- package/tests/unit/storage/table.spec.ts +620 -0
- package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
- package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
- package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
- package/.oxlintrc.json +0 -5
- package/src/random/uuid.ts +0 -103
- package/tests/unit/random/uuid.spec.ts +0 -37
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表示可挂入等待队列的节点最小结构。
|
|
3
|
+
*
|
|
4
|
+
* 队列本身不关心节点承载的业务数据,只要求节点能记录自己是否在队列中,以及前后相邻节点。
|
|
5
|
+
*/
|
|
6
|
+
export interface InternalWaitQueueEntry<Entry extends InternalWaitQueueEntry<Entry>> {
|
|
7
|
+
isQueued: boolean
|
|
8
|
+
previous: Entry | undefined
|
|
9
|
+
next: Entry | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 表示面向等待者节点的双向链表队列。
|
|
14
|
+
*/
|
|
15
|
+
export class InternalWaitQueue<Entry extends InternalWaitQueueEntry<Entry>> {
|
|
16
|
+
private head: Entry | undefined
|
|
17
|
+
private tail: Entry | undefined
|
|
18
|
+
|
|
19
|
+
private count: number
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.head = undefined
|
|
23
|
+
this.tail = undefined
|
|
24
|
+
this.count = 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 查看当前队头节点但不移除。
|
|
29
|
+
*/
|
|
30
|
+
peek(): Entry | undefined {
|
|
31
|
+
return this.head
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 读取队列中的节点数量。
|
|
36
|
+
*/
|
|
37
|
+
getCount(): number {
|
|
38
|
+
return this.count
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 判断队列是否为空。
|
|
43
|
+
*/
|
|
44
|
+
isEmpty(): boolean {
|
|
45
|
+
return this.head === undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 把节点追加到队尾。
|
|
50
|
+
*/
|
|
51
|
+
enqueue(entry: Entry): void {
|
|
52
|
+
entry.previous = this.tail
|
|
53
|
+
entry.next = undefined
|
|
54
|
+
entry.isQueued = true
|
|
55
|
+
|
|
56
|
+
if (this.tail === undefined) {
|
|
57
|
+
this.head = entry
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.tail.next = entry
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.tail = entry
|
|
64
|
+
this.count = this.count + 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 从队列中移除指定节点。
|
|
69
|
+
*
|
|
70
|
+
* 该操作不要求节点位于队头,因此适合被 timeout 或 abort 的等待者直接自移除。
|
|
71
|
+
*/
|
|
72
|
+
remove(entry: Entry): void {
|
|
73
|
+
if (entry.isQueued === false) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (entry.previous === undefined) {
|
|
78
|
+
this.head = entry.next
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
entry.previous.next = entry.next
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (entry.next === undefined) {
|
|
85
|
+
this.tail = entry.previous
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
entry.next.previous = entry.previous
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
entry.previous = undefined
|
|
92
|
+
entry.next = undefined
|
|
93
|
+
entry.isQueued = false
|
|
94
|
+
this.count = this.count - 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 取出并返回队头节点。
|
|
99
|
+
*/
|
|
100
|
+
shift(): Entry | undefined {
|
|
101
|
+
const head = this.head
|
|
102
|
+
if (head === undefined) {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.remove(head)
|
|
107
|
+
return head
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { MutexAcquireOptions, MutexOptions, MutexPermit } from "./mutex.ts"
|
|
2
|
+
import type { KeyedPermitDetails } from "./permit.ts"
|
|
3
|
+
|
|
4
|
+
import { Mutex } from "./mutex.ts"
|
|
5
|
+
import { Permit } from "./permit.ts"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 表示 `KeyedLock` 成功获取后的释放句柄。
|
|
9
|
+
*
|
|
10
|
+
* 句柄中会保留当前命中的 key,便于调试或上层记录持锁对象。
|
|
11
|
+
*/
|
|
12
|
+
export type KeyedLockPermit<Key> = Permit<KeyedPermitDetails<"keyed-lock", Key>>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 表示单个 key 对应的内部状态。
|
|
16
|
+
*
|
|
17
|
+
* 每个 key 都维护一个独立的 `Mutex`,并通过引用计数判断是否可以从状态表中清理。
|
|
18
|
+
*/
|
|
19
|
+
interface InternalKeyedLockState<Key> {
|
|
20
|
+
key: Key
|
|
21
|
+
mutex: Mutex
|
|
22
|
+
referenceCount: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 表示构造 `KeyedLock` 时支持的配置。
|
|
27
|
+
*
|
|
28
|
+
* 当前直接复用 `Mutex` 的配置项,以保证每个 key 下的锁行为一致。
|
|
29
|
+
*/
|
|
30
|
+
export interface KeyedLockOptions extends MutexOptions {
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 表示按 key 维度拆分互斥域的锁。
|
|
35
|
+
*
|
|
36
|
+
* 相同 key 的任务会串行执行,不同 key 之间则互不影响,适合做资源 ID、租户 ID、文件路径等粒度的独占控制。
|
|
37
|
+
*/
|
|
38
|
+
export class KeyedLock<Key> {
|
|
39
|
+
private readonly stateMap: Map<Key, InternalKeyedLockState<Key>>
|
|
40
|
+
|
|
41
|
+
private readonly mutexOptions: MutexOptions
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 创建一个按 key 分桶的互斥锁管理器。
|
|
45
|
+
*/
|
|
46
|
+
constructor(options: KeyedLockOptions = {}) {
|
|
47
|
+
this.stateMap = new Map<Key, InternalKeyedLockState<Key>>()
|
|
48
|
+
this.mutexOptions = options
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 判断当前是否已经为指定 key 建立内部状态。
|
|
53
|
+
*/
|
|
54
|
+
hasKey(key: Key): boolean {
|
|
55
|
+
return this.stateMap.has(key)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 读取当前仍被追踪的 key 数量。
|
|
60
|
+
*/
|
|
61
|
+
getKeyCount(): number {
|
|
62
|
+
return this.stateMap.size
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 取得指定 key 的状态;若不存在则惰性创建。
|
|
67
|
+
*/
|
|
68
|
+
private getOrCreateState(key: Key): InternalKeyedLockState<Key> {
|
|
69
|
+
const existingState = this.stateMap.get(key)
|
|
70
|
+
if (existingState !== undefined) {
|
|
71
|
+
return existingState
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const state: InternalKeyedLockState<Key> = {
|
|
75
|
+
key,
|
|
76
|
+
mutex: new Mutex(this.mutexOptions),
|
|
77
|
+
referenceCount: 0,
|
|
78
|
+
}
|
|
79
|
+
this.stateMap.set(key, state)
|
|
80
|
+
return state
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 在某个 key 不再被持有且没有等待者时回收对应状态。
|
|
85
|
+
*/
|
|
86
|
+
private cleanupState(state: InternalKeyedLockState<Key>): void {
|
|
87
|
+
if (state.referenceCount !== 0) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (state.mutex.isLocked() === true || state.mutex.getPendingCount() !== 0) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const currentState = this.stateMap.get(state.key)
|
|
96
|
+
if (currentState === state) {
|
|
97
|
+
this.stateMap.delete(state.key)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 基于底层 `Mutex` permit 构造带 key 元信息的释放句柄。
|
|
103
|
+
*/
|
|
104
|
+
private buildPermit(state: InternalKeyedLockState<Key>, permit: MutexPermit): KeyedLockPermit<Key> {
|
|
105
|
+
return new Permit<KeyedPermitDetails<"keyed-lock", Key>>({
|
|
106
|
+
details: {
|
|
107
|
+
coordination: "keyed-lock",
|
|
108
|
+
key: state.key,
|
|
109
|
+
},
|
|
110
|
+
duplicateReleaseMessage: "KeyedLock permit release was called more than once.",
|
|
111
|
+
onDuplicateRelease: this.mutexOptions.onDuplicateRelease,
|
|
112
|
+
onRelease: (): void => {
|
|
113
|
+
permit.release()
|
|
114
|
+
state.referenceCount = state.referenceCount - 1
|
|
115
|
+
this.cleanupState(state)
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 尝试立即获取指定 key 的独占锁。
|
|
122
|
+
*/
|
|
123
|
+
tryAcquire(key: Key): KeyedLockPermit<Key> | undefined {
|
|
124
|
+
const state = this.getOrCreateState(key)
|
|
125
|
+
const permit = state.mutex.tryAcquire()
|
|
126
|
+
|
|
127
|
+
if (permit === undefined) {
|
|
128
|
+
this.cleanupState(state)
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
state.referenceCount = state.referenceCount + 1
|
|
133
|
+
return this.buildPermit(state, permit)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 等待直到成功获取指定 key 的独占锁。
|
|
138
|
+
*/
|
|
139
|
+
async acquire(key: Key, options: MutexAcquireOptions = {}): Promise<KeyedLockPermit<Key>> {
|
|
140
|
+
const state = this.getOrCreateState(key)
|
|
141
|
+
state.referenceCount = state.referenceCount + 1
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const permit = await state.mutex.acquire(options)
|
|
145
|
+
return this.buildPermit(state, permit)
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
state.referenceCount = state.referenceCount - 1
|
|
149
|
+
this.cleanupState(state)
|
|
150
|
+
throw error
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 在指定 key 的独占锁保护下执行回调。
|
|
156
|
+
*/
|
|
157
|
+
async runExclusive<T>(key: Key, callback: () => Promise<T>, options?: MutexAcquireOptions): Promise<T>
|
|
158
|
+
async runExclusive<T>(key: Key, callback: () => T, options?: MutexAcquireOptions): Promise<T>
|
|
159
|
+
async runExclusive<T>(key: Key, callback: () => Promise<T> | T, options: MutexAcquireOptions = {}): Promise<T> {
|
|
160
|
+
const permit = await this.acquire(key, options)
|
|
161
|
+
try {
|
|
162
|
+
return await callback()
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
permit.release()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { CoordinationPermitDetails } from "./permit.ts"
|
|
2
|
+
import type { InternalWaitQueueEntry } from "./internal/wait-queue.ts"
|
|
3
|
+
import type { InternalWaitConstraintsEntry, InternalWaitConstraintsOptions } from "./internal/wait-constraints.ts"
|
|
4
|
+
|
|
5
|
+
import { InternalWaitQueue } from "./internal/wait-queue.ts"
|
|
6
|
+
import { Permit } from "./permit.ts"
|
|
7
|
+
import {
|
|
8
|
+
internalAssertTimeoutOption,
|
|
9
|
+
internalBindWaitConstraints,
|
|
10
|
+
internalCleanupWaitConstraints,
|
|
11
|
+
internalThrowIfAborted,
|
|
12
|
+
} from "./internal/wait-constraints.ts"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 链表形式的等待节点。
|
|
16
|
+
*
|
|
17
|
+
* 之所以不使用数组,是因为等待中的任务可能会被 timeout 或 abort 主动取消。
|
|
18
|
+
* 使用双向链表后,任意节点都可以在 O(1) 时间内从等待队列中移除,不需要做
|
|
19
|
+
* `indexOf + splice` 这样的线性扫描。
|
|
20
|
+
*/
|
|
21
|
+
interface InternalMutexQueueEntry extends InternalWaitQueueEntry<InternalMutexQueueEntry>, InternalWaitConstraintsEntry {
|
|
22
|
+
resolve: (permit: MutexPermit) => void
|
|
23
|
+
reject: (reason: unknown) => void
|
|
24
|
+
isSettled: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 表示获取互斥锁时可选的等待控制参数。
|
|
29
|
+
*/
|
|
30
|
+
export interface MutexAcquireOptions extends InternalWaitConstraintsOptions {
|
|
31
|
+
/**
|
|
32
|
+
* 等待锁的超时时间,单位为毫秒。
|
|
33
|
+
*/
|
|
34
|
+
timeout?: number | undefined
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 等待期间使用的中止信号;一旦中止,会将本次等待从队列中移除并拒绝。
|
|
38
|
+
*/
|
|
39
|
+
abortSignal?: AbortSignal | undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 表示 `Mutex` 成功获取后的占用句柄。
|
|
44
|
+
*/
|
|
45
|
+
export type MutexPermit = Permit<CoordinationPermitDetails<"mutex">>
|
|
46
|
+
|
|
47
|
+
const INTERNAL_MUTEX_DUPLICATE_RELEASE_MESSAGE = "Mutex permit release was called more than once."
|
|
48
|
+
/**
|
|
49
|
+
* 表示 `Mutex` 在检测到重复 `release()` 误用时使用的处理函数。
|
|
50
|
+
*/
|
|
51
|
+
export type MutexDuplicateReleaseHandler = (message: string) => void
|
|
52
|
+
/**
|
|
53
|
+
* 表示构造 `Mutex` 时可选的运行时配置。
|
|
54
|
+
*/
|
|
55
|
+
export interface MutexOptions {
|
|
56
|
+
/**
|
|
57
|
+
* 重复调用同一个 `release()` 时的可选处理器。
|
|
58
|
+
*
|
|
59
|
+
* 未提供时,`Mutex` 会保持静默;提供后,每个 `Mutex` 实例最多调用一次。
|
|
60
|
+
*/
|
|
61
|
+
onDuplicateRelease?: MutexDuplicateReleaseHandler | undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 表示用于串行化异步临界区的互斥锁。
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```
|
|
69
|
+
* const mutex = new Mutex()
|
|
70
|
+
* let counter = 0
|
|
71
|
+
*
|
|
72
|
+
* await Promise.all([
|
|
73
|
+
* mutex.runExclusive(async () => {
|
|
74
|
+
* const current = counter
|
|
75
|
+
* await Promise.resolve()
|
|
76
|
+
* counter = current + 1
|
|
77
|
+
* }),
|
|
78
|
+
* mutex.runExclusive(async () => {
|
|
79
|
+
* const current = counter
|
|
80
|
+
* await Promise.resolve()
|
|
81
|
+
* counter = current + 1
|
|
82
|
+
* }),
|
|
83
|
+
* ])
|
|
84
|
+
*
|
|
85
|
+
* // Expect: 2
|
|
86
|
+
* const example1 = counter
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export class Mutex {
|
|
90
|
+
/**
|
|
91
|
+
* 等待队列中的每个节点都代表一个尚未拿到锁的等待者。
|
|
92
|
+
*/
|
|
93
|
+
private readonly queue: InternalWaitQueue<InternalMutexQueueEntry>
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 当前是否已有持锁者进入临界区。
|
|
97
|
+
*/
|
|
98
|
+
private internalIsLocked: boolean
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 重复 release 误用的可选处理器。
|
|
102
|
+
*/
|
|
103
|
+
private readonly onDuplicateRelease: MutexDuplicateReleaseHandler | undefined
|
|
104
|
+
|
|
105
|
+
constructor(options: MutexOptions = {}) {
|
|
106
|
+
this.queue = new InternalWaitQueue<InternalMutexQueueEntry>()
|
|
107
|
+
this.internalIsLocked = false
|
|
108
|
+
this.onDuplicateRelease = options.onDuplicateRelease
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 读取当前锁是否已经被持有。
|
|
113
|
+
*/
|
|
114
|
+
isLocked(): boolean {
|
|
115
|
+
return this.internalIsLocked
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 读取当前等待队列中的任务数量。
|
|
120
|
+
*/
|
|
121
|
+
getPendingCount(): number {
|
|
122
|
+
return this.queue.getCount()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 尝试把锁交给下一个可运行的等待者。
|
|
127
|
+
*
|
|
128
|
+
* 这里始终从队头取任务,保持 FIFO;若遇到已失效节点,则跳过直到找到下一个有效等待者。
|
|
129
|
+
*/
|
|
130
|
+
private dispatch(): void {
|
|
131
|
+
if (this.internalIsLocked === true) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
while (this.queue.isEmpty() === false) {
|
|
136
|
+
const nextEntry = this.queue.shift()
|
|
137
|
+
if (nextEntry === undefined) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (nextEntry.isSettled === true) {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.internalIsLocked = true
|
|
146
|
+
nextEntry.isSettled = true
|
|
147
|
+
internalCleanupWaitConstraints(nextEntry)
|
|
148
|
+
nextEntry.resolve(this.buildPermit())
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 为当前持锁者构造一个 permit。
|
|
155
|
+
*/
|
|
156
|
+
private buildPermit(): MutexPermit {
|
|
157
|
+
return new Permit<CoordinationPermitDetails<"mutex">>({
|
|
158
|
+
details: { coordination: "mutex" },
|
|
159
|
+
duplicateReleaseMessage: INTERNAL_MUTEX_DUPLICATE_RELEASE_MESSAGE,
|
|
160
|
+
onDuplicateRelease: this.onDuplicateRelease,
|
|
161
|
+
onRelease: (): void => {
|
|
162
|
+
this.internalIsLocked = false
|
|
163
|
+
this.dispatch()
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 以失败状态结束一个等待节点。
|
|
170
|
+
*
|
|
171
|
+
* 该逻辑会统一处理:标记结束、清理约束、从队列摘除、拒绝 Promise,最后尝试继续调度后继等待者。
|
|
172
|
+
*/
|
|
173
|
+
private rejectEntry(entry: InternalMutexQueueEntry, reason: unknown): void {
|
|
174
|
+
if (entry.isSettled === true) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
entry.isSettled = true
|
|
179
|
+
internalCleanupWaitConstraints(entry)
|
|
180
|
+
this.queue.remove(entry)
|
|
181
|
+
entry.reject(reason)
|
|
182
|
+
this.dispatch()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 尝试立即获取锁;若当前不可用则返回 `undefined`。
|
|
187
|
+
*/
|
|
188
|
+
tryAcquire(): MutexPermit | undefined {
|
|
189
|
+
if (this.internalIsLocked === true || this.queue.isEmpty() === false) {
|
|
190
|
+
return undefined
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.internalIsLocked = true
|
|
194
|
+
return this.buildPermit()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 等待直到成功获取锁。
|
|
199
|
+
*
|
|
200
|
+
* 当锁暂时不可用时,调用方会进入 FIFO 等待队列;一旦前面的持锁者释放,队头等待者会被唤醒。
|
|
201
|
+
*/
|
|
202
|
+
async acquire(options: MutexAcquireOptions = {}): Promise<MutexPermit> {
|
|
203
|
+
internalAssertTimeoutOption("Mutex acquire", options.timeout)
|
|
204
|
+
|
|
205
|
+
internalThrowIfAborted("Mutex acquire", options.abortSignal)
|
|
206
|
+
|
|
207
|
+
const immediatePermit = this.tryAcquire()
|
|
208
|
+
if (immediatePermit !== undefined) {
|
|
209
|
+
return immediatePermit
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const permit = await new Promise<MutexPermit>((resolve, reject) => {
|
|
213
|
+
const entry: InternalMutexQueueEntry = {
|
|
214
|
+
resolve,
|
|
215
|
+
reject,
|
|
216
|
+
isSettled: false,
|
|
217
|
+
isQueued: false,
|
|
218
|
+
previous: undefined,
|
|
219
|
+
next: undefined,
|
|
220
|
+
timeoutId: undefined,
|
|
221
|
+
abortSignal: undefined,
|
|
222
|
+
abortListener: undefined,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
internalBindWaitConstraints(entry, options, {
|
|
226
|
+
abortOperation: "Mutex acquire",
|
|
227
|
+
timeoutOperation: "Mutex acquire",
|
|
228
|
+
onAbort: error => {
|
|
229
|
+
this.rejectEntry(entry, error)
|
|
230
|
+
},
|
|
231
|
+
onTimeout: error => {
|
|
232
|
+
this.rejectEntry(entry, error)
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
this.queue.enqueue(entry)
|
|
236
|
+
this.dispatch()
|
|
237
|
+
})
|
|
238
|
+
return permit
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 在独占锁保护下执行回调。
|
|
243
|
+
*
|
|
244
|
+
* 无论回调成功还是抛错,permit 都会在 `finally` 中释放,避免把锁永久占住。
|
|
245
|
+
*/
|
|
246
|
+
async runExclusive<T>(callback: () => Promise<T>, options?: MutexAcquireOptions): Promise<T>
|
|
247
|
+
async runExclusive<T>(callback: () => T, options?: MutexAcquireOptions): Promise<T>
|
|
248
|
+
async runExclusive<T>(callback: () => Promise<T> | T, options: MutexAcquireOptions = {}): Promise<T> {
|
|
249
|
+
const permit = await this.acquire(options)
|
|
250
|
+
try {
|
|
251
|
+
return await callback()
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
permit.release()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const INTERNAL_DEFAULT_DUPLICATE_RELEASE_MESSAGE = "Permit release was called more than once."
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 表示 `Permit` 在检测到重复 `release()` 误用时使用的处理函数。
|
|
5
|
+
*/
|
|
6
|
+
export type PermitDuplicateReleaseHandler = (message: string) => void
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 表示构造 `Permit` 时的可选元信息。
|
|
10
|
+
*/
|
|
11
|
+
export interface PermitOptions<Details> {
|
|
12
|
+
/**
|
|
13
|
+
* `Permit` 被正常释放时的回调。
|
|
14
|
+
*/
|
|
15
|
+
onRelease: () => void
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 释放句柄附带的只读细节。
|
|
19
|
+
*/
|
|
20
|
+
details?: Details | undefined
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 重复释放时的可选处理器。
|
|
24
|
+
*/
|
|
25
|
+
onDuplicateRelease?: PermitDuplicateReleaseHandler | undefined
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 重复释放时的提示信息。
|
|
29
|
+
*/
|
|
30
|
+
duplicateReleaseMessage?: string | undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 表示与某个协调原语关联的 permit 细节。
|
|
35
|
+
*/
|
|
36
|
+
export interface CoordinationPermitDetails<Coordination extends string = string> {
|
|
37
|
+
coordination: Coordination
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 表示带有模式信息的 permit 细节。
|
|
42
|
+
*/
|
|
43
|
+
export interface ModePermitDetails<Coordination extends string, Mode extends string>
|
|
44
|
+
extends CoordinationPermitDetails<Coordination> {
|
|
45
|
+
mode: Mode
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 表示带有 key 信息的 permit 细节。
|
|
50
|
+
*/
|
|
51
|
+
export interface KeyedPermitDetails<Coordination extends string, Key>
|
|
52
|
+
extends CoordinationPermitDetails<Coordination> {
|
|
53
|
+
key: Key
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 表示一次已成功获取的协调资源占用句柄。
|
|
58
|
+
*
|
|
59
|
+
* `Permit` 本身不负责排队或调度,它只是对“当前持有一次资源”的显式表达,
|
|
60
|
+
* 并把释放动作和调试元信息封装在一起。
|
|
61
|
+
*/
|
|
62
|
+
export class Permit<Details = undefined> implements Disposable {
|
|
63
|
+
/**
|
|
64
|
+
* 创建该 permit 时附带的只读元信息。
|
|
65
|
+
*/
|
|
66
|
+
readonly details: Details | undefined
|
|
67
|
+
|
|
68
|
+
private isReleasedValue: boolean
|
|
69
|
+
|
|
70
|
+
private readonly onRelease: () => void
|
|
71
|
+
|
|
72
|
+
private readonly onDuplicateRelease: PermitDuplicateReleaseHandler | undefined
|
|
73
|
+
|
|
74
|
+
private readonly duplicateReleaseMessage: string
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 创建一个 permit,并绑定释放时需要执行的回调。
|
|
78
|
+
*/
|
|
79
|
+
constructor(options: PermitOptions<Details>) {
|
|
80
|
+
this.details = options.details
|
|
81
|
+
this.isReleasedValue = false
|
|
82
|
+
this.onRelease = options.onRelease
|
|
83
|
+
this.onDuplicateRelease = options.onDuplicateRelease
|
|
84
|
+
this.duplicateReleaseMessage = options.duplicateReleaseMessage ?? INTERNAL_DEFAULT_DUPLICATE_RELEASE_MESSAGE
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 读取当前 `Permit` 是否已经释放。
|
|
89
|
+
*/
|
|
90
|
+
isReleased(): boolean {
|
|
91
|
+
return this.isReleasedValue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 读取当前 `Permit` 是否仍然有效。
|
|
96
|
+
*/
|
|
97
|
+
isActive(): boolean {
|
|
98
|
+
return this.isReleasedValue === false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 主动释放本次占用。
|
|
103
|
+
*/
|
|
104
|
+
release(): void {
|
|
105
|
+
if (this.isReleasedValue === true) {
|
|
106
|
+
this.onDuplicateRelease?.(this.duplicateReleaseMessage)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.isReleasedValue = true
|
|
111
|
+
this.onRelease()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 与其他模块保持一致的显式释放别名。
|
|
116
|
+
*/
|
|
117
|
+
dispose(): void {
|
|
118
|
+
this.release()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 支持 `using` 语法自动释放。
|
|
123
|
+
*/
|
|
124
|
+
[Symbol.dispose](): void {
|
|
125
|
+
this.release()
|
|
126
|
+
}
|
|
127
|
+
}
|