@planet-matrix/mobius-model 0.4.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.
Files changed (179) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +134 -21
  3. package/dist/index.js +45 -4
  4. package/dist/index.js.map +186 -11
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +16 -10
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/basic/README.md +69 -117
  12. package/src/basic/enhance.ts +10 -0
  13. package/src/basic/function.ts +81 -62
  14. package/src/basic/index.ts +2 -0
  15. package/src/basic/is.ts +152 -71
  16. package/src/basic/object.ts +82 -0
  17. package/src/basic/promise.ts +29 -8
  18. package/src/basic/string.ts +2 -33
  19. package/src/color/README.md +105 -0
  20. package/src/color/index.ts +3 -0
  21. package/src/color/internal.ts +42 -0
  22. package/src/color/rgb/analyze.ts +236 -0
  23. package/src/color/rgb/construct.ts +130 -0
  24. package/src/color/rgb/convert.ts +227 -0
  25. package/src/color/rgb/derive.ts +303 -0
  26. package/src/color/rgb/index.ts +6 -0
  27. package/src/color/rgb/internal.ts +208 -0
  28. package/src/color/rgb/parse.ts +302 -0
  29. package/src/color/rgb/serialize.ts +144 -0
  30. package/src/color/types.ts +57 -0
  31. package/src/color/xyz/analyze.ts +80 -0
  32. package/src/color/xyz/construct.ts +19 -0
  33. package/src/color/xyz/convert.ts +71 -0
  34. package/src/color/xyz/index.ts +3 -0
  35. package/src/color/xyz/internal.ts +23 -0
  36. package/src/css/README.md +93 -0
  37. package/src/css/class.ts +559 -0
  38. package/src/css/index.ts +1 -0
  39. package/src/encoding/README.md +92 -0
  40. package/src/encoding/base64.ts +107 -0
  41. package/src/encoding/index.ts +1 -0
  42. package/src/environment/README.md +97 -0
  43. package/src/environment/basic.ts +26 -0
  44. package/src/environment/device.ts +311 -0
  45. package/src/environment/feature.ts +285 -0
  46. package/src/environment/geo.ts +337 -0
  47. package/src/environment/index.ts +7 -0
  48. package/src/environment/runtime.ts +400 -0
  49. package/src/environment/snapshot.ts +60 -0
  50. package/src/environment/variable.ts +239 -0
  51. package/src/event/README.md +90 -0
  52. package/src/event/class-event-proxy.ts +228 -0
  53. package/src/event/common.ts +19 -0
  54. package/src/event/event-manager.ts +203 -0
  55. package/src/event/index.ts +4 -0
  56. package/src/event/instance-event-proxy.ts +186 -0
  57. package/src/event/internal.ts +24 -0
  58. package/src/exception/README.md +96 -0
  59. package/src/exception/browser.ts +219 -0
  60. package/src/exception/index.ts +4 -0
  61. package/src/exception/nodejs.ts +169 -0
  62. package/src/exception/normalize.ts +106 -0
  63. package/src/exception/types.ts +99 -0
  64. package/src/identifier/README.md +92 -0
  65. package/src/identifier/id.ts +119 -0
  66. package/src/identifier/index.ts +2 -0
  67. package/src/identifier/uuid.ts +187 -0
  68. package/src/index.ts +18 -1
  69. package/src/log/README.md +79 -0
  70. package/src/log/index.ts +5 -0
  71. package/src/log/log-emitter.ts +72 -0
  72. package/src/log/log-record.ts +10 -0
  73. package/src/log/log-scheduler.ts +74 -0
  74. package/src/log/log-type.ts +8 -0
  75. package/src/log/logger.ts +543 -0
  76. package/src/orchestration/README.md +89 -0
  77. package/src/orchestration/coordination/barrier.ts +214 -0
  78. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  79. package/src/orchestration/coordination/errors.ts +98 -0
  80. package/src/orchestration/coordination/index.ts +16 -0
  81. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  82. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  83. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  84. package/src/orchestration/coordination/mutex.ts +257 -0
  85. package/src/orchestration/coordination/permit.ts +127 -0
  86. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  87. package/src/orchestration/coordination/semaphore.ts +280 -0
  88. package/src/orchestration/index.ts +1 -0
  89. package/src/random/README.md +78 -0
  90. package/src/random/index.ts +1 -0
  91. package/src/random/string.ts +35 -0
  92. package/src/reactor/README.md +4 -0
  93. package/src/reactor/reactor-core/primitive.ts +9 -9
  94. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  95. package/src/singleton/README.md +79 -0
  96. package/src/singleton/factory.ts +55 -0
  97. package/src/singleton/index.ts +2 -0
  98. package/src/singleton/manager.ts +204 -0
  99. package/src/storage/README.md +107 -0
  100. package/src/storage/index.ts +1 -0
  101. package/src/storage/table.ts +449 -0
  102. package/src/timer/README.md +86 -0
  103. package/src/timer/expiration/expiration-manager.ts +594 -0
  104. package/src/timer/expiration/index.ts +3 -0
  105. package/src/timer/expiration/min-heap.ts +208 -0
  106. package/src/timer/expiration/remaining-manager.ts +241 -0
  107. package/src/timer/index.ts +1 -0
  108. package/src/type/README.md +54 -307
  109. package/src/type/class.ts +2 -2
  110. package/src/type/index.ts +14 -14
  111. package/src/type/is.ts +265 -2
  112. package/src/type/object.ts +37 -0
  113. package/src/type/string.ts +7 -2
  114. package/src/type/tuple.ts +6 -6
  115. package/src/type/union.ts +16 -0
  116. package/src/web/README.md +77 -0
  117. package/src/web/capture.ts +35 -0
  118. package/src/web/clipboard.ts +97 -0
  119. package/src/web/dom.ts +117 -0
  120. package/src/web/download.ts +16 -0
  121. package/src/web/event.ts +46 -0
  122. package/src/web/index.ts +10 -0
  123. package/src/web/local-storage.ts +113 -0
  124. package/src/web/location.ts +28 -0
  125. package/src/web/permission.ts +172 -0
  126. package/src/web/script-loader.ts +432 -0
  127. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  128. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  129. package/tests/unit/basic/array.spec.ts +1 -1
  130. package/tests/unit/basic/object.spec.ts +32 -1
  131. package/tests/unit/basic/stream.spec.ts +1 -1
  132. package/tests/unit/basic/string.spec.ts +0 -9
  133. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  134. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  135. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  136. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  137. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  138. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  139. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  140. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  141. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  142. package/tests/unit/css/class.spec.ts +157 -0
  143. package/tests/unit/encoding/base64.spec.ts +40 -0
  144. package/tests/unit/environment/basic.spec.ts +20 -0
  145. package/tests/unit/environment/device.spec.ts +146 -0
  146. package/tests/unit/environment/feature.spec.ts +388 -0
  147. package/tests/unit/environment/geo.spec.ts +111 -0
  148. package/tests/unit/environment/runtime.spec.ts +364 -0
  149. package/tests/unit/environment/snapshot.spec.ts +4 -0
  150. package/tests/unit/environment/variable.spec.ts +190 -0
  151. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  152. package/tests/unit/event/event-manager.spec.ts +246 -0
  153. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  154. package/tests/unit/exception/browser.spec.ts +213 -0
  155. package/tests/unit/exception/nodejs.spec.ts +144 -0
  156. package/tests/unit/exception/normalize.spec.ts +57 -0
  157. package/tests/unit/identifier/id.spec.ts +71 -0
  158. package/tests/unit/identifier/uuid.spec.ts +85 -0
  159. package/tests/unit/log/log-emitter.spec.ts +33 -0
  160. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  161. package/tests/unit/log/log-type.spec.ts +7 -0
  162. package/tests/unit/log/logger.spec.ts +222 -0
  163. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  164. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  165. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  166. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  167. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  168. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  169. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  170. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  171. package/tests/unit/random/string.spec.ts +11 -0
  172. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  173. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  174. package/tests/unit/singleton/singleton.spec.ts +49 -0
  175. package/tests/unit/storage/table.spec.ts +620 -0
  176. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  177. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  178. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  179. package/.oxlintrc.json +0 -5
