1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Hash, HashMap, Option, Result, TxHashMap } from "effect"
3
4describe("TxHashMap", () => {
5 describe("constructors", () => {
6 it.effect("empty", () =>
7 Effect.tx(Effect.gen(function*() {
8 const txMap = yield* TxHashMap.empty<string, number>()
9 const isEmpty = yield* TxHashMap.isEmpty(txMap)
10 const size = yield* TxHashMap.size(txMap)
11
12 assert.strictEqual(isEmpty, true)
13 assert.strictEqual(size, 0)
14 })))
15
16 it.effect("make", () =>
17 Effect.tx(Effect.gen(function*() {
18 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2], ["c", 3])
19 const size = yield* TxHashMap.size(txMap)
20 const a = yield* TxHashMap.get(txMap, "a")
21 const b = yield* TxHashMap.get(txMap, "b")
22 const c = yield* TxHashMap.get(txMap, "c")
23
24 assert.strictEqual(size, 3)
25 assert.deepStrictEqual(a, Option.some(1))
26 assert.deepStrictEqual(b, Option.some(2))
27 assert.deepStrictEqual(c, Option.some(3))
28 })))
29
30 it.effect("fromIterable", () =>
31 Effect.tx(Effect.gen(function*() {
32 const entries = [["a", 1], ["b", 2], ["c", 3]] as const
33 const txMap = yield* TxHashMap.fromIterable(entries)
34 const size = yield* TxHashMap.size(txMap)
35 const a = yield* TxHashMap.get(txMap, "a")
36 const b = yield* TxHashMap.get(txMap, "b")
37 const c = yield* TxHashMap.get(txMap, "c")
38
39 assert.strictEqual(size, 3)
40 assert.deepStrictEqual(a, Option.some(1))
41 assert.deepStrictEqual(b, Option.some(2))
42 assert.deepStrictEqual(c, Option.some(3))
43 })))
44 })
45
46 describe("basic operations", () => {
47 it.effect("get - existing key", () =>
48 Effect.tx(Effect.gen(function*() {
49 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
50 const a = yield* TxHashMap.get(txMap, "a")
51 const b = yield* TxHashMap.get(txMap, "b")
52
53 assert.deepStrictEqual(a, Option.some(1))
54 assert.deepStrictEqual(b, Option.some(2))
55 })))
56
57 it.effect("get - non-existing key", () =>
58 Effect.tx(Effect.gen(function*() {
59 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
60 const c = yield* TxHashMap.get(txMap, "c")
61
62 assert.deepStrictEqual(c, Option.none())
63 })))
64
65 it.effect("has - existing key", () =>
66 Effect.tx(Effect.gen(function*() {
67 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
68 const hasA = yield* TxHashMap.has(txMap, "a")
69 const hasB = yield* TxHashMap.has(txMap, "b")
70
71 assert.strictEqual(hasA, true)
72 assert.strictEqual(hasB, true)
73 })))
74
75 it.effect("has - non-existing key", () =>
76 Effect.tx(Effect.gen(function*() {
77 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
78 const hasC = yield* TxHashMap.has(txMap, "c")
79
80 assert.strictEqual(hasC, false)
81 })))
82
83 it.effect("set - new key", () =>
84 Effect.tx(Effect.gen(function*() {
85 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
86 yield* TxHashMap.set(txMap, "c", 3)
87
88 const size = yield* TxHashMap.size(txMap)
89 const c = yield* TxHashMap.get(txMap, "c")
90
91 assert.strictEqual(size, 3)
92 assert.deepStrictEqual(c, Option.some(3))
93 })))
94
95 it.effect("set - existing key", () =>
96 Effect.tx(Effect.gen(function*() {
97 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
98 yield* TxHashMap.set(txMap, "a", 10)
99
100 const size = yield* TxHashMap.size(txMap)
101 const a = yield* TxHashMap.get(txMap, "a")
102
103 assert.strictEqual(size, 2) // size unchanged
104 assert.deepStrictEqual(a, Option.some(10))
105 })))
106
107 it.effect("remove - existing key", () =>
108 Effect.tx(Effect.gen(function*() {
109 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2], ["c", 3])
110 const removed = yield* TxHashMap.remove(txMap, "b")
111
112 const size = yield* TxHashMap.size(txMap)
113 const hasB = yield* TxHashMap.has(txMap, "b")
114
115 assert.strictEqual(removed, true)
116 assert.strictEqual(size, 2)
117 assert.strictEqual(hasB, false)
118 })))
119
120 it.effect("remove - non-existing key", () =>
121 Effect.tx(Effect.gen(function*() {
122 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
123 const removed = yield* TxHashMap.remove(txMap, "c")
124
125 const size = yield* TxHashMap.size(txMap)
126
127 assert.strictEqual(removed, false)
128 assert.strictEqual(size, 2)
129 })))
130
131 it.effect("clear", () =>
132 Effect.tx(Effect.gen(function*() {
133 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2], ["c", 3])
134 yield* TxHashMap.clear(txMap)
135
136 const size = yield* TxHashMap.size(txMap)
137 const isEmpty = yield* TxHashMap.isEmpty(txMap)
138
139 assert.strictEqual(size, 0)
140 assert.strictEqual(isEmpty, true)
141 })))
142 })
143
144 describe("query operations", () => {
145 it.effect("size", () =>
146 Effect.tx(Effect.gen(function*() {
147 const empty = yield* TxHashMap.empty<string, number>()
148 const small = yield* TxHashMap.make(["a", 1])
149 const large = yield* TxHashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4])
150
151 assert.strictEqual(yield* TxHashMap.size(empty), 0)
152 assert.strictEqual(yield* TxHashMap.size(small), 1)
153 assert.strictEqual(yield* TxHashMap.size(large), 4)
154 })))
155
156 it.effect("isEmpty", () =>
157 Effect.tx(Effect.gen(function*() {
158 const empty = yield* TxHashMap.empty<string, number>()
159 const nonEmpty = yield* TxHashMap.make(["a", 1])
160
161 assert.strictEqual(yield* TxHashMap.isEmpty(empty), true)
162 assert.strictEqual(yield* TxHashMap.isEmpty(nonEmpty), false)
163 })))
164
165 it.effect("isNonEmpty", () =>
166 Effect.tx(Effect.gen(function*() {
167 const empty = yield* TxHashMap.empty<string, number>()
168 const nonEmpty = yield* TxHashMap.make(["a", 1])
169
170 assert.strictEqual(yield* TxHashMap.isNonEmpty(empty), false)
171 assert.strictEqual(yield* TxHashMap.isNonEmpty(nonEmpty), true)
172 })))
173 })
174
175 describe("advanced operations", () => {
176 it.effect("modify - existing key", () =>
177 Effect.tx(Effect.gen(function*() {
178 const txMap = yield* TxHashMap.make(["counter", 5])
179 const oldValue = yield* TxHashMap.modify(txMap, "counter", (n) => n * 2)
180 const newValue = yield* TxHashMap.get(txMap, "counter")
181
182 assert.deepStrictEqual(oldValue, Option.some(5))
183 assert.deepStrictEqual(newValue, Option.some(10))
184 })))
185
186 it.effect("modify - non-existing key", () =>
187 Effect.tx(Effect.gen(function*() {
188 const txMap = yield* TxHashMap.empty<string, number>()
189 const oldValue = yield* TxHashMap.modify(txMap, "counter", (n) => n * 2)
190 const size = yield* TxHashMap.size(txMap)
191
192 assert.deepStrictEqual(oldValue, Option.none())
193 assert.strictEqual(size, 0)
194 })))
195
196 it.effect("modifyAt - insert new value", () =>
197 Effect.tx(Effect.gen(function*() {
198 const txMap = yield* TxHashMap.make(["a", 1])
199 yield* TxHashMap.modifyAt(txMap, "b", () => Option.some(2))
200
201 const size = yield* TxHashMap.size(txMap)
202 const b = yield* TxHashMap.get(txMap, "b")
203
204 assert.strictEqual(size, 2)
205 assert.deepStrictEqual(b, Option.some(2))
206 })))
207
208 it.effect("modifyAt - remove existing value", () =>
209 Effect.tx(Effect.gen(function*() {
210 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
211 yield* TxHashMap.modifyAt(txMap, "a", () => Option.none())
212
213 const size = yield* TxHashMap.size(txMap)
214 const hasA = yield* TxHashMap.has(txMap, "a")
215
216 assert.strictEqual(size, 1)
217 assert.strictEqual(hasA, false)
218 })))
219
220 it.effect("keys", () =>
221 Effect.tx(Effect.gen(function*() {
222 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2], ["c", 3])
223 const keys = yield* TxHashMap.keys(txMap)
224
225 assert.deepStrictEqual(keys.sort(), ["a", "b", "c"])
226 })))
227
228 it.effect("values", () =>
229 Effect.tx(Effect.gen(function*() {
230 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2], ["c", 3])
231 const values = yield* TxHashMap.values(txMap)
232
233 assert.deepStrictEqual(values.sort(), [1, 2, 3])
234 })))
235
236 it.effect("entries", () =>
237 Effect.tx(Effect.gen(function*() {
238 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
239 const entries = yield* TxHashMap.entries(txMap)
240
241 assert.deepStrictEqual(entries.sort(), [["a", 1], ["b", 2]])
242 })))
243
244 it.effect("snapshot", () =>
245 Effect.tx(Effect.gen(function*() {
246 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
247 const snapshot = yield* TxHashMap.snapshot(txMap)
248
249 // Modify the TxHashMap after taking snapshot
250 yield* TxHashMap.set(txMap, "c", 3)
251
252 // Snapshot should be unchanged
253 assert.strictEqual(HashMap.size(snapshot), 2)
254 assert.deepStrictEqual(HashMap.get(snapshot, "a"), Option.some(1))
255 assert.deepStrictEqual(HashMap.get(snapshot, "b"), Option.some(2))
256 assert.deepStrictEqual(HashMap.get(snapshot, "c"), Option.none())
257
258 // Original should be modified
259 assert.strictEqual(yield* TxHashMap.size(txMap), 3)
260 })))
261 })
262
263 describe("bulk operations", () => {
264 it.effect("union", () =>
265 Effect.tx(Effect.gen(function*() {
266 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
267 const other = HashMap.make(["b", 20], ["c", 3]) // "b" should be overwritten
268
269 yield* TxHashMap.union(txMap, other)
270
271 const size = yield* TxHashMap.size(txMap)
272 const a = yield* TxHashMap.get(txMap, "a")
273 const b = yield* TxHashMap.get(txMap, "b")
274 const c = yield* TxHashMap.get(txMap, "c")
275
276 assert.strictEqual(size, 3)
277 assert.deepStrictEqual(a, Option.some(1))
278 assert.deepStrictEqual(b, Option.some(20)) // overwritten
279 assert.deepStrictEqual(c, Option.some(3))
280 })))
281
282 it.effect("removeMany", () =>
283 Effect.tx(Effect.gen(function*() {
284 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4])
285 yield* TxHashMap.removeMany(txMap, ["b", "d"])
286
287 const size = yield* TxHashMap.size(txMap)
288 const hasA = yield* TxHashMap.has(txMap, "a")
289 const hasB = yield* TxHashMap.has(txMap, "b")
290 const hasC = yield* TxHashMap.has(txMap, "c")
291 const hasD = yield* TxHashMap.has(txMap, "d")
292
293 assert.strictEqual(size, 2)
294 assert.strictEqual(hasA, true)
295 assert.strictEqual(hasB, false)
296 assert.strictEqual(hasC, true)
297 assert.strictEqual(hasD, false)
298 })))
299
300 it.effect("setMany", () =>
301 Effect.tx(Effect.gen(function*() {
302 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
303 const newEntries = [["c", 3], ["d", 4], ["a", 10]] as const // "a" should be overwritten
304
305 yield* TxHashMap.setMany(txMap, newEntries)
306
307 const size = yield* TxHashMap.size(txMap)
308 const a = yield* TxHashMap.get(txMap, "a")
309 const b = yield* TxHashMap.get(txMap, "b")
310 const c = yield* TxHashMap.get(txMap, "c")
311 const d = yield* TxHashMap.get(txMap, "d")
312
313 assert.strictEqual(size, 4)
314 assert.deepStrictEqual(a, Option.some(10)) // overwritten
315 assert.deepStrictEqual(b, Option.some(2)) // preserved
316 assert.deepStrictEqual(c, Option.some(3)) // new
317 assert.deepStrictEqual(d, Option.some(4)) // new
318 })))
319 })
320
321 describe("transactional semantics", () => {
322 it.effect("single operations are automatically transactional", () =>
323 Effect.tx(Effect.gen(function*() {
324 const txMap = yield* TxHashMap.make(["counter", 0])
325
326 // These operations should be individually atomic
327 yield* TxHashMap.set(txMap, "counter", 1)
328 yield* TxHashMap.modify(txMap, "counter", (n) => n + 1)
329
330 const result = yield* TxHashMap.get(txMap, "counter")
331 assert.deepStrictEqual(result, Option.some(2))
332 })))
333
334 it.effect("multi-step operations commit together", () =>
335 Effect.tx(Effect.gen(function*() {
336 const txMap = yield* TxHashMap.make(["a", 1], ["b", 2])
337
338 // Multi-step operations
339 const currentA = yield* TxHashMap.get(txMap, "a")
340 if (Option.isSome(currentA)) {
341 yield* TxHashMap.set(txMap, "a", currentA.value * 10)
342 yield* TxHashMap.remove(txMap, "b")
343 yield* TxHashMap.set(txMap, "c", 3)
344 }
345
346 const a = yield* TxHashMap.get(txMap, "a")
347 const hasB = yield* TxHashMap.has(txMap, "b")
348 const c = yield* TxHashMap.get(txMap, "c")
349 const size = yield* TxHashMap.size(txMap)
350
351 assert.deepStrictEqual(a, Option.some(10))
352 assert.strictEqual(hasB, false)
353 assert.deepStrictEqual(c, Option.some(3))
354 assert.strictEqual(size, 2)
355 })))
356 })
357
358 describe("pipe syntax", () => {
359 it.effect("supports data-last operations", () =>
360 Effect.tx(Effect.gen(function*() {
361 const txMap = yield* TxHashMap.empty<string, number>()
362
363 // Test data-last pipe operations
364 yield* Effect.flatMap(TxHashMap.set(txMap, "a", 1), () => Effect.void)
365 yield* Effect.flatMap(TxHashMap.set(txMap, "b", 2), () => Effect.void)
366
367 const result = yield* TxHashMap.get(txMap, "a")
368 const size = yield* TxHashMap.size(txMap)
369
370 assert.deepStrictEqual(result, Option.some(1))
371 assert.strictEqual(size, 2)
372 })))
373 })
374
375 describe("Phase 1: Essential Functions", () => {
376 it.effect("isTxHashMap should correctly identify TxHashMap instances", () =>
377 Effect.tx(Effect.gen(function*() {
378 const txMap = yield* TxHashMap.make(["key", "value"])
379
380 // Positive cases
381 assert.strictEqual(TxHashMap.isTxHashMap(txMap), true)
382
383 // Negative cases
384 assert.strictEqual(TxHashMap.isTxHashMap({}), false)
385 assert.strictEqual(TxHashMap.isTxHashMap(null), false)
386 assert.strictEqual(TxHashMap.isTxHashMap(undefined), false)
387 assert.strictEqual(TxHashMap.isTxHashMap("string"), false)
388 assert.strictEqual(TxHashMap.isTxHashMap(42), false)
389 assert.strictEqual(TxHashMap.isTxHashMap([]), false)
390 assert.strictEqual(TxHashMap.isTxHashMap(new Map()), false)
391 })))
392
393 it.effect("getHash should lookup values using custom hash", () =>
394 Effect.tx(Effect.gen(function*() {
395 const txMap = yield* TxHashMap.make(
396 ["user123", { name: "Alice", role: "admin" }],
397 ["user456", { name: "Bob", role: "user" }]
398 )
399
400 const userId = "user123"
401 const hash = Hash.string(userId)
402
403 // Test data-first
404 const user1 = yield* TxHashMap.getHash(txMap, userId, hash)
405 assert.deepStrictEqual(user1, Option.some({ name: "Alice", role: "admin" }))
406
407 // Test data-last
408 const user2 = yield* txMap.pipe(TxHashMap.getHash(userId, hash))
409 assert.deepStrictEqual(user2, Option.some({ name: "Alice", role: "admin" }))
410
411 // Test non-existent key
412 const notFound = yield* TxHashMap.getHash(txMap, "user999", Hash.string("user999"))
413 assert.deepStrictEqual(notFound, Option.none())
414
415 // Test with wrong hash (may not find the value due to hash mismatch)
416 const wrongHash = yield* TxHashMap.getHash(txMap, userId, Hash.string("different"))
417 assert.deepStrictEqual(wrongHash, Option.none())
418 })))
419
420 it.effect("hasHash should check existence using custom hash", () =>
421 Effect.tx(Effect.gen(function*() {
422 const txMap = yield* TxHashMap.make(
423 ["admin", { read: true, write: true }],
424 ["user", { read: true, write: false }]
425 )
426
427 const role = "admin"
428 const hash = Hash.string(role)
429
430 // Test data-first
431 const hasAdmin1 = yield* TxHashMap.hasHash(txMap, role, hash)
432 assert.strictEqual(hasAdmin1, true)
433
434 // Test data-last
435 const hasAdmin2 = yield* txMap.pipe(TxHashMap.hasHash(role, hash))
436 assert.strictEqual(hasAdmin2, true)
437
438 // Test non-existent key
439 const hasGuest = yield* TxHashMap.hasHash(txMap, "guest", Hash.string("guest"))
440 assert.strictEqual(hasGuest, false)
441
442 // Test with empty map
443 const emptyMap = yield* TxHashMap.empty<string, { read: boolean }>()
444 const hasInEmpty = yield* TxHashMap.hasHash(emptyMap, "admin", hash)
445 assert.strictEqual(hasInEmpty, false)
446 })))
447
448 it.effect("getHash and hasHash should work together", () =>
449 Effect.tx(Effect.gen(function*() {
450 const txMap = yield* TxHashMap.make(
451 ["session_abc", { userId: "user1", active: true }],
452 ["session_def", { userId: "user2", active: false }]
453 )
454
455 const sessionId = "session_abc"
456 const hash = Hash.string(sessionId)
457
458 // Check existence first
459 const exists = yield* TxHashMap.hasHash(txMap, sessionId, hash)
460 assert.strictEqual(exists, true)
461
462 // Then get the value
463 const session = yield* TxHashMap.getHash(txMap, sessionId, hash)
464 assert.deepStrictEqual(session, Option.some({ userId: "user1", active: true }))
465
466 // Test non-existent session
467 const invalidSessionId = "session_xyz"
468 const invalidHash = Hash.string(invalidSessionId)
469
470 const invalidExists = yield* TxHashMap.hasHash(txMap, invalidSessionId, invalidHash)
471 assert.strictEqual(invalidExists, false)
472
473 const invalidSession = yield* TxHashMap.getHash(txMap, invalidSessionId, invalidHash)
474 assert.deepStrictEqual(invalidSession, Option.none())
475 })))
476
477 it.effect("hash functions should work correctly", () =>
478 Effect.tx(Effect.gen(function*() {
479 const txMap = yield* TxHashMap.make(["key1", "value1"])
480 const hash = Hash.string("key2")
481
482 // Add a new key-value pair
483 yield* TxHashMap.set(txMap, "key2", "value2")
484
485 // Check with hash functions
486 const exists = yield* TxHashMap.hasHash(txMap, "key2", hash)
487 assert.strictEqual(exists, true)
488
489 const value = yield* TxHashMap.getHash(txMap, "key2", hash)
490 assert.deepStrictEqual(value, Option.some("value2"))
491
492 // Verify changes persisted after transaction
493 const finalExists = yield* TxHashMap.hasHash(txMap, "key2", hash)
494 assert.strictEqual(finalExists, true)
495 })))
496 })
497
498 describe("Phase 2: Functional Programming Operations", () => {
499 it.effect("map should transform values while preserving keys", () =>
500 Effect.tx(Effect.gen(function*() {
501 const txMap = yield* TxHashMap.make(
502 ["user1", { name: "Alice", age: 25 }],
503 ["user2", { name: "Bob", age: 30 }],
504 ["user3", { name: "Charlie", age: 35 }]
505 )
506
507 // Test data-first: extract names
508 const nameMap = yield* TxHashMap.map(txMap, (user) => user.name)
509
510 const alice = yield* TxHashMap.get(nameMap, "user1")
511 const bob = yield* TxHashMap.get(nameMap, "user2")
512 const charlie = yield* TxHashMap.get(nameMap, "user3")
513
514 assert.deepStrictEqual(alice, Option.some("Alice"))
515 assert.deepStrictEqual(bob, Option.some("Bob"))
516 assert.deepStrictEqual(charlie, Option.some("Charlie"))
517
518 // Test data-last: increment ages
519 const ageMap = yield* txMap.pipe(TxHashMap.map((user, key) => ({ ...user, age: user.age + 1, key })))
520
521 const updatedAlice = yield* TxHashMap.get(ageMap, "user1")
522 assert.deepStrictEqual(updatedAlice, Option.some({ name: "Alice", age: 26, key: "user1" }))
523 })))
524
525 it.effect("filter should keep only matching entries", () =>
526 Effect.tx(Effect.gen(function*() {
527 const txMap = yield* TxHashMap.make(
528 ["apple", { price: 1.50, category: "fruit" }],
529 ["carrot", { price: 0.80, category: "vegetable" }],
530 ["banana", { price: 2.00, category: "fruit" }],
531 ["lettuce", { price: 1.20, category: "vegetable" }]
532 )
533
534 // Test data-first: filter fruits
535 const fruitMap = yield* TxHashMap.filter(txMap, (item) => item.category === "fruit")
536
537 const size = yield* TxHashMap.size(fruitMap)
538 const hasApple = yield* TxHashMap.has(fruitMap, "apple")
539 const hasBanana = yield* TxHashMap.has(fruitMap, "banana")
540 const hasCarrot = yield* TxHashMap.has(fruitMap, "carrot")
541
542 assert.strictEqual(size, 2)
543 assert.strictEqual(hasApple, true)
544 assert.strictEqual(hasBanana, true)
545 assert.strictEqual(hasCarrot, false)
546
547 // Test data-last: filter expensive items
548 const expensiveMap = yield* txMap.pipe(TxHashMap.filter((item) => item.price > 1.50))
549 const expensiveSize = yield* TxHashMap.size(expensiveMap)
550 assert.strictEqual(expensiveSize, 1) // only banana
551 })))
552
553 it.effect("reduce should fold over all entries", () =>
554 Effect.tx(Effect.gen(function*() {
555 const txMap = yield* TxHashMap.make(
556 ["item1", 10],
557 ["item2", 20],
558 ["item3", 30]
559 )
560
561 // Test data-first: sum all values
562 const sum = yield* TxHashMap.reduce(txMap, 0, (acc, value) => acc + value)
563 assert.strictEqual(sum, 60)
564
565 // Test data-last: count entries with key info
566 const info = yield* txMap.pipe(
567 TxHashMap.reduce({ count: 0, keys: [] as Array<string> }, (acc, _, key) => ({
568 count: acc.count + 1,
569 keys: [...acc.keys, key]
570 }))
571 )
572
573 assert.strictEqual(info.count, 3)
574 assert.deepStrictEqual(info.keys.sort(), ["item1", "item2", "item3"])
575
576 // Test with empty map
577 const emptyMap = yield* TxHashMap.empty<string, number>()
578 const emptySum = yield* TxHashMap.reduce(emptyMap, 0, (acc, value) => acc + value)
579 assert.strictEqual(emptySum, 0)
580 })))
581
582 it.effect("filterMap should filter and transform in one operation", () =>
583 Effect.tx(Effect.gen(function*() {
584 const txMap = yield* TxHashMap.make(
585 ["valid1", "10"],
586 ["invalid", "not-a-number"],
587 ["valid2", "20"],
588 ["empty", ""],
589 ["valid3", "30"]
590 )
591
592 // Test data-first: parse numbers, filter out invalid ones
593 const numberMap = yield* TxHashMap.filterMap(txMap, (value) => {
594 const num = parseInt(value, 10)
595 return isNaN(num) ? Result.failVoid : Result.succeed(num)
596 })
597
598 const size = yield* TxHashMap.size(numberMap)
599 const val1 = yield* TxHashMap.get(numberMap, "valid1")
600 const val2 = yield* TxHashMap.get(numberMap, "valid2")
601 const val3 = yield* TxHashMap.get(numberMap, "valid3")
602 const invalid = yield* TxHashMap.get(numberMap, "invalid")
603
604 assert.strictEqual(size, 3)
605 assert.deepStrictEqual(val1, Option.some(10))
606 assert.deepStrictEqual(val2, Option.some(20))
607 assert.deepStrictEqual(val3, Option.some(30))
608 assert.deepStrictEqual(invalid, Option.none())
609
610 // Test data-last: extract emails from user objects
611 const userMap = yield* TxHashMap.make(
612 ["user1", { name: "Alice", email: "alice@example.com" }],
613 ["user2", { name: "Bob" }], // no email
614 ["user3", { name: "Charlie", email: "charlie@example.com" }]
615 )
616
617 const emailMap = yield* userMap.pipe(
618 TxHashMap.filterMap(
619 (user, key) => "email" in user ? Result.succeed(`${key}:${user.email}`) : Result.failVoid
620 )
621 )
622
623 const emailSize = yield* TxHashMap.size(emailMap)
624 const user1Email = yield* TxHashMap.get(emailMap, "user1")
625 const user2Email = yield* TxHashMap.get(emailMap, "user2")
626 const user3Email = yield* TxHashMap.get(emailMap, "user3")
627
628 assert.strictEqual(emailSize, 2)
629 assert.deepStrictEqual(user1Email, Option.some("user1:alice@example.com"))
630 assert.deepStrictEqual(user2Email, Option.none())
631 assert.deepStrictEqual(user3Email, Option.some("user3:charlie@example.com"))
632 })))
633
634 it.effect("hasBy should check if any entry matches predicate", () =>
635 Effect.tx(Effect.gen(function*() {
636 const txMap = yield* TxHashMap.make(
637 ["admin", { role: "admin", active: true }],
638 ["user1", { role: "user", active: true }],
639 ["user2", { role: "user", active: false }]
640 )
641
642 // Test data-first: check for admin users
643 const hasAdmin = yield* TxHashMap.hasBy(txMap, (user) => user.role === "admin")
644 assert.strictEqual(hasAdmin, true)
645
646 const hasGuest = yield* TxHashMap.hasBy(txMap, (user) => user.role === "guest")
647 assert.strictEqual(hasGuest, false)
648
649 // Test data-last: check for inactive users
650 const hasInactive = yield* txMap.pipe(TxHashMap.hasBy((user) => !user.active))
651 assert.strictEqual(hasInactive, true)
652
653 // Test with empty map
654 const emptyMap = yield* TxHashMap.empty<string, { role: string }>()
655 const hasAny = yield* TxHashMap.hasBy(emptyMap, () => true)
656 assert.strictEqual(hasAny, false)
657 })))
658
659 it.effect("findFirst should return first matching entry", () =>
660 Effect.tx(Effect.gen(function*() {
661 const txMap = yield* TxHashMap.make(
662 ["session1", { userId: "user1", loginTime: "09:00" }],
663 ["session2", { userId: "user2", loginTime: "10:00" }],
664 ["session3", { userId: "user1", loginTime: "11:00" }]
665 )
666
667 // Test data-first: find user1's first session
668 const user1Session = yield* TxHashMap.findFirst(txMap, (session) => session.userId === "user1")
669 assert.deepStrictEqual(
670 user1Session,
671 Option.some<[string, { userId: string; loginTime: string }]>([
672 "session3",
673 { userId: "user1", loginTime: "11:00" }
674 ])
675 )
676
677 // Test data-last: find session not belonging to user1 or user2
678 const otherSession = yield* txMap.pipe(
679 TxHashMap.findFirst((session) => session.userId !== "user1" && session.userId !== "user2")
680 )
681 assert.deepStrictEqual(otherSession, Option.none())
682
683 // Test with empty map
684 const emptyMap = yield* TxHashMap.empty<string, { userId: string }>()
685 const notFound = yield* TxHashMap.findFirst(emptyMap, () => true)
686 assert.deepStrictEqual(notFound, Option.none())
687 })))
688
689 it.effect("some should check if any entry matches predicate", () =>
690 Effect.tx(Effect.gen(function*() {
691 const txMap = yield* TxHashMap.make(
692 ["order1", { status: "pending", amount: 100 }],
693 ["order2", { status: "completed", amount: 250 }],
694 ["order3", { status: "pending", amount: 50 }]
695 )
696
697 // Test data-first: check if any order is completed
698 const hasCompleted = yield* TxHashMap.some(txMap, (order) => order.status === "completed")
699 assert.strictEqual(hasCompleted, true)
700
701 const hasCancelled = yield* TxHashMap.some(txMap, (order) => order.status === "cancelled")
702 assert.strictEqual(hasCancelled, false)
703
704 // Test data-last: check if any order exceeds amount
705 const hasLargeOrder = yield* txMap.pipe(TxHashMap.some((order) => order.amount > 200))
706 assert.strictEqual(hasLargeOrder, true)
707
708 const hasVeryLargeOrder = yield* txMap.pipe(TxHashMap.some((order) => order.amount > 1000))
709 assert.strictEqual(hasVeryLargeOrder, false)
710
711 // Test with empty map
712 const emptyMap = yield* TxHashMap.empty<string, { status: string }>()
713 const emptyResult = yield* TxHashMap.some(emptyMap, () => true)
714 assert.strictEqual(emptyResult, false)
715 })))
716
717 it.effect("every should check if all entries match predicate", () =>
718 Effect.tx(Effect.gen(function*() {
719 const txMap = yield* TxHashMap.make(
720 ["task1", { completed: true, priority: "high" }],
721 ["task2", { completed: true, priority: "medium" }],
722 ["task3", { completed: true, priority: "low" }]
723 )
724
725 // Test data-first: check if all tasks are completed
726 const allCompleted = yield* TxHashMap.every(txMap, (task) => task.completed)
727 assert.strictEqual(allCompleted, true)
728
729 const allHighPriority = yield* TxHashMap.every(txMap, (task) => task.priority === "high")
730 assert.strictEqual(allHighPriority, false)
731
732 // Add an incomplete task
733 yield* TxHashMap.set(txMap, "task4", { completed: false, priority: "high" })
734
735 // Test data-last: now not all tasks are completed
736 const stillAllCompleted = yield* txMap.pipe(TxHashMap.every((task) => task.completed))
737 assert.strictEqual(stillAllCompleted, false)
738
739 // Test that all tasks have a priority
740 const allHavePriority = yield* txMap.pipe(TxHashMap.every((task) => Boolean(task.priority)))
741 assert.strictEqual(allHavePriority, true)
742
743 // Test with empty map (vacuous truth)
744 const emptyMap = yield* TxHashMap.empty<string, { completed: boolean }>()
745 const emptyResult = yield* TxHashMap.every(emptyMap, () => false)
746 assert.strictEqual(emptyResult, true) // empty set satisfies all predicates
747 })))
748
749 it.effect("functional operations should work correctly", () =>
750 Effect.tx(Effect.gen(function*() {
751 const txMap = yield* TxHashMap.make(
752 ["item1", { price: 10, discount: 0 }],
753 ["item2", { price: 20, discount: 0 }],
754 ["item3", { price: 30, discount: 0 }]
755 )
756
757 // Apply bulk discount
758 const discountedMap = yield* TxHashMap.map(txMap, (item) => ({
759 ...item,
760 discount: 0.1,
761 discountedPrice: item.price * 0.9
762 }))
763
764 // Filter expensive items
765 const expensiveItems = yield* TxHashMap.filter(discountedMap, (item) => item.price > 15)
766
767 // Calculate total savings
768 const totalSavings = yield* TxHashMap.reduce(
769 expensiveItems,
770 0,
771 (acc, item) => acc + (item.price - item.discountedPrice)
772 )
773
774 // Verify operations worked correctly
775 const expensiveSize = yield* TxHashMap.size(expensiveItems)
776 assert.strictEqual(expensiveSize, 2) // item2 and item3
777
778 assert.strictEqual(totalSavings, 5) // (20 + 30) * 0.1 = 5
779
780 // Check that all expensive items have discount
781 const allDiscounted = yield* TxHashMap.every(expensiveItems, (item) => item.discount > 0)
782 assert.strictEqual(allDiscounted, true)
783 })))
784 })
785
786 describe("Phase 3: Specialized Operations", () => {
787 it.effect("forEach should execute side effects for each entry", () =>
788 Effect.tx(Effect.gen(function*() {
789 const txMap = yield* TxHashMap.make(
790 ["user1", { name: "Alice", status: "active" }],
791 ["user2", { name: "Bob", status: "inactive" }],
792 ["user3", { name: "Charlie", status: "active" }]
793 )
794
795 // Collect side effects
796 const processed: Array<{ user: string; name: string; status: string }> = []
797
798 // Test data-first: process all users
799 yield* TxHashMap.forEach(txMap, (userData, userId) =>
800 Effect.sync(() => {
801 processed.push({ user: userId, name: userData.name, status: userData.status })
802 }))
803
804 assert.strictEqual(processed.length, 3)
805 assert.strictEqual(processed.filter((p) => p.status === "active").length, 2)
806
807 // Test data-last: process only active users
808 const activeUsers: Array<string> = []
809 yield* txMap.pipe(
810 TxHashMap.forEach((userData, userId) =>
811 userData.status === "active"
812 ? Effect.sync(() => activeUsers.push(userId))
813 : Effect.void
814 )
815 )
816
817 assert.strictEqual(activeUsers.length, 2)
818 assert.deepStrictEqual(activeUsers.sort(), ["user1", "user3"])
819 })))
820
821 it.effect("flatMap should transform and flatten TxHashMaps", () =>
822 Effect.tx(Effect.gen(function*() {
823 const departments = yield* TxHashMap.make(
824 ["eng", ["alice", "bob"]],
825 ["sales", ["charlie"]]
826 )
827
828 // Transform departments into individual employee records
829 const employees = yield* TxHashMap.flatMap(
830 departments,
831 (members, dept) =>
832 Effect.gen(function*() {
833 const empMap = yield* TxHashMap.empty<string, { department: string; role: string }>()
834 for (let i = 0; i < members.length; i++) {
835 const member = members[i]
836 const role = i === 0 ? "lead" : "member"
837 yield* TxHashMap.set(empMap, member, { department: dept, role })
838 }
839 return empMap
840 })
841 )
842
843 const size = yield* TxHashMap.size(employees)
844 assert.strictEqual(size, 3)
845
846 const alice = yield* TxHashMap.get(employees, "alice")
847 assert.deepStrictEqual(alice, Option.some({ department: "eng", role: "lead" }))
848
849 const bob = yield* TxHashMap.get(employees, "bob")
850 assert.deepStrictEqual(bob, Option.some({ department: "eng", role: "member" }))
851
852 const charlie = yield* TxHashMap.get(employees, "charlie")
853 assert.deepStrictEqual(charlie, Option.some({ department: "sales", role: "lead" }))
854 })))
855
856 it.effect("compact should remove None values and unwrap Some values", () =>
857 Effect.tx(Effect.gen(function*() {
858 const optionalData = yield* TxHashMap.make<string, Option.Option<{ value: number }>>(
859 ["valid1", Option.some({ value: 10 })],
860 ["invalid1", Option.none()],
861 ["valid2", Option.some({ value: 20 })],
862 ["invalid2", Option.none()],
863 ["valid3", Option.some({ value: 30 })]
864 )
865
866 // Compact to remove None values
867 const compacted = yield* TxHashMap.compact(optionalData)
868
869 const size = yield* TxHashMap.size(compacted)
870 assert.strictEqual(size, 3)
871
872 const valid1 = yield* TxHashMap.get(compacted, "valid1")
873 assert.deepStrictEqual(valid1, Option.some({ value: 10 }))
874
875 const valid2 = yield* TxHashMap.get(compacted, "valid2")
876 assert.deepStrictEqual(valid2, Option.some({ value: 20 }))
877
878 const valid3 = yield* TxHashMap.get(compacted, "valid3")
879 assert.deepStrictEqual(valid3, Option.some({ value: 30 }))
880
881 const invalid1 = yield* TxHashMap.get(compacted, "invalid1")
882 assert.deepStrictEqual(invalid1, Option.none())
883
884 const invalid2 = yield* TxHashMap.get(compacted, "invalid2")
885 assert.deepStrictEqual(invalid2, Option.none())
886 })))
887
888 it.effect("toEntries should return all entries as array", () =>
889 Effect.tx(Effect.gen(function*() {
890 const txMap = yield* TxHashMap.make(
891 ["a", 1],
892 ["b", 2],
893 ["c", 3]
894 )
895
896 const entriesArray = yield* TxHashMap.toEntries(txMap)
897 assert.strictEqual(entriesArray.length, 3)
898
899 // Sort for consistent comparison
900 const sortedEntries = entriesArray.sort(([a], [b]) => a.localeCompare(b))
901 assert.deepStrictEqual(sortedEntries, [["a", 1], ["b", 2], ["c", 3]])
902
903 // Verify it's the same as regular entries
904 const regularEntries = yield* TxHashMap.entries(txMap)
905 const sortedRegular = regularEntries.sort(([a], [b]) => a.localeCompare(b))
906 assert.deepStrictEqual(sortedEntries, sortedRegular)
907 })))
908
909 it.effect("toValues should return all values as array", () =>
910 Effect.tx(Effect.gen(function*() {
911 const txMap = yield* TxHashMap.make(
912 ["x", { price: 100 }],
913 ["y", { price: 200 }],
914 ["z", { price: 300 }]
915 )
916
917 const valuesArray = yield* TxHashMap.toValues(txMap)
918 assert.strictEqual(valuesArray.length, 3)
919
920 // Sort for consistent comparison
921 const sortedValues = valuesArray.sort((a, b) => a.price - b.price)
922 assert.deepStrictEqual(sortedValues, [{ price: 100 }, { price: 200 }, { price: 300 }])
923
924 // Verify it's the same as regular values
925 const regularValues = yield* TxHashMap.values(txMap)
926 const sortedRegular = regularValues.sort((a, b) => a.price - b.price)
927 assert.deepStrictEqual(sortedValues, sortedRegular)
928 })))
929
930 it.effect("specialized operations should work correctly", () =>
931 Effect.tx(Effect.gen(function*() {
932 const txMap = yield* TxHashMap.make<string, Option.Option<number>>(
933 ["a", Option.some(1)],
934 ["b", Option.none()],
935 ["c", Option.some(3)]
936 )
937
938 // Add more data
939 yield* TxHashMap.set(txMap, "d", Option.some(4))
940 yield* TxHashMap.set(txMap, "e", Option.none())
941
942 // Compact the map
943 const compacted = yield* TxHashMap.compact(txMap)
944 const compactedSize = yield* TxHashMap.size(compacted)
945 assert.strictEqual(compactedSize, 3) // a, c, d
946
947 // Use forEach for side effects
948 const sum = { value: 0 }
949 yield* TxHashMap.forEach(compacted, (value) =>
950 Effect.sync(() => {
951 sum.value += value
952 }))
953 assert.strictEqual(sum.value, 8) // 1 + 3 + 4
954
955 // Test toValues and toEntries
956 const values = yield* TxHashMap.toValues(compacted)
957 const entries = yield* TxHashMap.toEntries(compacted)
958
959 assert.strictEqual(values.length, 3)
960 assert.strictEqual(entries.length, 3)
961 })))
962 })
963})