import * as RA from "../../Array.js" import * as Chunk from "../../Chunk.js" import * as Equal from "../../Equal.js" import type { LazyArg } from "../../Function.js" import { dual, pipe } from "../../Function.js" import * as Hash from "../../Hash.js" import * as HashMap from "../../HashMap.js" import * as Option from "../../Option.js" import { hasProperty } from "../../Predicate.js" import * as STM from "../../STM.js" import type * as TArray from "../../TArray.js" import type * as TMap from "../../TMap.js" import type * as TRef from "../../TRef.js" import * as core from "./core.js" import type * as Journal from "./journal.js" import * as stm from "./stm.js" import * as tArray from "./tArray.js" import * as tRef from "./tRef.js" /** @internal */ const TMapSymbolKey = "effect/TMap" /** @internal */ export const TMapTypeId: TMap.TMapTypeId = Symbol.for( TMapSymbolKey ) as TMap.TMapTypeId const tMapVariance = { /* c8 ignore next */ _K: (_: any) => _, /* c8 ignore next */ _V: (_: any) => _ } /** @internal */ class TMapImpl implements TMap.TMap { readonly [TMapTypeId] = tMapVariance constructor( readonly tBuckets: TRef.TRef>>, readonly tSize: TRef.TRef ) {} } const isTMap = (u: unknown) => hasProperty(u, TMapTypeId) /** @internal */ const InitialCapacity = 16 const LoadFactor = 0.75 /** @internal */ const nextPowerOfTwo = (size: number): number => { const n = -1 >>> Math.clz32(size - 1) return n < 0 ? 1 : n + 1 } /** @internal */ const hash = (key: K): number => { const h = Hash.hash(key) return h ^ (h >>> 16) } /** @internal */ const indexOf = (k: K, capacity: number): number => hash(k) & (capacity - 1) /** @internal */ const allocate = ( capacity: number, data: Chunk.Chunk ): STM.STM> => { const buckets = Array.from({ length: capacity }, () => Chunk.empty()) const distinct = new Map(data) let size = 0 for (const entry of distinct) { const index = indexOf(entry[0], capacity) buckets[index] = pipe(buckets[index], Chunk.prepend(entry)) size = size + 1 } return pipe( tArray.fromIterable(buckets), core.flatMap((buckets) => pipe( tRef.make(buckets), core.flatMap((tBuckets) => pipe( tRef.make(size), core.map((tSize) => new TMapImpl(tBuckets, tSize)) ) ) ) ) ) } /** @internal */ export const empty = (): STM.STM> => fromIterable([]) /** @internal */ export const find = dual< ( pf: (key: K, value: V) => Option.Option ) => (self: TMap.TMap) => STM.STM>, ( self: TMap.TMap, pf: (key: K, value: V) => Option.Option ) => STM.STM> >(2, (self, pf) => findSTM(self, (key, value) => { const option = pf(key, value) if (Option.isSome(option)) { return core.succeed(option.value) } return core.fail(Option.none()) })) /** @internal */ export const findSTM = dual< ( f: (key: K, value: V) => STM.STM, R> ) => (self: TMap.TMap) => STM.STM, E, R>, ( self: TMap.TMap, f: (key: K, value: V) => STM.STM, R> ) => STM.STM, E, R> >(2, ( self: TMap.TMap, f: (key: K, value: V) => STM.STM, R> ) => reduceSTM(self, Option.none(), (acc, value, key) => Option.isNone(acc) ? core.matchSTM(f(key, value), { onFailure: Option.match({ onNone: () => stm.succeedNone, onSome: core.fail }), onSuccess: stm.succeedSome }) : STM.succeed(acc))) /** @internal */ export const findAll = dual< ( pf: (key: K, value: V) => Option.Option ) => (self: TMap.TMap) => STM.STM>, ( self: TMap.TMap, pf: (key: K, value: V) => Option.Option ) => STM.STM> >(2, (self, pf) => findAllSTM(self, (key, value) => { const option = pf(key, value) if (Option.isSome(option)) { return core.succeed(option.value) } return core.fail(Option.none()) })) /** @internal */ export const findAllSTM = dual< ( pf: (key: K, value: V) => STM.STM, R> ) => (self: TMap.TMap) => STM.STM, E, R>, ( self: TMap.TMap, pf: (key: K, value: V) => STM.STM, R> ) => STM.STM, E, R> >(2, ( self: TMap.TMap, pf: (key: K, value: V) => STM.STM, R> ) => core.map( reduceSTM(self, Chunk.empty(), (acc, value, key) => core.matchSTM(pf(key, value), { onFailure: Option.match({ onNone: () => core.succeed(acc), onSome: core.fail }), onSuccess: (a) => core.succeed(Chunk.append(acc, a)) })), (a) => Array.from(a) )) /** @internal */ export const forEach = dual< (f: (key: K, value: V) => STM.STM) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, f: (key: K, value: V) => STM.STM) => STM.STM >(2, (self, f) => reduceSTM( self, void 0 as void, (_, value, key) => stm.asVoid(f(key, value)) )) /** @internal */ export const fromIterable = (iterable: Iterable): STM.STM> => stm.suspend(() => { const data = Chunk.fromIterable(iterable) const capacity = data.length < InitialCapacity ? InitialCapacity : nextPowerOfTwo(data.length) return allocate(capacity, data) }) /** @internal */ export const get = dual< (key: K) => (self: TMap.TMap) => STM.STM>, (self: TMap.TMap, key: K) => STM.STM> >(2, (self: TMap.TMap, key: K) => core.effect>((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) const index = indexOf(key, buckets.chunk.length) const bucket = tRef.unsafeGet(buckets.chunk[index], journal) return pipe( Chunk.findFirst(bucket, (entry) => Equal.equals(entry[0])(key)), Option.map((entry) => entry[1]) ) })) /** @internal */ export const getOrElse = dual< (key: K, fallback: LazyArg) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, key: K, fallback: LazyArg) => STM.STM >(3, (self, key, fallback) => core.map( get(self, key), Option.getOrElse(fallback) )) /** @internal */ export const has = dual< (key: K) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, key: K) => STM.STM >(2, (self, key) => core.map(get(self, key), Option.isSome)) /** @internal */ export const isEmpty = (self: TMap.TMap): STM.STM => core.map(tRef.get(self.tSize), (size) => size === 0) /** @internal */ export const keys = (self: TMap.TMap): STM.STM> => core.map(toReadonlyArray(self), RA.map((entry) => entry[0])) /** @internal */ export const make = (...entries: Array): STM.STM> => fromIterable(entries) /** @internal */ export const merge = dual< (key: K, value: V, f: (x: V, y: V) => V) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, key: K, value: V, f: (x: V, y: V) => V) => STM.STM >(4, (self, key, value, f) => core.flatMap( get(self, key), Option.match({ onNone: () => stm.as(set(self, key, value), value), onSome: (v0) => { const v1 = f(v0, value) return stm.as(set(self, key, v1), v1) } }) )) /** @internal */ export const reduce = dual< (zero: Z, f: (acc: Z, value: V, key: K) => Z) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, zero: Z, f: (acc: Z, value: V, key: K) => Z) => STM.STM >( 3, (self: TMap.TMap, zero: Z, f: (acc: Z, value: V, key: K) => Z) => core.effect((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) let result = zero let index = 0 while (index < buckets.chunk.length) { const bucket = buckets.chunk[index] const items = tRef.unsafeGet(bucket, journal) result = Chunk.reduce(items, result, (acc, entry) => f(acc, entry[1], entry[0])) index = index + 1 } return result }) ) /** @internal */ export const reduceSTM = dual< ( zero: Z, f: (acc: Z, value: V, key: K) => STM.STM ) => (self: TMap.TMap) => STM.STM, ( self: TMap.TMap, zero: Z, f: (acc: Z, value: V, key: K) => STM.STM ) => STM.STM >(3, (self, zero, f) => core.flatMap( toReadonlyArray(self), stm.reduce(zero, (acc, entry) => f(acc, entry[1], entry[0])) )) /** @internal */ export const remove = dual< (key: K) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, key: K) => STM.STM >(2, (self, key) => core.effect((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) const index = indexOf(key, buckets.chunk.length) const bucket = tRef.unsafeGet(buckets.chunk[index], journal) const [toRemove, toRetain] = Chunk.partition(bucket, (entry) => Equal.equals(entry[1], key)) if (Chunk.isNonEmpty(toRemove)) { const currentSize = tRef.unsafeGet(self.tSize, journal) tRef.unsafeSet(buckets.chunk[index], toRetain, journal) tRef.unsafeSet(self.tSize, currentSize - 1, journal) } })) /** @internal */ export const removeAll = dual< (keys: Iterable) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, keys: Iterable) => STM.STM >(2, (self: TMap.TMap, keys: Iterable) => core.effect((journal) => { const iterator = keys[Symbol.iterator]() let next: IteratorResult while ((next = iterator.next()) && !next.done) { const buckets = tRef.unsafeGet(self.tBuckets, journal) const index = indexOf(next.value, buckets.chunk.length) const bucket = tRef.unsafeGet(buckets.chunk[index], journal) const [toRemove, toRetain] = Chunk.partition(bucket, (entry) => Equal.equals(next.value)(entry[0])) if (Chunk.isNonEmpty(toRemove)) { const currentSize = tRef.unsafeGet(self.tSize, journal) tRef.unsafeSet(buckets.chunk[index], toRetain, journal) tRef.unsafeSet(self.tSize, currentSize - 1, journal) } } })) /** @internal */ export const removeIf: { ( predicate: (key: K, value: V) => boolean, options: { readonly discard: true } ): (self: TMap.TMap) => STM.STM ( predicate: (key: K, value: V) => boolean, options?: { readonly discard: false } ): (self: TMap.TMap) => STM.STM> ( self: TMap.TMap, predicate: (key: K, value: V) => boolean, options: { readonly discard: true } ): STM.STM ( self: TMap.TMap, predicate: (key: K, value: V) => boolean, options?: { readonly discard: false } ): STM.STM> } = dual((args) => isTMap(args[0]), ( self: TMap.TMap, predicate: (key: K, value: V) => boolean, options?: { readonly discard: boolean } ) => core.effect((journal) => { const discard = options?.discard === true const buckets = tRef.unsafeGet(self.tBuckets, journal) const capacity = buckets.chunk.length const removed: Array<[K, V]> = [] let index = 0 let newSize = 0 while (index < capacity) { const bucket = tRef.unsafeGet(buckets.chunk[index], journal) const iterator = bucket[Symbol.iterator]() let next: IteratorResult let newBucket = Chunk.empty() while ((next = iterator.next()) && !next.done) { const [k, v] = next.value if (!predicate(k, v)) { newBucket = Chunk.prepend(newBucket, next.value) newSize = newSize + 1 } else { if (!discard) { removed.push([k, v]) } } } tRef.unsafeSet(buckets.chunk[index], newBucket, journal) index = index + 1 } tRef.unsafeSet(self.tSize, newSize, journal) if (!discard) { return removed } })) /** @internal */ export const retainIf: { ( predicate: (key: K, value: V) => boolean, options: { readonly discard: true } ): (self: TMap.TMap) => STM.STM ( predicate: (key: K, value: V) => boolean, options?: { readonly discard: false } ): (self: TMap.TMap) => STM.STM> ( self: TMap.TMap, predicate: (key: K, value: V) => boolean, options: { readonly discard: true } ): STM.STM ( self: TMap.TMap, predicate: (key: K, value: V) => boolean, options?: { readonly discard: false } ): STM.STM> } = dual( (args) => isTMap(args[0]), (self, predicate, options) => removeIf(self, (key, value) => !predicate(key, value), options) ) /** @internal */ export const set = dual< (key: K, value: V) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, key: K, value: V) => STM.STM >(3, (self: TMap.TMap, key: K, value: V) => { const resize = (journal: Journal.Journal, buckets: TArray.TArray>): void => { const capacity = buckets.chunk.length const newCapacity = capacity << 1 const newBuckets = Array.from({ length: newCapacity }, () => Chunk.empty()) let index = 0 while (index < capacity) { const pairs = tRef.unsafeGet(buckets.chunk[index], journal) const iterator = pairs[Symbol.iterator]() let next: IteratorResult while ((next = iterator.next()) && !next.done) { const newIndex = indexOf(next.value[0], newCapacity) newBuckets[newIndex] = Chunk.prepend(newBuckets[newIndex], next.value) } index = index + 1 } // insert new pair const newIndex = indexOf(key, newCapacity) newBuckets[newIndex] = Chunk.prepend(newBuckets[newIndex], [key, value] as const) const newArray: Array>> = [] index = 0 while (index < newCapacity) { newArray[index] = new tRef.TRefImpl(newBuckets[index]) index = index + 1 } const newTArray: TArray.TArray> = new tArray.TArrayImpl(newArray) tRef.unsafeSet(self.tBuckets, newTArray, journal) } return core.effect((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) const capacity = buckets.chunk.length const index = indexOf(key, capacity) const bucket = tRef.unsafeGet(buckets.chunk[index], journal) const shouldUpdate = Chunk.some(bucket, (entry) => Equal.equals(key)(entry[0])) if (shouldUpdate) { const newBucket = Chunk.map(bucket, (entry) => Equal.equals(key)(entry[0]) ? [key, value] as const : entry) tRef.unsafeSet(buckets.chunk[index], newBucket, journal) } else { const newSize = tRef.unsafeGet(self.tSize, journal) + 1 tRef.unsafeSet(self.tSize, newSize, journal) if (capacity * LoadFactor < newSize) { resize(journal, buckets) } else { const newBucket = Chunk.prepend(bucket, [key, value] as const) tRef.unsafeSet(buckets.chunk[index], newBucket, journal) } } }) }) /** @internal */ export const setIfAbsent = dual< (key: K, value: V) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, key: K, value: V) => STM.STM >(3, (self, key, value) => core.flatMap( get(self, key), Option.match({ onNone: () => set(self, key, value), onSome: () => stm.void }) )) /** @internal */ export const size = (self: TMap.TMap): STM.STM => tRef.get(self.tSize) /** @internal */ export const takeFirst = dual< (pf: (key: K, value: V) => Option.Option) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, pf: (key: K, value: V) => Option.Option) => STM.STM >(2, (self: TMap.TMap, pf: (key: K, value: V) => Option.Option) => pipe( core.effect>((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) const capacity = buckets.chunk.length const size = tRef.unsafeGet(self.tSize, journal) let result: Option.Option = Option.none() let index = 0 while (index < capacity && Option.isNone(result)) { const bucket = tRef.unsafeGet(buckets.chunk[index], journal) const recreate = Chunk.some(bucket, (entry) => Option.isSome(pf(entry[0], entry[1]))) if (recreate) { const iterator = bucket[Symbol.iterator]() let newBucket = Chunk.empty() let next: IteratorResult while ((next = iterator.next()) && !next.done && Option.isNone(result)) { const option = pf(next.value[0], next.value[1]) if (Option.isSome(option) && Option.isNone(result)) { result = option } else { newBucket = Chunk.prepend(newBucket, next.value) } } tRef.unsafeSet(buckets.chunk[index], newBucket, journal) } index = index + 1 } if (Option.isSome(result)) { tRef.unsafeSet(self.tSize, size - 1, journal) } return result }), stm.collect((option) => Option.isSome(option) ? Option.some(option.value) : Option.none() ) )) /** @internal */ export const takeFirstSTM = dual< ( pf: (key: K, value: V) => STM.STM, R> ) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, pf: (key: K, value: V) => STM.STM, R>) => STM.STM >(2, (self, pf) => pipe( findSTM(self, (key, value) => core.map(pf(key, value), (a) => [key, a] as const)), stm.collect((option) => Option.isSome(option) ? Option.some(option.value) : Option.none()), core.flatMap((entry) => stm.as(remove(self, entry[0]), entry[1])) )) /** @internal */ export const takeSome = dual< ( pf: (key: K, value: V) => Option.Option ) => (self: TMap.TMap) => STM.STM>, ( self: TMap.TMap, pf: (key: K, value: V) => Option.Option ) => STM.STM> >(2, (self: TMap.TMap, pf: (key: K, value: V) => Option.Option) => pipe( core.effect>>((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) const capacity = buckets.chunk.length const builder: Array = [] let newSize = 0 let index = 0 while (index < capacity) { const bucket = tRef.unsafeGet(buckets.chunk[index], journal) const recreate = Chunk.some(bucket, (entry) => Option.isSome(pf(entry[0], entry[1]))) if (recreate) { const iterator = bucket[Symbol.iterator]() let newBucket = Chunk.empty() let next: IteratorResult while ((next = iterator.next()) && !next.done) { const option = pf(next.value[0], next.value[1]) if (Option.isSome(option)) { builder.push(option.value) } else { newBucket = Chunk.prepend(newBucket, next.value) newSize = newSize + 1 } } tRef.unsafeSet(buckets.chunk[index], newBucket, journal) } else { newSize = newSize + bucket.length } index = index + 1 } tRef.unsafeSet(self.tSize, newSize, journal) if (builder.length > 0) { return Option.some(builder as RA.NonEmptyArray) } return Option.none() }), stm.collect((option) => Option.isSome(option) ? Option.some(option.value) : Option.none>() ) )) /** @internal */ export const takeSomeSTM = dual< ( pf: (key: K, value: V) => STM.STM, R> ) => (self: TMap.TMap) => STM.STM, E, R>, ( self: TMap.TMap, pf: (key: K, value: V) => STM.STM, R> ) => STM.STM, E, R> >(2, ( self: TMap.TMap, pf: (key: K, value: V) => STM.STM, R> ) => pipe( findAllSTM( self, (key, value) => core.map(pf(key, value), (a) => [key, a] as const) ), core.map((chunk) => RA.isNonEmptyArray(chunk) ? Option.some(chunk) : Option.none() ), stm.collect((option) => Option.isSome(option) ? Option.some(option.value) : Option.none() ), core.flatMap((entries) => stm.as( removeAll(self, entries.map((entry) => entry[0])), RA.map(entries, (entry) => entry[1]) as RA.NonEmptyArray ) ) )) const toReadonlyArray = (self: TMap.TMap): STM.STM> => core.effect>((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) const capacity = buckets.chunk.length const builder: Array = [] let index = 0 while (index < capacity) { const bucket = buckets.chunk[index] for (const entry of tRef.unsafeGet(bucket, journal)) { builder.push(entry) } index = index + 1 } return builder }) /** @internal */ export const toChunk = (self: TMap.TMap): STM.STM> => reduce( self, Chunk.empty<[K, V]>(), (acc, value, key) => Chunk.append(acc, [key, value]) ) /** @internal */ export const toHashMap = (self: TMap.TMap): STM.STM> => reduce( self, HashMap.empty(), (acc, value, key) => pipe(acc, HashMap.set(key, value)) ) /** @internal */ export const toArray = (self: TMap.TMap): STM.STM> => reduce( self, [] as Array<[K, V]>, (acc, value, key) => { acc.unshift([key, value]) return acc } ) /** @internal */ export const toMap = (self: TMap.TMap): STM.STM> => reduce( self, new Map(), (acc, value, key) => acc.set(key, value) ) /** @internal */ export const transform = dual< (f: (key: K, value: V) => readonly [K, V]) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, f: (key: K, value: V) => readonly [K, V]) => STM.STM >( 2, (self: TMap.TMap, f: (key: K, value: V) => readonly [K, V]) => core.effect((journal) => { const buckets = pipe(self.tBuckets, tRef.unsafeGet(journal)) const capacity = buckets.chunk.length const newBuckets = Array.from({ length: capacity }, () => Chunk.empty()) let newSize = 0 let index = 0 while (index < capacity) { const bucket = buckets.chunk[index] const pairs = tRef.unsafeGet(bucket, journal) const iterator = pairs[Symbol.iterator]() let next: IteratorResult while ((next = iterator.next()) && !next.done) { const newPair = f(next.value[0], next.value[1]) const index = indexOf(newPair[0], capacity) const newBucket = newBuckets[index] if (!Chunk.some(newBucket, (entry) => Equal.equals(entry[0], newPair[0]))) { newBuckets[index] = Chunk.prepend(newBucket, newPair) newSize = newSize + 1 } } index = index + 1 } index = 0 while (index < capacity) { tRef.unsafeSet(buckets.chunk[index], newBuckets[index], journal) index = index + 1 } tRef.unsafeSet(self.tSize, newSize, journal) }) ) /** @internal */ export const transformSTM = dual< ( f: (key: K, value: V) => STM.STM ) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, f: (key: K, value: V) => STM.STM) => STM.STM >( 2, (self: TMap.TMap, f: (key: K, value: V) => STM.STM) => pipe( core.flatMap( toReadonlyArray(self), stm.forEach((entry) => f(entry[0], entry[1])) ), core.flatMap((newData) => core.effect((journal) => { const buckets = tRef.unsafeGet(self.tBuckets, journal) const capacity = buckets.chunk.length const newBuckets = Array.from({ length: capacity }, () => Chunk.empty()) const iterator = newData[Symbol.iterator]() let newSize = 0 let next: IteratorResult while ((next = iterator.next()) && !next.done) { const index = indexOf(next.value[0], capacity) const newBucket = newBuckets[index] if (!Chunk.some(newBucket, (entry) => Equal.equals(entry[0])(next.value[0]))) { newBuckets[index] = Chunk.prepend(newBucket, next.value) newSize = newSize + 1 } } let index = 0 while (index < capacity) { tRef.unsafeSet(buckets.chunk[index], newBuckets[index], journal) index = index + 1 } tRef.unsafeSet(self.tSize, newSize, journal) }) ) ) ) /** @internal */ export const transformValues = dual< (f: (value: V) => V) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, f: (value: V) => V) => STM.STM >(2, (self, f) => transform(self, (key, value) => [key, f(value)])) /** @internal */ export const transformValuesSTM = dual< (f: (value: V) => STM.STM) => (self: TMap.TMap) => STM.STM, (self: TMap.TMap, f: (value: V) => STM.STM) => STM.STM >(2, (self, f) => transformSTM( self, (key, value) => core.map(f(value), (value) => [key, value]) )) /** @internal */ export const updateWith = dual< ( key: K, f: (value: Option.Option) => Option.Option ) => (self: TMap.TMap) => STM.STM>, ( self: TMap.TMap, key: K, f: (value: Option.Option) => Option.Option ) => STM.STM> >(3, (self, key, f) => core.flatMap(get(self, key), (option) => Option.match( f(option), { onNone: () => stm.as(remove(self, key), Option.none()), onSome: (value) => stm.as(set(self, key, value), Option.some(value)) } ))) /** @internal */ export const values = (self: TMap.TMap): STM.STM> => core.map(toReadonlyArray(self), RA.map((entry) => entry[1]))