@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,222 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import type { LogEmitter, LogEmitterItem } from "#Source/log/index.ts"
4
+ import {
5
+ Logger,
6
+ getGlobalLogger,
7
+ } from "#Source/log/index.ts"
8
+
9
+ const makeMemoryEmitter = (sink: string[]): LogEmitterItem => {
10
+ class InternalMemoryEmitter implements LogEmitter {
11
+ readonly tags: string[]
12
+ readonly messages: unknown[]
13
+ readonly formattedMessages: string[]
14
+
15
+ constructor(options: { tags?: string[] | undefined, messages?: unknown[] | undefined }) {
16
+ this.tags = options.tags ?? []
17
+ this.messages = options.messages ?? []
18
+ this.formattedMessages = [
19
+ this.tags.map(tag => `[${tag}]`).join(""),
20
+ this.messages.map(message => String(message)).join(" "),
21
+ ]
22
+ }
23
+
24
+ emit(): void {
25
+ sink.push(this.formattedMessages.join(" "))
26
+ }
27
+ }
28
+
29
+ return {
30
+ logType: "info",
31
+ LogEmitter: InternalMemoryEmitter,
32
+ }
33
+ }
34
+
35
+ test("Logger.fromOptions returns provided logger and otherwise derives from global logger", () => {
36
+ const explicit = new Logger({ name: "Explicit" })
37
+ const resolvedExplicit = Logger.fromOptions({ logger: explicit })
38
+ const resolvedDerived = Logger.fromOptions({})
39
+
40
+ expect(resolvedExplicit).toBe(explicit)
41
+ expect(resolvedDerived.hasParent()).toBe(true)
42
+ expect(resolvedDerived.getNameTags()).toContain(getGlobalLogger().getName())
43
+ })
44
+
45
+ test("Logger.derive creates child logger with inherited configs", () => {
46
+ const parent = new Logger({ name: "Parent" })
47
+ const child = Logger.derive(parent)
48
+
49
+ expect(child.hasParent()).toBe(true)
50
+ expect(child.getNameTags()).toEqual(["Parent", "Unnamed"])
51
+ })
52
+
53
+ test("Logger config APIs merge and apply parent configs", () => {
54
+ const parent = new Logger({
55
+ name: "Parent",
56
+ configs: {
57
+ enabled: true,
58
+ autoSend: true,
59
+ filter: (): boolean => true,
60
+ emitters: [],
61
+ },
62
+ })
63
+ const child = new Logger({
64
+ name: "Child",
65
+ parent,
66
+ configs: {
67
+ enabled: false,
68
+ },
69
+ })
70
+
71
+ expect(child.getConfigs().enabled).toBe(true)
72
+
73
+ child.setConfigs({ enabled: false })
74
+ expect(child.getConfigs().enabled).toBe(false)
75
+
76
+ child.useParentConfigs()
77
+ expect(child.getConfigs().enabled).toBe(true)
78
+ })
79
+
80
+ test("Logger emitter APIs add, deduplicate and remove emitters", () => {
81
+ const sink: string[] = []
82
+ const emitter = makeMemoryEmitter(sink)
83
+ const logger = new Logger({
84
+ name: "EmitterLogger",
85
+ configs: {
86
+ emitters: [],
87
+ enabled: true,
88
+ autoSend: true,
89
+ filter: (): boolean => true,
90
+ },
91
+ })
92
+
93
+ logger.addLogEmitters([emitter, emitter])
94
+ expect(logger.getAllLogEmitters()).toHaveLength(1)
95
+ expect(logger.getLogEmitters("info")).toHaveLength(1)
96
+
97
+ logger.info("hello")
98
+ expect(sink).toHaveLength(1)
99
+
100
+ logger.removeLogEmitters([emitter])
101
+ expect(logger.getAllLogEmitters()).toHaveLength(0)
102
+ })
103
+
104
+ test("Logger tags APIs manage instance, session and once tags", () => {
105
+ const sink: string[] = []
106
+ const logger = new Logger({
107
+ name: "TagLogger",
108
+ configs: {
109
+ emitters: [makeMemoryEmitter(sink)],
110
+ enabled: true,
111
+ autoSend: true,
112
+ filter: (): boolean => true,
113
+ },
114
+ })
115
+
116
+ logger.addInstanceTags(["instance"]).tagStart(["session"]).addOnceTags(["once"])
117
+ logger.info("first")
118
+ logger.info("second")
119
+
120
+ expect(sink[0]).toContain("[instance]")
121
+ expect(sink[0]).toContain("[session]")
122
+ expect(sink[0]).toContain("[once]")
123
+ expect(sink[1]).toContain("[instance]")
124
+ expect(sink[1]).toContain("[session]")
125
+ expect(sink[1]).not.toContain("[once]")
126
+
127
+ logger.removeInstanceTags(["instance"]).tagEnd()
128
+ expect(logger.getInstanceTags()).toEqual([])
129
+ expect(logger.getSessionTags()).toEqual([])
130
+ })
131
+
132
+ test("Logger.autoTag adds temporary tags during disposable scope", () => {
133
+ const sink: string[] = []
134
+ const logger = new Logger({
135
+ name: "AutoTagLogger",
136
+ configs: {
137
+ emitters: [makeMemoryEmitter(sink)],
138
+ enabled: true,
139
+ autoSend: true,
140
+ filter: (): boolean => true,
141
+ },
142
+ })
143
+
144
+ const disposable = logger.autoTag(["scoped"])
145
+ logger.info("within")
146
+ disposable[Symbol.dispose]()
147
+ logger.info("outside")
148
+
149
+ expect(sink[0]).toContain("[scoped]")
150
+ expect(sink[1]).not.toContain("[scoped]")
151
+ })
152
+
153
+ test("Logger logging APIs respect enabled and filter configs", () => {
154
+ const sink: string[] = []
155
+ const logger = new Logger({
156
+ name: "FilterLogger",
157
+ configs: {
158
+ emitters: [makeMemoryEmitter(sink)],
159
+ enabled: false,
160
+ autoSend: true,
161
+ filter: (): boolean => true,
162
+ },
163
+ })
164
+
165
+ logger.log("a").info("b").warn("c").error("d").debug("e")
166
+ expect(sink).toHaveLength(0)
167
+
168
+ logger.setConfigs({ enabled: true, filter: () => false })
169
+ logger.info("blocked")
170
+ expect(sink).toHaveLength(0)
171
+
172
+ logger.setConfigs({ filter: () => true })
173
+ logger.info("allowed")
174
+ expect(sink).toHaveLength(1)
175
+ })
176
+
177
+ test("Logger.send flushes queued logs when autoSend is disabled", () => {
178
+ const sink: string[] = []
179
+ const logger = new Logger({
180
+ name: "BatchLogger",
181
+ configs: {
182
+ emitters: [makeMemoryEmitter(sink)],
183
+ enabled: true,
184
+ autoSend: false,
185
+ filter: (): boolean => true,
186
+ },
187
+ })
188
+
189
+ logger.info("queued")
190
+ expect(sink).toHaveLength(0)
191
+
192
+ logger.send()
193
+ expect(sink).toHaveLength(1)
194
+ })
195
+
196
+ test("Logger.batch creates manual child logger and supports disposal send", () => {
197
+ const sink: string[] = []
198
+ const logger = new Logger({
199
+ name: "ParentLogger",
200
+ configs: {
201
+ emitters: [makeMemoryEmitter(sink)],
202
+ enabled: true,
203
+ autoSend: true,
204
+ filter: (): boolean => true,
205
+ },
206
+ })
207
+
208
+ const child = logger.batch("batch-id")
209
+ child.info("queued")
210
+ expect(sink).toHaveLength(0)
211
+
212
+ child[Symbol.dispose]()
213
+ expect(sink).toHaveLength(1)
214
+ expect(sink[0]).toContain("[batch-id]")
215
+ })
216
+
217
+ test("getGlobalLogger returns singleton instance", () => {
218
+ const logger1 = getGlobalLogger()
219
+ const logger2 = getGlobalLogger()
220
+
221
+ expect(logger1).toBe(logger2)
222
+ })
@@ -0,0 +1,96 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import { Barrier, BrokenBarrierError, CoordinationAbortError, CoordinationTimeoutError } from "#Source/orchestration/index.ts"
4
+
5
+ const getRejectedReason = async (promise: Promise<unknown>): Promise<unknown> => {
6
+ try {
7
+ await promise
8
+ }
9
+ catch (error: unknown) {
10
+ return error
11
+ }
12
+
13
+ throw new Error("Expected promise to reject.")
14
+ }
15
+
16
+ afterEach(() => {
17
+ vi.clearAllTimers()
18
+ vi.useRealTimers()
19
+ })
20
+
21
+ test("Barrier completes generations and can be reused", async () => {
22
+ const barrier = new Barrier(2)
23
+ const firstGenerationWait = barrier.signalAndWait()
24
+
25
+ expect(barrier.getParticipantCount()).toBe(2)
26
+ expect(barrier.getGeneration()).toBe(0)
27
+ expect(barrier.getPendingCount()).toBe(1)
28
+ expect(barrier.getRemainingCount()).toBe(1)
29
+
30
+ const secondArrivalGeneration = await barrier.signalAndWait()
31
+ const firstArrivalGeneration = await firstGenerationWait
32
+
33
+ expect(firstArrivalGeneration).toBe(0)
34
+ expect(secondArrivalGeneration).toBe(0)
35
+ expect(barrier.getGeneration()).toBe(1)
36
+ expect(barrier.getPendingCount()).toBe(0)
37
+ expect(barrier.getRemainingCount()).toBe(2)
38
+
39
+ const thirdArrivalGenerationPromise = barrier.signalAndWait()
40
+ const fourthArrivalGeneration = await barrier.signalAndWait()
41
+
42
+ expect(await thirdArrivalGenerationPromise).toBe(1)
43
+ expect(fourthArrivalGeneration).toBe(1)
44
+ expect(barrier.getGeneration()).toBe(2)
45
+ })
46
+
47
+ test("Barrier timeout breaks the current generation for all waiting participants", async () => {
48
+ vi.useFakeTimers()
49
+
50
+ const barrier = new Barrier(3)
51
+ const firstWait = barrier.signalAndWait()
52
+ const secondWait = barrier.signalAndWait({ timeout: 25 })
53
+
54
+ expect(barrier.getPendingCount()).toBe(2)
55
+ expect(barrier.getRemainingCount()).toBe(1)
56
+
57
+ await vi.advanceTimersByTimeAsync(25)
58
+
59
+ const timeoutError = await getRejectedReason(secondWait)
60
+ const brokenError = await getRejectedReason(firstWait)
61
+
62
+ expect(timeoutError).toBeInstanceOf(CoordinationTimeoutError)
63
+ expect(brokenError).toBeInstanceOf(BrokenBarrierError)
64
+
65
+ if (!(brokenError instanceof BrokenBarrierError)) {
66
+ throw new Error("Expected brokenError to be an instance of BrokenBarrierError.")
67
+ }
68
+
69
+ expect(brokenError.reason).toBe(timeoutError)
70
+ expect(barrier.getGeneration()).toBe(1)
71
+ expect(barrier.getPendingCount()).toBe(0)
72
+ expect(barrier.getRemainingCount()).toBe(3)
73
+ })
74
+
75
+ test("Barrier abort rejects the failing participant and breaks peers", async () => {
76
+ const barrier = new Barrier(3)
77
+ const abortController = new AbortController()
78
+ const firstWait = barrier.signalAndWait()
79
+ const secondWait = barrier.signalAndWait({ abortSignal: abortController.signal })
80
+
81
+ abortController.abort("cancelled")
82
+
83
+ const abortError = await getRejectedReason(secondWait)
84
+ const brokenError = await getRejectedReason(firstWait)
85
+
86
+ expect(abortError).toBeInstanceOf(CoordinationAbortError)
87
+ expect(brokenError).toBeInstanceOf(BrokenBarrierError)
88
+
89
+ if (!(brokenError instanceof BrokenBarrierError)) {
90
+ throw new Error("Expected brokenError to be an instance of BrokenBarrierError.")
91
+ }
92
+
93
+ expect(brokenError.reason).toBe(abortError)
94
+ expect(barrier.getGeneration()).toBe(1)
95
+ expect(barrier.getPendingCount()).toBe(0)
96
+ })
@@ -0,0 +1,63 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import { CoordinationAbortError, CoordinationTimeoutError, CountDownLatch } from "#Source/orchestration/index.ts"
4
+
5
+ afterEach(() => {
6
+ vi.clearAllTimers()
7
+ vi.useRealTimers()
8
+ })
9
+
10
+ test("CountDownLatch opens when the count reaches zero and resolves waiters", async () => {
11
+ const countDownLatch = new CountDownLatch(2)
12
+ const firstWait = countDownLatch.wait()
13
+ const secondWait = countDownLatch.wait()
14
+
15
+ expect(countDownLatch.isOpen()).toBe(false)
16
+ expect(countDownLatch.tryWait()).toBe(false)
17
+ expect(countDownLatch.getRemainingCount()).toBe(2)
18
+ expect(countDownLatch.getPendingCount()).toBe(2)
19
+
20
+ expect(countDownLatch.countDown()).toBe(1)
21
+ expect(countDownLatch.arrive()).toBe(0)
22
+
23
+ await Promise.all([firstWait, secondWait])
24
+
25
+ expect(countDownLatch.isOpen()).toBe(true)
26
+ expect(countDownLatch.tryWait()).toBe(true)
27
+ expect(countDownLatch.getRemainingCount()).toBe(0)
28
+ expect(countDownLatch.getPendingCount()).toBe(0)
29
+ await expect(countDownLatch.wait()).resolves.toBeUndefined()
30
+ })
31
+
32
+ test("CountDownLatch wait rejects on timeout and abort without leaking pending entries", async () => {
33
+ vi.useFakeTimers()
34
+
35
+ const countDownLatch = new CountDownLatch(1)
36
+ const timeoutPromise = countDownLatch.wait({ timeout: 30 })
37
+
38
+ expect(countDownLatch.getPendingCount()).toBe(1)
39
+
40
+ await vi.advanceTimersByTimeAsync(30)
41
+
42
+ await expect(timeoutPromise).rejects.toBeInstanceOf(CoordinationTimeoutError)
43
+ expect(countDownLatch.getPendingCount()).toBe(0)
44
+
45
+ const abortController = new AbortController()
46
+ const abortPromise = countDownLatch.wait({ abortSignal: abortController.signal })
47
+
48
+ expect(countDownLatch.getPendingCount()).toBe(1)
49
+
50
+ abortController.abort("cancelled")
51
+
52
+ await expect(abortPromise).rejects.toBeInstanceOf(CoordinationAbortError)
53
+ expect(countDownLatch.getPendingCount()).toBe(0)
54
+ })
55
+
56
+ test("CountDownLatch ignores further countdowns after opening", () => {
57
+ const countDownLatch = new CountDownLatch(1)
58
+
59
+ expect(countDownLatch.countDown()).toBe(0)
60
+ expect(countDownLatch.countDown()).toBe(0)
61
+ expect(countDownLatch.arrive()).toBe(0)
62
+ expect(countDownLatch.isOpen()).toBe(true)
63
+ })
@@ -0,0 +1,29 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { BrokenBarrierError, CoordinationAbortError, CoordinationTimeoutError } from "#Source/orchestration/index.ts"
4
+
5
+ test("CoordinationAbortError stores operation, reason and formatted message", () => {
6
+ const error = new CoordinationAbortError("Mutex acquire", "cancelled")
7
+
8
+ expect(error.name).toBe("CoordinationAbortError")
9
+ expect(error.operation).toBe("Mutex acquire")
10
+ expect(error.reason).toBe("cancelled")
11
+ expect(error.message).toBe("Mutex acquire aborted. cancelled")
12
+ })
13
+
14
+ test("CoordinationTimeoutError stores operation, timeout and formatted message", () => {
15
+ const error = new CoordinationTimeoutError("Semaphore acquire", 25)
16
+
17
+ expect(error.name).toBe("CoordinationTimeoutError")
18
+ expect(error.operation).toBe("Semaphore acquire")
19
+ expect(error.timeout).toBe(25)
20
+ expect(error.message).toBe("Semaphore acquire timeout after 25ms.")
21
+ })
22
+
23
+ test("BrokenBarrierError stores reason and formatted message", () => {
24
+ const error = new BrokenBarrierError("peer aborted")
25
+
26
+ expect(error.name).toBe("BrokenBarrierError")
27
+ expect(error.reason).toBe("peer aborted")
28
+ expect(error.message).toBe("Barrier generation was broken by another participant. peer aborted")
29
+ })
@@ -0,0 +1,109 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import { CoordinationAbortError, KeyedLock } from "#Source/orchestration/index.ts"
4
+
5
+ test("KeyedLock isolates different keys and cleans state after release", () => {
6
+ const keyedLock = new KeyedLock<string>()
7
+
8
+ expect(keyedLock.getKeyCount()).toBe(0)
9
+ expect(keyedLock.hasKey("alpha")).toBe(false)
10
+
11
+ const alphaPermit = keyedLock.tryAcquire("alpha")
12
+ const betaPermit = keyedLock.tryAcquire("beta")
13
+
14
+ expect(alphaPermit?.details).toEqual({ coordination: "keyed-lock", key: "alpha" })
15
+ expect(betaPermit?.details).toEqual({ coordination: "keyed-lock", key: "beta" })
16
+ expect(keyedLock.tryAcquire("alpha")).toBeUndefined()
17
+ expect(keyedLock.hasKey("alpha")).toBe(true)
18
+ expect(keyedLock.hasKey("beta")).toBe(true)
19
+ expect(keyedLock.getKeyCount()).toBe(2)
20
+
21
+ alphaPermit?.release()
22
+ betaPermit?.release()
23
+
24
+ expect(keyedLock.hasKey("alpha")).toBe(false)
25
+ expect(keyedLock.hasKey("beta")).toBe(false)
26
+ expect(keyedLock.getKeyCount()).toBe(0)
27
+ })
28
+
29
+ test("KeyedLock acquire cleans aborted waiters and releases state when idle", async () => {
30
+ const keyedLock = new KeyedLock<string>()
31
+ const blockingPermit = keyedLock.tryAcquire("alpha")
32
+ const abortController = new AbortController()
33
+
34
+ expect(blockingPermit).toBeDefined()
35
+
36
+ const waitingPermitPromise = keyedLock.acquire("alpha", { abortSignal: abortController.signal })
37
+
38
+ expect(keyedLock.hasKey("alpha")).toBe(true)
39
+ expect(keyedLock.getKeyCount()).toBe(1)
40
+
41
+ abortController.abort("cancelled")
42
+
43
+ await expect(waitingPermitPromise).rejects.toBeInstanceOf(CoordinationAbortError)
44
+
45
+ expect(keyedLock.hasKey("alpha")).toBe(true)
46
+
47
+ blockingPermit?.release()
48
+
49
+ expect(keyedLock.hasKey("alpha")).toBe(false)
50
+ expect(keyedLock.getKeyCount()).toBe(0)
51
+ })
52
+
53
+ test("KeyedLock runExclusive serializes callbacks for the same key", async () => {
54
+ const keyedLock = new KeyedLock<string>()
55
+ let activeCount = 0
56
+ let peakCount = 0
57
+
58
+ await Promise.all([
59
+ keyedLock.runExclusive("alpha", async () => {
60
+ activeCount = activeCount + 1
61
+ peakCount = Math.max(peakCount, activeCount)
62
+ await Promise.resolve()
63
+ activeCount = activeCount - 1
64
+ }),
65
+ keyedLock.runExclusive("alpha", async () => {
66
+ activeCount = activeCount + 1
67
+ peakCount = Math.max(peakCount, activeCount)
68
+ await Promise.resolve()
69
+ activeCount = activeCount - 1
70
+ }),
71
+ ])
72
+
73
+ expect(peakCount).toBe(1)
74
+ expect(keyedLock.hasKey("alpha")).toBe(false)
75
+ })
76
+
77
+ test("KeyedLock duplicate release stays silent by default and uses custom handler when provided", () => {
78
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
79
+ // no-op to silence warnings during test
80
+ })
81
+ const silentKeyedLock = new KeyedLock<string>()
82
+ const silentPermit = silentKeyedLock.tryAcquire("alpha")
83
+
84
+ expect(silentPermit).toBeDefined()
85
+
86
+ silentPermit?.release()
87
+ silentPermit?.release()
88
+ silentPermit?.release()
89
+
90
+ expect(silentKeyedLock.hasKey("alpha")).toBe(false)
91
+ expect(warnSpy).not.toHaveBeenCalled()
92
+
93
+ const onDuplicateRelease = vi.fn()
94
+ const customKeyedLock = new KeyedLock<string>({ onDuplicateRelease })
95
+ const customPermit = customKeyedLock.tryAcquire("alpha")
96
+
97
+ expect(customPermit).toBeDefined()
98
+
99
+ customPermit?.release()
100
+ customPermit?.release()
101
+ customPermit?.release()
102
+
103
+ expect(customKeyedLock.hasKey("alpha")).toBe(false)
104
+ expect(onDuplicateRelease).toHaveBeenCalledTimes(2)
105
+ expect(onDuplicateRelease).toHaveBeenCalledWith("KeyedLock permit release was called more than once.")
106
+ expect(warnSpy).not.toHaveBeenCalled()
107
+
108
+ warnSpy.mockRestore()
109
+ })
@@ -0,0 +1,132 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import { CoordinationAbortError, CoordinationTimeoutError, Mutex } from "#Source/orchestration/index.ts"
4
+
5
+ afterEach(() => {
6
+ vi.clearAllTimers()
7
+ vi.useRealTimers()
8
+ vi.restoreAllMocks()
9
+ })
10
+
11
+ test("Mutex reports lock state and wakes queued acquirers in FIFO order", async () => {
12
+ const mutex = new Mutex()
13
+
14
+ expect(mutex.isLocked()).toBe(false)
15
+ expect(mutex.getPendingCount()).toBe(0)
16
+
17
+ const firstPermit = mutex.tryAcquire()
18
+
19
+ expect(firstPermit).toBeDefined()
20
+ expect(mutex.isLocked()).toBe(true)
21
+
22
+ const secondPermitPromise = mutex.acquire()
23
+ const thirdPermitPromise = mutex.acquire()
24
+
25
+ expect(mutex.getPendingCount()).toBe(2)
26
+ expect(mutex.tryAcquire()).toBeUndefined()
27
+
28
+ firstPermit?.release()
29
+
30
+ const secondPermit = await secondPermitPromise
31
+ let thirdResolved = false
32
+ void thirdPermitPromise.then(() => {
33
+ thirdResolved = true
34
+ })
35
+
36
+ await Promise.resolve()
37
+
38
+ expect(mutex.isLocked()).toBe(true)
39
+ expect(mutex.getPendingCount()).toBe(1)
40
+ expect(thirdResolved).toBe(false)
41
+
42
+ secondPermit.release()
43
+
44
+ const thirdPermit = await thirdPermitPromise
45
+
46
+ expect(mutex.getPendingCount()).toBe(0)
47
+ expect(mutex.isLocked()).toBe(true)
48
+
49
+ thirdPermit.release()
50
+
51
+ expect(mutex.isLocked()).toBe(false)
52
+ })
53
+
54
+ test("Mutex acquire rejects on timeout and abort while cleaning pending state", async () => {
55
+ vi.useFakeTimers()
56
+
57
+ const mutex = new Mutex()
58
+ const blockingPermit = mutex.tryAcquire()
59
+
60
+ expect(blockingPermit).toBeDefined()
61
+
62
+ const timeoutPromise = mutex.acquire({ timeout: 10 })
63
+
64
+ expect(mutex.getPendingCount()).toBe(1)
65
+
66
+ await vi.advanceTimersByTimeAsync(10)
67
+
68
+ await expect(timeoutPromise).rejects.toBeInstanceOf(CoordinationTimeoutError)
69
+ expect(mutex.getPendingCount()).toBe(0)
70
+
71
+ const abortController = new AbortController()
72
+ const abortPromise = mutex.acquire({ abortSignal: abortController.signal })
73
+
74
+ expect(mutex.getPendingCount()).toBe(1)
75
+
76
+ abortController.abort("cancelled")
77
+
78
+ await expect(abortPromise).rejects.toBeInstanceOf(CoordinationAbortError)
79
+ expect(mutex.getPendingCount()).toBe(0)
80
+
81
+ blockingPermit?.release()
82
+ })
83
+
84
+ test("Mutex runExclusive releases the lock after callback success or failure", async () => {
85
+ const mutex = new Mutex()
86
+
87
+ await expect(mutex.runExclusive(() => "done")).resolves.toBe("done")
88
+ expect(mutex.isLocked()).toBe(false)
89
+
90
+ const error = new Error("boom")
91
+
92
+ await expect(mutex.runExclusive(() => {
93
+ throw error
94
+ })).rejects.toThrow(error)
95
+
96
+ expect(mutex.isLocked()).toBe(false)
97
+ })
98
+
99
+ test("Mutex is silent by default when release is called more than once", () => {
100
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
101
+ // no-op to silence warnings during test
102
+ })
103
+ const mutex = new Mutex()
104
+
105
+ const permit = mutex.tryAcquire()
106
+ expect(permit).toBeDefined()
107
+
108
+ permit?.release()
109
+ permit?.release()
110
+ permit?.release()
111
+
112
+ expect(warnSpy).not.toHaveBeenCalled()
113
+ })
114
+
115
+ test("Mutex uses custom duplicate-release handler when provided", () => {
116
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
117
+ // no-op to silence warnings during test
118
+ })
119
+ const onDuplicateRelease = vi.fn()
120
+ const mutex = new Mutex({ onDuplicateRelease })
121
+
122
+ const permit = mutex.tryAcquire()
123
+ expect(permit).toBeDefined()
124
+
125
+ permit?.release()
126
+ permit?.release()
127
+ permit?.release()
128
+
129
+ expect(onDuplicateRelease).toHaveBeenCalledTimes(2)
130
+ expect(onDuplicateRelease).toHaveBeenCalledWith("Mutex permit release was called more than once.")
131
+ expect(warnSpy).not.toHaveBeenCalled()
132
+ })
@@ -0,0 +1,43 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import { Permit } from "#Source/orchestration/index.ts"
4
+
5
+ test("Permit release updates lifecycle state and only calls handlers once", () => {
6
+ const onRelease = vi.fn()
7
+ const onDuplicateRelease = vi.fn()
8
+ const permit = new Permit({
9
+ details: { coordination: "custom" },
10
+ duplicateReleaseMessage: "duplicate release",
11
+ onDuplicateRelease,
12
+ onRelease,
13
+ })
14
+
15
+ expect(permit.details).toEqual({ coordination: "custom" })
16
+ expect(permit.isActive()).toBe(true)
17
+ expect(permit.isReleased()).toBe(false)
18
+
19
+ permit.release()
20
+
21
+ expect(onRelease).toHaveBeenCalledTimes(1)
22
+ expect(permit.isActive()).toBe(false)
23
+ expect(permit.isReleased()).toBe(true)
24
+
25
+ permit.release()
26
+
27
+ expect(onDuplicateRelease).toHaveBeenCalledTimes(1)
28
+ expect(onDuplicateRelease).toHaveBeenCalledWith("duplicate release")
29
+ expect(onRelease).toHaveBeenCalledTimes(1)
30
+ })
31
+
32
+ test("Permit dispose and Symbol.dispose delegate to release", () => {
33
+ const onRelease = vi.fn()
34
+ const onDuplicateRelease = vi.fn()
35
+ const permit = new Permit({ onDuplicateRelease, onRelease })
36
+
37
+ permit.dispose()
38
+ permit[Symbol.dispose]()
39
+
40
+ expect(onRelease).toHaveBeenCalledTimes(1)
41
+ expect(onDuplicateRelease).toHaveBeenCalledTimes(1)
42
+ expect(permit.isReleased()).toBe(true)
43
+ })