UNPKG

@electric-sql/d2ts

Version:

D2TS is a TypeScript implementation of Differential Dataflow.

475 lines (380 loc) 15.6 kB
import { describe, it, expect, vi } from 'vitest' import { DefaultMap, WeakRefMap, hash, WeakRefBasedFinalizationRegistry } from '../src/utils.js' describe('DefaultMap', () => { it('should return default value for missing keys', () => { const map = new DefaultMap(() => 0) expect(map.get('missing')).toBe(0) }) it('should store and retrieve values', () => { const map = new DefaultMap(() => 0) map.set('key', 42) expect(map.get('key')).toBe(42) }) it('should accept initial entries', () => { const map = new DefaultMap(() => 0, [['key', 1]]) expect(map.get('key')).toBe(1) }) it('should update values using the update method', () => { const map = new DefaultMap(() => 0) map.update('key', (value) => value + 1) expect(map.get('key')).toBe(1) map.update('key', (value) => value * 2) expect(map.get('key')).toBe(2) }) }) describe('WeakRefMap', () => { it('should return default value for missing keys', () => { const map = new WeakRefMap<string, object>() expect(map.get('missing')).toBeNull() }) it('should store and retrieve values', () => { const map = new WeakRefMap<string, object>() const obj = { test: 'value' } map.set('key', obj) expect(map.get('key')).toBe(obj) }) it('should accept initial entries', () => { const map = new WeakRefMap<string, object>() const obj = { test: 'value' } map.set('key', obj) expect(map.get('key')).toBe(obj) }) // it('should cleanup dereferenced objects after garbage collection', async () => { // const map = new WeakRefMap<string, object>() // // Create object in a scope that will end // { // const obj = { test: 'value' } // map.set('key', obj) // expect(map.get('key')).toBe(obj) // } // // Force garbage collection if possible // // Note: This is environment-specific and may not work everywhere // // with node use --expose-gc to enable the `global.gc()` method // if (global.gc) { // // Run GC multiple times to ensure cleanup // for (let i = 0; i < 3; i++) { // global.gc() // // Give finalizers a chance to run // await new Promise(resolve => setTimeout(resolve, 0)) // } // // Object should be gone now // expect(map.get('key')).toBeNull() // } else { // console.warn('Test skipped: garbage collection not exposed. Run Node.js with --expose-gc flag.') // } // }) }) describe('hash', () => { describe('primitive types', () => { it('should hash null', () => { const result = hash(null) expect(typeof result).toBe('number') }) it('should hash undefined', () => { const result = hash(undefined) expect(typeof result).toBe('number') }) it('should hash strings', () => { const result1 = hash('hello') const result2 = hash('') const result3 = hash('test with spaces') const result4 = hash('special\nchars\t"') expect(typeof result1).toBe('number') expect(typeof result2).toBe('number') expect(typeof result3).toBe('number') expect(typeof result4).toBe('number') // Same strings should have same hash expect(hash('hello')).toBe(result1) }) it('should hash numbers', () => { const result1 = hash(42) const result2 = hash(0) const result3 = hash(-1) const result4 = hash(3.14159) const result5 = hash(Infinity) const result6 = hash(-Infinity) const result7 = hash(NaN) expect(typeof result1).toBe('number') expect(typeof result2).toBe('number') expect(typeof result3).toBe('number') expect(typeof result4).toBe('number') expect(typeof result5).toBe('number') expect(typeof result6).toBe('number') expect(typeof result7).toBe('number') // Same numbers should have same hash expect(hash(42)).toBe(result1) }) it('should hash booleans', () => { const result1 = hash(true) const result2 = hash(false) expect(typeof result1).toBe('number') expect(typeof result2).toBe('number') expect(result1).not.toBe(result2) // Same booleans should have same hash expect(hash(true)).toBe(result1) expect(hash(false)).toBe(result2) }) it('should hash bigint', () => { const result1 = hash(123n) const result2 = hash(456n) const result3 = hash(123n) expect(typeof result1).toBe('number') expect(typeof result2).toBe('number') expect(typeof result3).toBe('number') expect(result1).toBe(result3) // Same bigint should have same hash expect(result1).not.toBe(result2) // Different bigints should have different hash }) it('should hash symbols', () => { const sym1 = Symbol('test') const sym2 = Symbol('test') const sym3 = Symbol('different') const result1 = hash(sym1) const result2 = hash(sym2) const result3 = hash(sym3) expect(typeof result1).toBe('number') expect(typeof result2).toBe('number') expect(typeof result3).toBe('number') // Note: Different symbol instances with same description have same string representation expect(result1).toBe(result2) expect(result1).not.toBe(result3) }) }) describe('object types', () => { it('should hash plain objects', () => { const obj1 = { a: 1, b: 2 } const obj2 = { b: 2, a: 1 } // Different key order const hash1 = hash(obj1) const hash2 = hash(obj2) expect(typeof hash1).toBe('number') expect(typeof hash2).toBe('number') // Note: Different key orders might produce different hashes depending on JSON.stringify behavior }) it('should hash arrays', () => { const arr1 = [1, 2, 3] const arr2 = [1, 2, 3] const arr3 = [3, 2, 1] const hash1 = hash(arr1) const hash2 = hash(arr2) const hash3 = hash(arr3) expect(typeof hash1).toBe('number') expect(hash1).toBe(hash2) // Same content should have same hash expect(hash1).not.toBe(hash3) // Different content should have different hash }) it('should hash Date objects', () => { const date1 = new Date('2023-01-01') const date2 = new Date('2023-01-01') const date3 = new Date('2023-01-02') const hash1 = hash(date1) const hash2 = hash(date2) const hash3 = hash(date3) expect(typeof hash1).toBe('number') expect(hash1).toBe(hash2) // Same date should have same hash expect(hash1).not.toBe(hash3) // Different dates should have different hash }) it('should hash RegExp objects', () => { const regex1 = /test/g const regex2 = /test/g const regex3 = /different/i const hash1 = hash(regex1) const hash2 = hash(regex2) const hash3 = hash(regex3) expect(typeof hash1).toBe('number') expect(hash1).toBe(hash2) // Same regex should have same hash // Note: RegExp objects serialize to empty objects {}, so they all produce the same hash expect(hash1).toBe(hash3) // All RegExp objects have the same hash }) it('should hash nested objects', () => { const nested1 = { a: { b: { c: 1 } } } const nested2 = { a: { b: { c: 1 } } } const nested3 = { a: { b: { c: 2 } } } const hash1 = hash(nested1) const hash2 = hash(nested2) const hash3 = hash(nested3) expect(typeof hash1).toBe('number') expect(hash1).toBe(hash2) expect(hash1).not.toBe(hash3) }) it('should hash functions', () => { const func1 = function test() { return 1 } const func2 = function test() { return 1 } const func3 = function different() { return 2 } const hash1 = hash(func1) const hash2 = hash(func2) const hash3 = hash(func3) expect(typeof hash1).toBe('number') expect(typeof hash2).toBe('number') expect(typeof hash3).toBe('number') expect(hash1).toBe(hash2) // Same function definition should have same hash expect(hash1).not.toBe(hash3) // Different function should have different hash }) it('should hash Set objects', () => { const set1 = new Set([1, 2, 3]) const set2 = new Set([1, 2, 3]) const set3 = new Set([1, 2, 3, 4]) const hash1 = hash(set1) const hash2 = hash(set2) const hash3 = hash(set3) expect(typeof hash1).toBe('number') expect(hash1).toBe(hash2) // Same content should have same hash expect(hash1).not.toBe(hash3) // Different content should have different hash }) it('should hash Map objects', () => { const map1 = new Map([['a', 1], ['b', 2]]) const map2 = new Map([['a', 1], ['b', 2]]) const map3 = new Map([['a', 1], ['b', 2], ['c', 3]]) const hash1 = hash(map1) const hash2 = hash(map2) const hash3 = hash(map3) expect(typeof hash1).toBe('number') expect(hash1).toBe(hash2) // Same content should have same hash expect(hash1).not.toBe(hash3) // Different content should have different hash }) it('should hash Maps and Sets with unsupported types', () => { // Map with BigInt values const mapWithBigInt1 = new Map([['a', 123n], ['b', 456n]]) const mapWithBigInt2 = new Map([['a', 123n], ['b', 456n]]) const mapWithBigInt3 = new Map([['a', 123n], ['b', 789n]]) const hash1 = hash(mapWithBigInt1) const hash2 = hash(mapWithBigInt2) const hash3 = hash(mapWithBigInt3) expect(typeof hash1).toBe('number') expect(hash1).toBe(hash2) // Same BigInt content should have same hash expect(hash1).not.toBe(hash3) // Different BigInt content should have different hash // Set with Symbol values const sym1 = Symbol('test') const sym2 = Symbol('different') const setWithSymbols1 = new Set([sym1, sym2]) const setWithSymbols2 = new Set([sym1, sym2]) const setWithSymbols3 = new Set([sym1]) const hash4 = hash(setWithSymbols1) const hash5 = hash(setWithSymbols2) const hash6 = hash(setWithSymbols3) expect(typeof hash4).toBe('number') expect(hash4).toBe(hash5) // Same Symbol content should have same hash expect(hash4).not.toBe(hash6) // Different Symbol content should have different hash }) }) describe('caching behavior', () => { it('should cache hash values for objects', () => { const obj = { test: 'value' } const hash1 = hash(obj) const hash2 = hash(obj) expect(hash1).toBe(hash2) expect(typeof hash1).toBe('number') }) it('should return cached values on subsequent calls', () => { const obj = { complex: { nested: { data: [1, 2, 3] } } } // First call should compute and cache const hash1 = hash(obj) // Second call should return cached value const hash2 = hash(obj) expect(hash1).toBe(hash2) expect(typeof hash1).toBe('number') }) it('should not cache primitive values', () => { // Primitives should not be cached as they use JSON.stringify directly const hash1 = hash('test') const hash2 = hash('test') expect(hash1).toBe(hash2) expect(typeof hash1).toBe('number') }) }) describe('edge cases', () => { it('should handle empty objects and arrays', () => { expect(typeof hash({})).toBe('number') expect(typeof hash([])).toBe('number') expect(hash({})).not.toBe(hash([])) }) it('should handle objects with null and undefined values', () => { const obj1 = { a: null, b: undefined } const obj2 = { a: null, b: undefined } const hash1 = hash(obj1) const hash2 = hash(obj2) expect(hash1).toBe(hash2) expect(typeof hash1).toBe('number') }) it('should handle mixed type arrays', () => { const mixedArray = [1, 'string', true, null, { key: 'value' }] const sameArray = [1, 'string', true, null, { key: 'value' }] const hash1 = hash(mixedArray) const hash2 = hash(sameArray) expect(hash1).toBe(hash2) expect(typeof hash1).toBe('number') }) it('should produce consistent hashes for same content', () => { const obj = { string: 'test', number: 42, boolean: true, array: [1, 2, 3], nested: { inner: 'value' } } // Multiple calls should return the same hash const hashes = Array.from({ length: 5 }, () => hash(obj)) const firstHash = hashes[0] expect(hashes.every(h => h === firstHash)).toBe(true) expect(typeof firstHash).toBe('number') }) }) }) describe('WeakRefBasedFinalizationRegistry', () => { it('should register and unregister objects', () => { const finalizeSpy = vi.fn() const registry = new WeakRefBasedFinalizationRegistry(finalizeSpy) const target = { test: 'value' } const token = { token: 'value' } const value = 'test value' registry.register(target, value, token) registry.unregister(token) // The finalize callback should not have been called since we unregistered expect(finalizeSpy).not.toHaveBeenCalled() }) // TODO: find a way to make this actually work... // it('should call finalize when target is garbage collected', async () => { // const finalizeSpy = vi.fn() // const registry = new WeakRefBasedFinalizationRegistry(finalizeSpy, 100) // Use 100ms interval for testing // // Create object in a scope that will end // { // const target = { test: 'value' } // const value = 'test value' // registry.register(target, value, target) // } // // Force garbage collection if possible // if (global.gc) { // // Run GC multiple times to ensure cleanup // for (let i = 0; i < 3; i++) { // global.gc() // // Give finalizers a chance to run // await new Promise(resolve => setTimeout(resolve, 0)) // } // // Wait for the sweep interval (plus a small buffer) // await new Promise(resolve => setTimeout(resolve, 200)) // // The finalize callback should have been called // expect(finalizeSpy).toHaveBeenCalledWith('test value') // } else { // console.warn('Test skipped: garbage collection not exposed. Run Node.js with --expose-gc flag.') // } // }) it('should handle multiple registrations', () => { const finalizeSpy = vi.fn() const registry = new WeakRefBasedFinalizationRegistry(finalizeSpy) const target1 = { test: 'value1' } const target2 = { test: 'value2' } const token1 = { token: 'value1' } const token2 = { token: 'value2' } registry.register(target1, 'value1', token1) registry.register(target2, 'value2', token2) // Unregister one token registry.unregister(token1) // The finalize callback should not have been called expect(finalizeSpy).not.toHaveBeenCalled() }) it('should handle null token in unregister', () => { const finalizeSpy = vi.fn() const registry = new WeakRefBasedFinalizationRegistry(finalizeSpy) const target = { test: 'value' } registry.register(target, 'value', null) // Should not throw when unregistering with null token expect(() => registry.unregister(null)).not.toThrow() }) })