@electric-sql/d2ts
Version:
D2TS is a TypeScript implementation of Differential Dataflow.
184 lines (162 loc) • 5.25 kB
text/typescript
import murmurhash from 'murmurhash-js'
/**
* A map that uses WeakRefs to store objects, and automatically removes them when
* they are no longer referenced.
*/
export class WeakRefMap<K, V extends object> {
private cacheMap = new Map<K, WeakRef<V>>()
private finalizer = new AnyFinalizationRegistry((key: K) => {
this.cacheMap.delete(key)
})
set(key: K, value: V): void {
const cache = this.get(key)
if (cache) {
if (cache === value) return
this.finalizer.unregister(cache)
}
this.cacheMap.set(key, new WeakRef(value))
this.finalizer.register(value, key, value)
}
get(key: K): V | null {
return this.cacheMap.get(key)?.deref() ?? null
}
}
/**
* A map that returns a default value for keys that are not present.
*/
export class DefaultMap<K, V> extends Map<K, V> {
constructor(
private defaultValue: () => V,
entries?: Iterable<[K, V]>,
) {
super(entries)
}
get(key: K): V {
if (!this.has(key)) {
this.set(key, this.defaultValue())
}
return super.get(key)!
}
/**
* Update the value for a key using a function.
*/
update(key: K, updater: (value: V) => V): V {
const value = this.get(key)
const newValue = updater(value)
this.set(key, newValue)
return newValue
}
}
// JS engines have various limits on how many args can be passed to a function
// with a spread operator, so we need to split the operation into chunks
// 32767 is the max for Chrome 14, all others are higher
// TODO: investigate the performance of this and other approaches
const chunkSize = 30000
export function chunkedArrayPush(array: unknown[], other: unknown[]) {
if (other.length <= chunkSize) {
array.push(...other)
} else {
for (let i = 0; i < other.length; i += chunkSize) {
const chunk = other.slice(i, i + chunkSize)
array.push(...chunk)
}
}
}
const hashCache = new WeakMap()
/**
* Replacer function for JSON.stringify that converts unsupported types to strings
*/
function hashReplacer(_key: string, value: any): any {
if (typeof value === 'bigint') {
return String(value)
} else if (typeof value === 'symbol') {
return String(value)
} else if (typeof value === 'function') {
return String(value)
} else if (value === undefined) {
return 'undefined'
} else if (value instanceof Map) {
return `Map(${JSON.stringify(Array.from(value.entries()), hashReplacer)})`
} else if (value instanceof Set) {
return `Set(${JSON.stringify(Array.from(value.values()), hashReplacer)})`
}
return value
}
/**
* A hash method that caches the hash of a value in a week map
*/
export function hash(data: any): string | number {
if (
data === null ||
data === undefined ||
(typeof data !== 'object' && typeof data !== 'function')
) {
// Can't be cached in the weak map because it's not an object
const serialized = JSON.stringify(data, hashReplacer)
return murmurhash.murmur3(serialized)
}
if (hashCache.has(data)) {
return hashCache.get(data)
}
const serialized = JSON.stringify(data, hashReplacer)
const hashValue = murmurhash.murmur3(JSON.stringify(serialized))
hashCache.set(data, hashValue)
return hashValue
}
/**
* This is a mock implementation of FinalizationRegistry which uses WeakRef to
* track the target objects. It's used in environments where FinalizationRegistry
* is not available but WeakRef is (e.g. React Native >=0.75 on New Architecture).
* Based on https://gist.github.com/cray0000/abecb1ca71fd28a1d8efff2be9e0f6c5
* MIT License - Copyright Cray0000
*/
export class WeakRefBasedFinalizationRegistry {
private counter = 0
private registrations = new Map()
private sweepTimeout: NodeJS.Timeout | undefined
private finalize: (value: any) => void
private sweepIntervalMs = 10_000
constructor(finalize: (value: any) => void, sweepIntervalMs?: number) {
this.finalize = finalize
if (sweepIntervalMs !== undefined) {
this.sweepIntervalMs = sweepIntervalMs
}
}
register(target: any, value: any, token: any) {
this.registrations.set(this.counter, {
targetRef: new WeakRef(target),
tokenRef: token != null ? new WeakRef(token) : undefined,
value,
})
this.counter++
this.scheduleSweep()
}
unregister(token: any) {
if (token == null) return
this.registrations.forEach((registration, key) => {
if (registration.tokenRef?.deref() === token) {
this.registrations.delete(key)
}
})
}
// Bound so it can be used directly as setTimeout callback.
private sweep = () => {
clearTimeout(this.sweepTimeout)
this.sweepTimeout = undefined
this.registrations.forEach((registration, key) => {
if (registration.targetRef.deref() !== undefined) return
const value = registration.value
this.registrations.delete(key)
this.finalize(value)
})
if (this.registrations.size > 0) this.scheduleSweep()
}
private scheduleSweep() {
if (this.sweepTimeout) return
this.sweepTimeout = setTimeout(this.sweep, this.sweepIntervalMs)
}
}
const AnyFinalizationRegistry =
typeof FinalizationRegistry !== 'undefined'
? FinalizationRegistry
: WeakRefBasedFinalizationRegistry