@@ -0,0 +1,464 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import { ExpirationManager } from "#Source/timer/index.ts"
4
+
5
+ test("ExpirationManager getNow delegates to the configured clock", () => {
6
+ let now = 100
7
+ const expirationManager = new ExpirationManager<"alpha">({
8
+ enableRemainingManager: false,
9
+ clock: {
10
+ now: (): number => now,
11
+ },
12
+ })
13
+
14
+ expect(expirationManager.getNow()).toBe(100)
15
+
16
+ now = 250
17
+
18
+ expect(expirationManager.getNow()).toBe(250)
19
+
20
+ expirationManager.terminate()
21
+ })
22
+
23
+ test("ExpirationManager hasExpiration reports retained active, expired, and removed entries", () => {
24
+ vi.useFakeTimers()
25
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
26
+
27
+ try {
28
+ const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma" | "missing">({
29
+ enableRemainingManager: false,
30
+ })
31
+
32
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
33
+ expirationManager.upsertExpiration("beta", Date.now() - 1)
34
+ expirationManager.upsertExpiration("gamma", Date.now() + 1_000)
35
+ expirationManager.removeExpiration("gamma")
36
+
37
+ expect(expirationManager.hasExpiration("alpha")).toBe(true)
38
+ expect(expirationManager.hasExpiration("beta")).toBe(true)
39
+ expect(expirationManager.hasExpiration("gamma")).toBe(true)
40
+ expect(expirationManager.hasExpiration("missing")).toBe(false)
41
+
42
+ expirationManager.terminate()
43
+ }
44
+ finally {
45
+ vi.clearAllTimers()
46
+ vi.useRealTimers()
47
+ }
48
+ })
49
+
50
+ test("ExpirationManager getExpirationState returns a cloned state snapshot", () => {
51
+ vi.useFakeTimers()
52
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
53
+
54
+ try {
55
+ const expirationManager = new ExpirationManager<"alpha" | "missing">({
56
+ enableRemainingManager: false,
57
+ })
58
+ const endAt = Date.now() + 1_000
59
+
60
+ expirationManager.upsertExpiration("alpha", endAt)
61
+
62
+ const expirationState = expirationManager.getExpirationState("alpha")
63
+
64
+ expect(expirationState).toEqual({
65
+ name: "alpha",
66
+ endAt,
67
+ state: "active",
68
+ })
69
+
70
+ if (expirationState !== undefined) {
71
+ expirationState.state = "removed"
72
+ expirationState.endAt = 0
73
+ }
74
+
75
+ expect(expirationManager.getExpirationState("alpha")).toEqual({
76
+ name: "alpha",
77
+ endAt,
78
+ state: "active",
79
+ })
80
+ expect(expirationManager.getExpirationState("missing")).toBeUndefined()
81
+
82
+ expirationManager.terminate()
83
+ }
84
+ finally {
85
+ vi.clearAllTimers()
86
+ vi.useRealTimers()
87
+ }
88
+ })
89
+
90
+ test("ExpirationManager getExpirationSnapshot only includes active entries", () => {
91
+ vi.useFakeTimers()
92
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
93
+
94
+ try {
95
+ const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({
96
+ enableRemainingManager: false,
97
+ })
98
+ const alphaEndAt = Date.now() + 1_000
99
+
100
+ expirationManager.upsertExpiration("alpha", alphaEndAt)
101
+ expirationManager.upsertExpiration("beta", Date.now() - 1)
102
+ expirationManager.upsertExpiration("gamma", Date.now() + 1_000)
103
+ expirationManager.removeExpiration("gamma")
104
+
105
+ expect(expirationManager.getExpirationSnapshot()).toEqual({
106
+ alpha: alphaEndAt,
107
+ })
108
+
109
+ expirationManager.terminate()
110
+ }
111
+ finally {
112
+ vi.clearAllTimers()
113
+ vi.useRealTimers()
114
+ }
115
+ })
116
+
117
+ test("ExpirationManager upsertExpiration stores state changes and rejects invalid timestamps", () => {
118
+ vi.useFakeTimers()
119
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
120
+
121
+ try {
122
+ const expirationManager = new ExpirationManager<"alpha">({
123
+ enableRemainingManager: false,
124
+ })
125
+ const expirationStates: Array<Record<string, unknown>> = []
126
+ const activeEndAt = Date.now() + 2_000
127
+ const expiredEndAt = Date.now() - 1
128
+
129
+ expirationManager.event.subscribe("expirationState", (expirationState): void => {
130
+ expirationStates.push(structuredClone(expirationState))
131
+ })
132
+
133
+ expirationManager.upsertExpiration("alpha", activeEndAt)
134
+ expirationManager.upsertExpiration("alpha", activeEndAt)
135
+ expirationManager.upsertExpiration("alpha", expiredEndAt)
136
+
137
+ expect(expirationStates).toEqual([
138
+ {
139
+ alpha: {
140
+ name: "alpha",
141
+ endAt: activeEndAt,
142
+ state: "active",
143
+ },
144
+ },
145
+ {
146
+ alpha: {
147
+ name: "alpha",
148
+ endAt: expiredEndAt,
149
+ state: "expired",
150
+ },
151
+ },
152
+ ])
153
+
154
+ expect(() => expirationManager.upsertExpiration("alpha", Number.NaN)).toThrow(RangeError)
155
+
156
+ expirationManager.terminate()
157
+ }
158
+ finally {
159
+ vi.clearAllTimers()
160
+ vi.useRealTimers()
161
+ }
162
+ })
163
+
164
+ test("ExpirationManager updateExpirationBatch applies partial updates and active-only removals", () => {
165
+ vi.useFakeTimers()
166
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
167
+
168
+ try {
169
+ const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({
170
+ enableRemainingManager: false,
171
+ })
172
+ const alphaEndAt = Date.now() + 1_000
173
+ const betaEndAt = Date.now() - 1
174
+ const gammaEndAt = Date.now() + 2_000
175
+
176
+ expirationManager.updateExpirationBatch({
177
+ alpha: alphaEndAt,
178
+ beta: betaEndAt,
179
+ })
180
+ expirationManager.updateExpirationBatch({
181
+ alpha: undefined,
182
+ beta: undefined,
183
+ gamma: gammaEndAt,
184
+ })
185
+
186
+ expect(expirationManager.getExpirationState("alpha")).toEqual({
187
+ name: "alpha",
188
+ endAt: alphaEndAt,
189
+ state: "removed",
190
+ })
191
+ expect(expirationManager.getExpirationState("beta")).toEqual({
192
+ name: "beta",
193
+ endAt: betaEndAt,
194
+ state: "expired",
195
+ })
196
+ expect(expirationManager.getExpirationState("gamma")).toEqual({
197
+ name: "gamma",
198
+ endAt: gammaEndAt,
199
+ state: "active",
200
+ })
201
+
202
+ expirationManager.terminate()
203
+ }
204
+ finally {
205
+ vi.clearAllTimers()
206
+ vi.useRealTimers()
207
+ }
208
+ })
209
+
210
+ test("ExpirationManager removeExpiration only marks active entries as removed", () => {
211
+ vi.useFakeTimers()
212
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
213
+
214
+ try {
215
+ const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({
216
+ enableRemainingManager: false,
217
+ })
218
+
219
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
220
+ expirationManager.upsertExpiration("beta", Date.now() - 1)
221
+ expirationManager.upsertExpiration("gamma", Date.now() + 1_000)
222
+ expirationManager.removeExpiration("gamma")
223
+ expirationManager.removeExpiration("alpha")
224
+ expirationManager.removeExpiration("beta")
225
+
226
+ expect(expirationManager.getExpirationState("alpha")?.state).toBe("removed")
227
+ expect(expirationManager.getExpirationState("beta")?.state).toBe("expired")
228
+ expect(expirationManager.getExpirationState("gamma")?.state).toBe("removed")
229
+
230
+ expirationManager.terminate()
231
+ }
232
+ finally {
233
+ vi.clearAllTimers()
234
+ vi.useRealTimers()
235
+ }
236
+ })
237
+
238
+ test("ExpirationManager clearExpiredExpirations removes expired entries and returns the count", () => {
239
+ vi.useFakeTimers()
240
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
241
+
242
+ try {
243
+ const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({
244
+ enableRemainingManager: false,
245
+ })
246
+
247
+ expirationManager.upsertExpiration("alpha", Date.now() - 1)
248
+ expirationManager.upsertExpiration("beta", Date.now() - 2)
249
+ expirationManager.upsertExpiration("gamma", Date.now() + 1_000)
250
+
251
+ expect(expirationManager.clearExpiredExpirations()).toBe(2)
252
+ expect(expirationManager.hasExpiration("alpha")).toBe(false)
253
+ expect(expirationManager.hasExpiration("beta")).toBe(false)
254
+ expect(expirationManager.hasExpiration("gamma")).toBe(true)
255
+
256
+ expirationManager.terminate()
257
+ }
258
+ finally {
259
+ vi.clearAllTimers()
260
+ vi.useRealTimers()
261
+ }
262
+ })
263
+
264
+ test("ExpirationManager clearRemovedExpirations removes removed entries and returns the count", () => {
265
+ vi.useFakeTimers()
266
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
267
+
268
+ try {
269
+ const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({
270
+ enableRemainingManager: false,
271
+ })
272
+
273
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
274
+ expirationManager.upsertExpiration("beta", Date.now() + 2_000)
275
+ expirationManager.upsertExpiration("gamma", Date.now() - 1)
276
+ expirationManager.removeExpiration("alpha")
277
+ expirationManager.removeExpiration("beta")
278
+
279
+ expect(expirationManager.clearRemovedExpirations()).toBe(2)
280
+ expect(expirationManager.hasExpiration("alpha")).toBe(false)
281
+ expect(expirationManager.hasExpiration("beta")).toBe(false)
282
+ expect(expirationManager.getExpirationState("gamma")?.state).toBe("expired")
283
+
284
+ expirationManager.terminate()
285
+ }
286
+ finally {
287
+ vi.clearAllTimers()
288
+ vi.useRealTimers()
289
+ }
290
+ })
291
+
292
+ test("ExpirationManager pause blocks queued emissions and scheduler ticks", () => {
293
+ vi.useFakeTimers()
294
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
295
+
296
+ try {
297
+ const expirationManager = new ExpirationManager<"alpha">({
298
+ enableRemainingManager: false,
299
+ })
300
+ const expirationStates: Array<Record<string, unknown>> = []
301
+
302
+ expirationManager.event.subscribe("expirationState", (expirationState): void => {
303
+ expirationStates.push(structuredClone(expirationState))
304
+ })
305
+
306
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
307
+ expirationManager.pause()
308
+ expirationManager.upsertExpiration("alpha", Date.now() + 3_000)
309
+ vi.advanceTimersByTime(4_000)
310
+
311
+ expect(expirationStates).toHaveLength(1)
312
+ expect(expirationManager.getExpirationState("alpha")?.state).toBe("active")
313
+
314
+ expirationManager.terminate()
315
+ }
316
+ finally {
317
+ vi.clearAllTimers()
318
+ vi.useRealTimers()
319
+ }
320
+ })
321
+
322
+ test("ExpirationManager resume flushes the latest paused snapshot and expires overdue entries", () => {
323
+ vi.useFakeTimers()
324
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
325
+
326
+ try {
327
+ const expirationManager = new ExpirationManager<"alpha" | "beta">({
328
+ enableRemainingManager: false,
329
+ })
330
+ const expirationStates: Array<Record<string, unknown>> = []
331
+ const alphaInitialEndAt = Date.now() + 2_000
332
+ const alphaUpdatedEndAt = Date.now() + 5_000
333
+ const betaEndAt = Date.now() + 1_000
334
+
335
+ expirationManager.event.subscribe("expirationState", (expirationState): void => {
336
+ expirationStates.push(structuredClone(expirationState))
337
+ })
338
+
339
+ expirationManager.upsertExpiration("alpha", alphaInitialEndAt)
340
+ expirationManager.pause()
341
+ expirationManager.updateExpirationBatch({
342
+ alpha: alphaUpdatedEndAt,
343
+ beta: betaEndAt,
344
+ })
345
+ expirationManager.updateExpirationBatch({
346
+ beta: undefined,
347
+ })
348
+
349
+ vi.advanceTimersByTime(6_000)
350
+ expirationManager.resume()
351
+
352
+ expect(expirationStates).toEqual([
353
+ {
354
+ alpha: {
355
+ name: "alpha",
356
+ endAt: alphaInitialEndAt,
357
+ state: "active",
358
+ },
359
+ },
360
+ {
361
+ alpha: {
362
+ name: "alpha",
363
+ endAt: alphaUpdatedEndAt,
364
+ state: "expired",
365
+ },
366
+ beta: {
367
+ name: "beta",
368
+ endAt: betaEndAt,
369
+ state: "removed",
370
+ },
371
+ },
372
+ ])
373
+
374
+ expirationManager.terminate()
375
+ }
376
+ finally {
377
+ vi.clearAllTimers()
378
+ vi.useRealTimers()
379
+ }
380
+ })
381
+
382
+ test("ExpirationManager terminate stops future scheduling and ignores later writes", () => {
383
+ vi.useFakeTimers()
384
+ vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0))
385
+
386
+ try {
387
+ const expirationManager = new ExpirationManager<"alpha" | "beta">({
388
+ enableRemainingManager: false,
389
+ })
390
+ const expirationStates: Array<Record<string, unknown>> = []
391
+
392
+ expirationManager.event.subscribe("expirationState", (expirationState): void => {
393
+ expirationStates.push(structuredClone(expirationState))
394
+ })
395
+
396
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
397
+ expirationManager.terminate()
398
+ vi.advanceTimersByTime(2_000)
399
+ expirationManager.upsertExpiration("beta", Date.now() + 1_000)
400
+
401
+ expect(expirationStates).toHaveLength(1)
402
+ expect(expirationManager.hasExpiration("beta")).toBe(false)
403
+ }
404
+ finally {
405
+ vi.clearAllTimers()
406
+ vi.useRealTimers()
407
+ }
408
+ })
409
+
410
+ test("ExpirationManager pause during expirationState does not interrupt the current emission", () => {
411
+ const expirationManager = new ExpirationManager<"alpha">({
412
+ enableRemainingManager: false,
413
+ })
414
+ const eventOrder: string[] = []
415
+
416
+ expirationManager.event.subscribe("expirationState", () => {
417
+ eventOrder.push("state")
418
+ expirationManager.pause()
419
+ })
420
+
421
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
422
+
423
+ expect(eventOrder).toEqual(["state"])
424
+
425
+ expirationManager.terminate()
426
+ })
427
+
428
+ test("ExpirationManager resume does not replay an already emitted state snapshot", () => {
429
+ const expirationManager = new ExpirationManager<"alpha">({
430
+ enableRemainingManager: false,
431
+ })
432
+ const eventOrder: string[] = []
433
+
434
+ expirationManager.event.subscribe("expirationState", () => {
435
+ eventOrder.push("state")
436
+
437
+ if (eventOrder.length === 1) {
438
+ expirationManager.pause()
439
+ }
440
+ })
441
+
442
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
443
+ expirationManager.resume()
444
+
445
+ expect(eventOrder).toEqual(["state"])
446
+
447
+ expirationManager.terminate()
448
+ })
449
+
450
+ test("ExpirationManager terminate during expirationState does not throw", () => {
451
+ const expirationManager = new ExpirationManager<"alpha">({
452
+ enableRemainingManager: false,
453
+ })
454
+ const eventOrder: string[] = []
455
+
456
+ expirationManager.event.subscribe("expirationState", () => {
457
+ eventOrder.push("state")
458
+ expirationManager.terminate()
459
+ })
460
+
461
+ expirationManager.upsertExpiration("alpha", Date.now() + 1_000)
462
+
463
+ expect(eventOrder).toEqual(["state"])
464
+ })
@@ -0,0 +1,71 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { MinHeap } from "#Source/timer/index.ts"
4
+
5
+ test("MinHeap size returns the current node count", () => {
6
+ const heap = new MinHeap<"alpha" | "beta">()
7
+
8
+ expect(heap.size()).toBe(0)
9
+
10
+ heap.push({ name: "alpha", endAt: 3 })
11
+ heap.push({ name: "beta", endAt: 1 })
12
+
13
+ expect(heap.size()).toBe(2)
14
+ })
15
+
16
+ test("MinHeap peek returns the smallest endAt without removing it", () => {
17
+ const heap = new MinHeap<"alpha" | "beta" | "gamma">()
18
+
19
+ expect(heap.peek()).toBeUndefined()
20
+
21
+ heap.push({ name: "alpha", endAt: 5 })
22
+ heap.push({ name: "beta", endAt: 2 })
23
+ heap.push({ name: "gamma", endAt: 4 })
24
+
25
+ expect(heap.peek()).toEqual({ name: "beta", endAt: 2 })
26
+ expect(heap.size()).toBe(3)
27
+ })
28
+
29
+ test("MinHeap push maintains heap order for later reads", () => {
30
+ const heap = new MinHeap<"alpha" | "beta" | "gamma">()
31
+
32
+ heap.push({ name: "alpha", endAt: 7 })
33
+ heap.push({ name: "beta", endAt: 3 })
34
+ heap.push({ name: "gamma", endAt: 5 })
35
+
36
+ expect(heap.peek()).toEqual({ name: "beta", endAt: 3 })
37
+ expect(heap.pop()).toEqual({ name: "beta", endAt: 3 })
38
+ expect(heap.pop()).toEqual({ name: "gamma", endAt: 5 })
39
+ expect(heap.pop()).toEqual({ name: "alpha", endAt: 7 })
40
+ })
41
+
42
+ test("MinHeap remove deletes the named node and keeps the remaining order valid", () => {
43
+ const heap = new MinHeap<"alpha" | "beta" | "gamma" | "delta" | "missing">()
44
+
45
+ heap.push({ name: "alpha", endAt: 7 })
46
+ heap.push({ name: "beta", endAt: 3 })
47
+ heap.push({ name: "gamma", endAt: 5 })
48
+ heap.push({ name: "delta", endAt: 1 })
49
+
50
+ heap.remove("beta")
51
+ heap.remove("missing")
52
+
53
+ expect(heap.pop()).toEqual({ name: "delta", endAt: 1 })
54
+ expect(heap.pop()).toEqual({ name: "gamma", endAt: 5 })
55
+ expect(heap.pop()).toEqual({ name: "alpha", endAt: 7 })
56
+ })
57
+
58
+ test("MinHeap pop removes and returns nodes in ascending endAt order", () => {
59
+ const heap = new MinHeap<"alpha" | "beta" | "gamma">()
60
+
61
+ expect(heap.pop()).toBeUndefined()
62
+
63
+ heap.push({ name: "alpha", endAt: 4 })
64
+ heap.push({ name: "beta", endAt: 2 })
65
+ heap.push({ name: "gamma", endAt: 6 })
66
+
67
+ expect(heap.pop()).toEqual({ name: "beta", endAt: 2 })
68
+ expect(heap.pop()).toEqual({ name: "alpha", endAt: 4 })
69
+ expect(heap.pop()).toEqual({ name: "gamma", endAt: 6 })
70
+ expect(heap.pop()).toBeUndefined()
71
+ })