@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,594 @@
|
|
|
1
|
+
import type { BaseEvents } from "#Source/event/index.ts"
|
|
2
|
+
import { EventManager } from "#Source/event/index.ts"
|
|
3
|
+
|
|
4
|
+
import { MinHeap } from "./min-heap.ts"
|
|
5
|
+
import { RemainingManager } from "./remaining-manager.ts"
|
|
6
|
+
|
|
7
|
+
const MAX_SCHEDULER_DELAY = 2_147_483_647
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 提供当前墙钟时间(wall-clock time)的毫秒值。
|
|
11
|
+
*/
|
|
12
|
+
export interface ExpirationManagerClock {
|
|
13
|
+
/**
|
|
14
|
+
* 返回当前时间戳,单位为毫秒。
|
|
15
|
+
*/
|
|
16
|
+
now(): number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 表示一个以毫秒为单位的过期时点。
|
|
21
|
+
*/
|
|
22
|
+
export type ExpirationEndAt = number
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 描述一组按过期名称索引的过期时点字典。
|
|
26
|
+
*/
|
|
27
|
+
export type ExpirationDict<ExpirationName extends string> = {
|
|
28
|
+
[K in ExpirationName]?: ExpirationEndAt | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 描述 `ExpirationManager` 对外发出的事件表。
|
|
33
|
+
*
|
|
34
|
+
* 当管理器处于暂停状态时,事件不会立即发出,而是延后并折叠为恢复时的最新快照。
|
|
35
|
+
*/
|
|
36
|
+
export interface ExpirationManagerEvents<ExpirationName extends string> extends BaseEvents {
|
|
37
|
+
/**
|
|
38
|
+
* 在保留的过期状态快照发生可观察变化时发出最新结果。
|
|
39
|
+
*/
|
|
40
|
+
expirationState: (expirationState: ExpirationState<ExpirationName>) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hold one coalesced emission payload derived from current manager state.
|
|
45
|
+
*/
|
|
46
|
+
interface ExpirationEmission<ExpirationName extends string> {
|
|
47
|
+
expirationState: ExpirationState<ExpirationName>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 描述一条被管理器保留的过期状态记录。
|
|
52
|
+
*/
|
|
53
|
+
export interface ExpirationStateItem<ExpirationName extends string> {
|
|
54
|
+
/**
|
|
55
|
+
* 该过期项的名称。
|
|
56
|
+
*/
|
|
57
|
+
name: ExpirationName
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 该过期项对应的绝对到期时点,单位为毫秒。
|
|
61
|
+
*/
|
|
62
|
+
endAt: ExpirationEndAt
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 该过期项当前的状态。
|
|
66
|
+
*/
|
|
67
|
+
state: "active" | "expired" | "removed"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 描述一组按过期名称索引的保留状态字典。
|
|
72
|
+
*/
|
|
73
|
+
export type ExpirationState<ExpirationName extends string> = {
|
|
74
|
+
[K in ExpirationName]?: ExpirationStateItem<ExpirationName> | undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 描述创建 `ExpirationManager` 时可用的配置项。
|
|
79
|
+
*/
|
|
80
|
+
export interface ExpirationManagerOptions<ExpirationName extends string> {
|
|
81
|
+
/**
|
|
82
|
+
* 用于初始化管理器的过期项集合。
|
|
83
|
+
*/
|
|
84
|
+
initialExpirations?: ExpirationDict<ExpirationName> | undefined
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 控制是否同时启用派生剩余时间管理器的周期性检查。
|
|
88
|
+
*/
|
|
89
|
+
enableRemainingManager?: boolean | undefined
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 提供自定义时间来源;未提供时默认使用 `Date.now()`。
|
|
93
|
+
*/
|
|
94
|
+
clock?: ExpirationManagerClock | undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 管理具名过期项的状态、事件与定时调度。
|
|
99
|
+
*
|
|
100
|
+
* `ExpirationManager` owns the source-of-truth expiration state dictionary and uses a `MinHeap`
|
|
101
|
+
* to keep the nearest `endAt` value at the top. That makes rescheduling efficient because
|
|
102
|
+
* the manager only needs to inspect the heap root to determine the next wake-up time,
|
|
103
|
+
* instead of scanning all expiration entries on every update.
|
|
104
|
+
*
|
|
105
|
+
* When the next expiration is farther away than the platform timer limit, scheduling is
|
|
106
|
+
* performed in bounded segments until the target timestamp becomes reachable.
|
|
107
|
+
*
|
|
108
|
+
* Pausing does not freeze time or reject writes. State updates may continue while paused,
|
|
109
|
+
* but expiration events and derived remaining events are deferred. Multiple paused-time
|
|
110
|
+
* state changes are coalesced into the latest snapshot and flushed after `resume()`.
|
|
111
|
+
*
|
|
112
|
+
* Remaining time is treated as derived state rather than primary state. The companion
|
|
113
|
+
* `RemainingManager` reads snapshots from `ExpirationManager`, computes second-based
|
|
114
|
+
* remaining values on demand, and can periodically emit those derived values without
|
|
115
|
+
* duplicating expiration ownership.
|
|
116
|
+
*/
|
|
117
|
+
export class ExpirationManager<ExpirationName extends string = string> {
|
|
118
|
+
private expirationState: ExpirationState<ExpirationName>
|
|
119
|
+
|
|
120
|
+
private clock: ExpirationManagerClock
|
|
121
|
+
private heap: MinHeap<ExpirationName>
|
|
122
|
+
private schedulerTimer: ReturnType<typeof setTimeout> | null
|
|
123
|
+
private paused: boolean
|
|
124
|
+
private terminated: boolean
|
|
125
|
+
private expirationEmissionQueue: Array<ExpirationEmission<ExpirationName>>
|
|
126
|
+
private flushingExpirationEmissionQueue: boolean
|
|
127
|
+
|
|
128
|
+
readonly event: EventManager<ExpirationManagerEvents<ExpirationName>>
|
|
129
|
+
readonly remainingManager: RemainingManager<ExpirationName>
|
|
130
|
+
|
|
131
|
+
constructor(options: ExpirationManagerOptions<ExpirationName>) {
|
|
132
|
+
const { initialExpirations, enableRemainingManager, clock } = options
|
|
133
|
+
const initialExpirationSource: ExpirationDict<ExpirationName> = initialExpirations ?? {}
|
|
134
|
+
|
|
135
|
+
this.expirationState = {}
|
|
136
|
+
|
|
137
|
+
this.clock = clock ?? {
|
|
138
|
+
now: (): number => Date.now()
|
|
139
|
+
}
|
|
140
|
+
this.heap = new MinHeap()
|
|
141
|
+
this.schedulerTimer = null
|
|
142
|
+
this.paused = false
|
|
143
|
+
this.terminated = false
|
|
144
|
+
this.expirationEmissionQueue = []
|
|
145
|
+
this.flushingExpirationEmissionQueue = false
|
|
146
|
+
|
|
147
|
+
this.event = new EventManager()
|
|
148
|
+
this.remainingManager = new RemainingManager<ExpirationName>({
|
|
149
|
+
expirationManager: this,
|
|
150
|
+
enabled: enableRemainingManager
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
this.updateExpirationBatch(initialExpirationSource)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 返回当前管理器使用的墙钟时间。
|
|
158
|
+
*/
|
|
159
|
+
getNow(): number {
|
|
160
|
+
return this.clock.now()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 判断某个具名过期项当前是否仍有保留状态。
|
|
165
|
+
*
|
|
166
|
+
* 只要该名称当前处于 `active`、`expired` 或 `removed` 任一保留状态,就会返回 `true`。
|
|
167
|
+
*/
|
|
168
|
+
hasExpiration(name: ExpirationName): boolean {
|
|
169
|
+
return this.expirationState[name] !== undefined
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 返回某个具名过期项的状态快照。
|
|
174
|
+
*/
|
|
175
|
+
getExpirationState(name: ExpirationName): ExpirationStateItem<ExpirationName> | undefined {
|
|
176
|
+
return structuredClone(this.expirationState[name])
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 返回当前所有活跃过期项的绝对到期时点快照。
|
|
181
|
+
*/
|
|
182
|
+
getExpirationSnapshot(): ExpirationDict<ExpirationName> {
|
|
183
|
+
const expirationDict: ExpirationDict<ExpirationName> = {}
|
|
184
|
+
|
|
185
|
+
for (const name of Object.keys(this.expirationState)) {
|
|
186
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
187
|
+
const expirationName = name as ExpirationName
|
|
188
|
+
const expirationState = this.expirationState[expirationName]
|
|
189
|
+
|
|
190
|
+
if (expirationState === undefined) {
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (expirationState.state !== "active") {
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
expirationDict[expirationName] = expirationState.endAt
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return expirationDict
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check whether event emission is currently blocked.
|
|
205
|
+
*/
|
|
206
|
+
private isEmissionBlocked(): boolean {
|
|
207
|
+
return this.terminated === true || this.paused === true
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Flush queued state emissions in order until emission becomes blocked again.
|
|
212
|
+
*/
|
|
213
|
+
private flushExpirationEmissionQueue(): void {
|
|
214
|
+
if (this.isEmissionBlocked() === true) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (this.flushingExpirationEmissionQueue === true) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.flushingExpirationEmissionQueue = true
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
while (true) {
|
|
225
|
+
if (this.isEmissionBlocked() === true) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const emission = this.expirationEmissionQueue.shift()
|
|
230
|
+
if (emission === undefined) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.event.emit("expirationState", emission.expirationState)
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
this.flushingExpirationEmissionQueue = false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build the latest event payload from current manager state.
|
|
243
|
+
*/
|
|
244
|
+
private createExpirationEmission(): ExpirationEmission<ExpirationName> {
|
|
245
|
+
return {
|
|
246
|
+
expirationState: structuredClone(this.expirationState),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Replace any queued paused-time emission with the latest coalesced snapshot.
|
|
252
|
+
*/
|
|
253
|
+
private replaceQueuedExpirationEmission(): void {
|
|
254
|
+
const emission = this.createExpirationEmission()
|
|
255
|
+
this.expirationEmissionQueue = [emission]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Queue an emission for the current state.
|
|
260
|
+
*
|
|
261
|
+
* While paused, the queue is collapsed to a single latest snapshot so `resume()`
|
|
262
|
+
* only flushes the final observable state.
|
|
263
|
+
*/
|
|
264
|
+
private enqueueExpirationEmission(): void {
|
|
265
|
+
if (this.paused === true) {
|
|
266
|
+
this.replaceQueuedExpirationEmission()
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const emission = this.createExpirationEmission()
|
|
271
|
+
this.expirationEmissionQueue.push(emission)
|
|
272
|
+
this.flushExpirationEmissionQueue()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Mark overdue active entries as expired when their stored timestamps are no longer in the future.
|
|
277
|
+
*/
|
|
278
|
+
private updateExpired(now: number): boolean {
|
|
279
|
+
let changed = false
|
|
280
|
+
|
|
281
|
+
while (true) {
|
|
282
|
+
const top = this.heap.peek()
|
|
283
|
+
if (top === undefined || top.endAt > now) {
|
|
284
|
+
return changed
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.heap.pop()
|
|
288
|
+
const expiration = this.expirationState[top.name]
|
|
289
|
+
if (expiration === undefined) {
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
if (expiration.state === "active" && expiration.endAt === top.endAt) {
|
|
293
|
+
this.expirationState[top.name] = { ...expiration, state: "expired" }
|
|
294
|
+
changed = true
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private tick(): void {
|
|
300
|
+
if (this.terminated === true || this.paused === true) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const changed = this.updateExpired(this.getNow())
|
|
305
|
+
if (changed === true) {
|
|
306
|
+
this.enqueueExpirationEmission()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.schedule()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private schedule(): void {
|
|
313
|
+
if (this.terminated === true || this.paused === true) {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (this.schedulerTimer !== null) {
|
|
318
|
+
clearTimeout(this.schedulerTimer)
|
|
319
|
+
this.schedulerTimer = null
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const next = this.heap.peek()
|
|
323
|
+
if (next === undefined) {
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const delay = Math.min(MAX_SCHEDULER_DELAY, Math.max(0, next.endAt - this.getNow()))
|
|
328
|
+
this.schedulerTimer = setTimeout(() => this.tick(), delay)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private validateExpirationEndAt(endAt: number): void {
|
|
332
|
+
if (Number.isFinite(endAt) !== true || endAt < 0) {
|
|
333
|
+
throw new RangeError("Expiration endAt must be a finite timestamp greater than or equal to 0.")
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private internalUpsertExpiration(name: ExpirationName, endAt: number, now: number): boolean {
|
|
338
|
+
this.validateExpirationEndAt(endAt)
|
|
339
|
+
|
|
340
|
+
const oldState = this.expirationState[name]
|
|
341
|
+
const newState = { name, endAt, state: endAt <= now ? "expired" : "active" } as const
|
|
342
|
+
|
|
343
|
+
if (
|
|
344
|
+
oldState !== undefined &&
|
|
345
|
+
oldState.name === newState.name &&
|
|
346
|
+
oldState.endAt === newState.endAt &&
|
|
347
|
+
oldState.state === newState.state
|
|
348
|
+
) {
|
|
349
|
+
return false
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.expirationState[name] = newState
|
|
353
|
+
this.heap.remove(name)
|
|
354
|
+
if (newState.state === "active") {
|
|
355
|
+
this.heap.push({ name, endAt })
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return true
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 使用毫秒级绝对时点设置或替换一个具名过期项。
|
|
363
|
+
*/
|
|
364
|
+
upsertExpiration(name: ExpirationName, endAt: number): void {
|
|
365
|
+
if (this.terminated === true) {
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const changed = this.internalUpsertExpiration(name, endAt, this.getNow())
|
|
370
|
+
if (changed === true) {
|
|
371
|
+
this.enqueueExpirationEmission()
|
|
372
|
+
|
|
373
|
+
this.schedule()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 使用毫秒级绝对时点执行一次局部批量更新。
|
|
379
|
+
*
|
|
380
|
+
* 对于 `dict` 中每个被显式提供的键,行为如下:
|
|
381
|
+
* - 数值会把对应过期项设置或替换为给定的绝对 `endAt` 时点。
|
|
382
|
+
* - 显式的 `undefined` 只会在当前状态为 `active` 时把该项标记为 `removed`。
|
|
383
|
+
* - 当前已处于 `expired` 或 `removed` 的项,遇到显式 `undefined` 时保持不变。
|
|
384
|
+
* - `dict` 中未出现的键不会被处理,现有状态保持不变。
|
|
385
|
+
*
|
|
386
|
+
* 当至少有一项真实发生变化时,会在所有显式更新完成后统一发出一次状态变化通知。
|
|
387
|
+
*/
|
|
388
|
+
updateExpirationBatch(dict: ExpirationDict<ExpirationName>): void {
|
|
389
|
+
if (this.terminated === true) {
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const name of Object.keys(dict)) {
|
|
394
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
395
|
+
const expirationName = name as ExpirationName
|
|
396
|
+
const endAt = dict[expirationName]
|
|
397
|
+
|
|
398
|
+
if (endAt === undefined) {
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
this.validateExpirationEndAt(endAt)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let changed = false
|
|
406
|
+
const now = this.getNow()
|
|
407
|
+
for (const name of Object.keys(dict)) {
|
|
408
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
409
|
+
const expirationName = name as ExpirationName
|
|
410
|
+
const endAt = dict[expirationName]
|
|
411
|
+
|
|
412
|
+
if (endAt === undefined) {
|
|
413
|
+
const expiration = this.expirationState[expirationName]
|
|
414
|
+
if (expiration === undefined) {
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
if (expiration.state === "active") {
|
|
418
|
+
this.expirationState[expirationName] = { ...expiration, state: "removed" }
|
|
419
|
+
this.heap.remove(expirationName)
|
|
420
|
+
changed = true
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
changed = this.internalUpsertExpiration(expirationName, endAt, now) || changed
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (changed === true) {
|
|
430
|
+
this.enqueueExpirationEmission()
|
|
431
|
+
|
|
432
|
+
this.schedule()
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 将一个当前处于活跃状态的过期项标记为 `removed`。
|
|
438
|
+
*
|
|
439
|
+
* 已经处于 `expired` 或 `removed` 的项不会被再次修改。
|
|
440
|
+
*/
|
|
441
|
+
removeExpiration(name: ExpirationName): void {
|
|
442
|
+
if (this.terminated === true) {
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const expiration = this.expirationState[name]
|
|
447
|
+
if (expiration === undefined) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (expiration.state !== "active") {
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.expirationState[name] = { ...expiration, state: "removed" }
|
|
455
|
+
this.heap.remove(name)
|
|
456
|
+
|
|
457
|
+
this.enqueueExpirationEmission()
|
|
458
|
+
|
|
459
|
+
this.schedule()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* 清除所有当前状态为 `expired` 的保留项,并返回清除数量。
|
|
464
|
+
*/
|
|
465
|
+
clearExpiredExpirations(): number {
|
|
466
|
+
if (this.terminated === true) {
|
|
467
|
+
return 0
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let clearedCount = 0
|
|
471
|
+
|
|
472
|
+
for (const name of Object.keys(this.expirationState)) {
|
|
473
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
474
|
+
const expirationName = name as ExpirationName
|
|
475
|
+
const expirationState = this.expirationState[expirationName]
|
|
476
|
+
if (expirationState === undefined) {
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
if (expirationState.state !== "expired") {
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.heap.remove(expirationName)
|
|
484
|
+
delete this.expirationState[expirationName]
|
|
485
|
+
clearedCount = clearedCount + 1
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (clearedCount > 0) {
|
|
489
|
+
this.enqueueExpirationEmission()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return clearedCount
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 清除所有当前状态为 `removed` 的保留项,并返回清除数量。
|
|
497
|
+
*/
|
|
498
|
+
clearRemovedExpirations(): number {
|
|
499
|
+
if (this.terminated === true) {
|
|
500
|
+
return 0
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let clearedCount = 0
|
|
504
|
+
|
|
505
|
+
for (const name of Object.keys(this.expirationState)) {
|
|
506
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
507
|
+
const expirationName = name as ExpirationName
|
|
508
|
+
const expirationState = this.expirationState[expirationName]
|
|
509
|
+
if (expirationState === undefined) {
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
if (expirationState.state !== "removed") {
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.heap.remove(expirationName)
|
|
517
|
+
delete this.expirationState[expirationName]
|
|
518
|
+
clearedCount = clearedCount + 1
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (clearedCount > 0) {
|
|
522
|
+
this.enqueueExpirationEmission()
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return clearedCount
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 暂停过期调度与事件发出。
|
|
530
|
+
*
|
|
531
|
+
* 这不会冻结绝对到期时点,也不会阻止状态写入。暂停期间时间仍会继续流逝,
|
|
532
|
+
* 只是过期推进与事件通知会延后到后续恢复时再统一处理。
|
|
533
|
+
*/
|
|
534
|
+
pause(): void {
|
|
535
|
+
if (this.terminated === true) {
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this.paused = true
|
|
540
|
+
|
|
541
|
+
if (this.schedulerTimer !== null) {
|
|
542
|
+
clearTimeout(this.schedulerTimer)
|
|
543
|
+
this.schedulerTimer = null
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
this.remainingManager.pause()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* 恢复过期调度,并刷新暂停期间积累的最新状态通知。
|
|
551
|
+
*
|
|
552
|
+
* 在暂停期间已经越过其绝对 `endAt` 的项,可能会在恢复后立即转为 `expired`。
|
|
553
|
+
* 若暂停期间发生过多次状态变更,对外只会发出折叠后的最新快照。
|
|
554
|
+
*/
|
|
555
|
+
resume(): void {
|
|
556
|
+
if (this.terminated === true) {
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (this.paused !== true) {
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
this.paused = false
|
|
564
|
+
|
|
565
|
+
const changed = this.updateExpired(this.getNow())
|
|
566
|
+
if (changed === true) {
|
|
567
|
+
this.replaceQueuedExpirationEmission()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.schedule()
|
|
571
|
+
this.remainingManager.resume()
|
|
572
|
+
this.flushExpirationEmissionQueue()
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 终止当前管理器及其派生剩余时间管理器,并释放它们拥有的定时资源。
|
|
577
|
+
*/
|
|
578
|
+
terminate(): void {
|
|
579
|
+
if (this.terminated === true) {
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
this.terminated = true
|
|
584
|
+
this.paused = true
|
|
585
|
+
|
|
586
|
+
if (this.schedulerTimer !== null) {
|
|
587
|
+
clearTimeout(this.schedulerTimer)
|
|
588
|
+
this.schedulerTimer = null
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
this.expirationEmissionQueue = []
|
|
592
|
+
this.remainingManager.terminate()
|
|
593
|
+
}
|
|
594
|
+
}
|