UNPKG

@electric-sql/d2ts

Version:

D2TS is a TypeScript implementation of Differential Dataflow.

922 lines (771 loc) 30.1 kB
import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { D2 } from '../../src/d2.js' import { MultiSet } from '../../src/multiset.js' import { MessageType } from '../../src/types.js' import { topKWithFractionalIndex as inMemoryTopKWithFractionalIndex } from '../../src/operators/topKWithFractionalIndex.js' import { topKWithFractionalIndex as sqliteTopKWithFractionalIndex } from '../../src/sqlite/operators/topKWithFractionalIndex.js' import { output } from '../../src/operators/index.js' import { BetterSQLite3Wrapper } from '../../src/sqlite/database.js' import Database from 'better-sqlite3' // Helper function to check if indices are in lexicographic order function checkLexicographicOrder(results: any[]) { // Extract values and their indices const valuesWithIndices = results.map(([[_, [value, index]]]) => ({ value, index, })) // Sort by value using the same comparator as in the test const sortedByValue = [...valuesWithIndices].sort((a, b) => a.value.value < b.value.value ? -1 : 1, ) // Check that indices are in the same order as the sorted values for (let i = 0; i < sortedByValue.length - 1; i++) { const currentIndex = sortedByValue[i].index const nextIndex = sortedByValue[i + 1].index // Indices should be in lexicographic order expect(currentIndex < nextIndex).toBe(true) } return true } // Helper function to verify the expected order of elements function verifyOrder(results: any[], expectedOrder: string[]) { // Extract values in the order they appear in the results const actualOrder = results.map(([[_, [value, __]]]) => value.value) // Sort both arrays to ensure consistent comparison const sortedActual = [...actualOrder].sort() const sortedExpected = [...expectedOrder].sort() // First check that we have the same elements expect(sortedActual).toEqual(sortedExpected) // Now check that the indices result in the correct order const valueToIndex = new Map() for (const [[_, [value, index]]] of results) { valueToIndex.set(value.value, index) } // Sort the values by their indices const sortedByIndex = [...valueToIndex.entries()] .sort((a, b) => (a[1] < b[1] ? -1 : 1)) .map(([value]) => value) // The order should match the expected order expect(sortedByIndex).toEqual(expectedOrder) } describe('Operators', () => { describe('TopKWithFractionalIndex operation', () => { testTopKWithFractionalIndex(inMemoryTopKWithFractionalIndex) }) }) describe('SQLite Operators', () => { describe('TopKWithFractionalIndex operation', () => { let db: BetterSQLite3Wrapper beforeEach(() => { const sqlite = new Database(':memory:') db = new BetterSQLite3Wrapper(sqlite) }) afterEach(() => { db.close() }) const wrappedTopK = ((stream, options) => { // @ts-ignore return sqliteTopKWithFractionalIndex(stream, { ...options, db: db, }) }) as typeof inMemoryTopKWithFractionalIndex testTopKWithFractionalIndex(wrappedTopK) }) }) function testTopKWithFractionalIndex( topKWithFractionalIndex: typeof inMemoryTopKWithFractionalIndex, ) { it('should assign fractional indices to sorted elements', () => { const graph = new D2({ initialFrontier: 0 }) const input = graph.newInput<[null, { id: number; value: string }]>() const allMessages: any[] = [] input.pipe( topKWithFractionalIndex((a, b) => a.value.localeCompare(b.value)), output((message) => { if (message.type === MessageType.DATA) { allMessages.push(message.data) } }), ) graph.finalize() // Initial data - a, b, c, d, e input.sendData( 0, new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'b' }], 1], [[null, { id: 3, value: 'c' }], 1], [[null, { id: 4, value: 'd' }], 1], [[null, { id: 5, value: 'e' }], 1], ]), ) input.sendFrontier(1) graph.run() // Initial result should have all elements with fractional indices const initialResult = allMessages[0].collection.getInner() expect(initialResult.length).toBe(5) // Check that indices are in lexicographic order expect(checkLexicographicOrder(initialResult)).toBe(true) // Store the initial indices for later comparison const initialIndices = new Map() for (const [[_, [value, index]]] of initialResult) { initialIndices.set(value.id, index) } // Now let's move 'c' to the beginning by changing its value input.sendData( 1, new MultiSet([ [[null, { id: 3, value: 'a-' }], 1], // This should now be first [[null, { id: 3, value: 'c' }], -1], // Remove the old value ]), ) input.sendFrontier(2) graph.run() // Check the changes const changes = allMessages[1].collection.getInner() // We should only emit as many changes as we received // We received 2 changes (1 addition, 1 removal) // We should emit at most 2 changes expect(changes.length).toBeLessThanOrEqual(2) expect(changes.length).toBe(2) // 1 removal + 1 addition // Find the removal and addition const removal = changes.find(([_, multiplicity]) => multiplicity < 0) const addition = changes.find(([_, multiplicity]) => multiplicity > 0) // Check that we removed 'c' and added 'a-' expect(removal?.[0][1][0].value).toBe('c') expect(addition?.[0][1][0].value).toBe('a-') // Check that the id is the same (id 3) expect(removal?.[0][1][0].id).toBe(3) expect(addition?.[0][1][0].id).toBe(3) // Get the new index const newIndex = addition?.[0][1][1] const oldIndex = removal?.[0][1][1] // The new index should be different from the old one expect(newIndex).not.toBe(oldIndex) // Reconstruct the current state by applying the changes const currentState = new Map() for (const [[_, [value, index]]] of initialResult) { currentState.set(JSON.stringify(value), [value, index]) } // Apply the changes for (const [[_, [value, index]], multiplicity] of changes) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check const currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) // Check that indices are still in lexicographic order after the changes expect(checkLexicographicOrder(currentStateArray)).toBe(true) }) it('should handle limit and offset correctly', () => { const graph = new D2({ initialFrontier: 0 }) const input = graph.newInput<[null, { id: number; value: string }]>() const allMessages: any[] = [] input.pipe( topKWithFractionalIndex((a, b) => a.value.localeCompare(b.value), { limit: 3, offset: 1, }), output((message) => { if (message.type === MessageType.DATA) { allMessages.push(message.data) } }), ) graph.finalize() // Initial data - a, b, c, d, e input.sendData( 0, new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'b' }], 1], [[null, { id: 3, value: 'c' }], 1], [[null, { id: 4, value: 'd' }], 1], [[null, { id: 5, value: 'e' }], 1], ]), ) input.sendFrontier(1) graph.run() // Initial result should be b, c, d (offset 1, limit 3) const initialResult = allMessages[0].collection.getInner() expect(initialResult.length).toBe(3) // Check that indices are in lexicographic order expect(checkLexicographicOrder(initialResult)).toBe(true) // Check that we have the correct elements (b, c, d) const initialIds = new Set( initialResult.map(([[_, [value, __]]]) => value.id), ) expect(initialIds.has(1)).toBe(false) // 'a' should be excluded (offset) expect(initialIds.has(2)).toBe(true) // 'b' should be included expect(initialIds.has(3)).toBe(true) // 'c' should be included expect(initialIds.has(4)).toBe(true) // 'd' should be included expect(initialIds.has(5)).toBe(false) // 'e' should be excluded (limit) // Now let's add a new element that should be included in the result input.sendData( 1, new MultiSet([ [[null, { id: 6, value: 'c+' }], 1], // This should be between c and d ]), ) input.sendFrontier(2) graph.run() // Check the changes const changes = allMessages[1].collection.getInner() // We should only emit as many changes as we received // We received 1 change (1 addition) // Since we have a limit, this will push out 1 element, so we'll emit 2 changes // This is still optimal as we're only emitting the minimum necessary changes expect(changes.length).toBe(2) // 1 removal + 1 addition // Find the removal and addition const removal = changes.find(([_, multiplicity]) => multiplicity < 0) const addition = changes.find(([_, multiplicity]) => multiplicity > 0) // Check that we removed 'd' and added 'c+' expect(removal?.[0][1][0].value).toBe('d') expect(addition?.[0][1][0].value).toBe('c+') // Check that the ids are correct expect(removal?.[0][1][0].id).toBe(4) // 'd' has id 4 expect(addition?.[0][1][0].id).toBe(6) // 'c+' has id 6 // The new element reuses the index of the removed element expect(addition?.[0][1][1]).toBe(removal?.[0][1][1]) // Reconstruct the current state by applying the changes const currentState = new Map() for (const [[_, [value, index]]] of initialResult) { currentState.set(JSON.stringify(value), [value, index]) } // Apply the changes for (const [[_, [value, index]], multiplicity] of changes) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check const currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) // Check that indices are still in lexicographic order after the changes expect(checkLexicographicOrder(currentStateArray)).toBe(true) }) it('should handle elements moving positions correctly', () => { const graph = new D2({ initialFrontier: 0 }) const input = graph.newInput<[null, { id: number; value: string }]>() const allMessages: any[] = [] input.pipe( topKWithFractionalIndex((a, b) => a.value.localeCompare(b.value)), output((message) => { if (message.type === MessageType.DATA) { allMessages.push(message.data) } }), ) graph.finalize() // Initial data - a, b, c, d, e input.sendData( 0, new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'b' }], 1], [[null, { id: 3, value: 'c' }], 1], [[null, { id: 4, value: 'd' }], 1], [[null, { id: 5, value: 'e' }], 1], ]), ) input.sendFrontier(1) graph.run() // Initial result should have all elements with fractional indices const initialResult = allMessages[0].collection.getInner() expect(initialResult.length).toBe(5) // Check that indices are in lexicographic order expect(checkLexicographicOrder(initialResult)).toBe(true) // Store the initial indices for later comparison const initialIndices = new Map() for (const [[_, [value, index]]] of initialResult) { initialIndices.set(value.id, index) } // Now let's swap 'b' and 'd' input.sendData( 1, new MultiSet([ [[null, { id: 2, value: 'd+' }], 1], // 'b' becomes 'd+' [[null, { id: 2, value: 'b' }], -1], // Remove old 'b' [[null, { id: 4, value: 'b+' }], 1], // 'd' becomes 'b+' [[null, { id: 4, value: 'd' }], -1], // Remove old 'd' ]), ) input.sendFrontier(2) graph.run() // Check the changes const changes = allMessages[1].collection.getInner() // We should only emit as many changes as we received // We received 4 changes (2 additions, 2 removals) // We should emit at most 4 changes expect(changes.length).toBeLessThanOrEqual(4) expect(changes.length).toBe(4) // 2 removals + 2 additions // Find the removals and additions const removals = changes.filter(([_, multiplicity]) => multiplicity < 0) const additions = changes.filter(([_, multiplicity]) => multiplicity > 0) expect(removals.length).toBe(2) expect(additions.length).toBe(2) // Check that we removed 'b' and 'd' const removedValues = new Set( removals.map(([[_, [value, __]]]) => value.value), ) expect(removedValues.has('b')).toBe(true) expect(removedValues.has('d')).toBe(true) // Check that we added 'b+' and 'd+' const addedValues = new Set( additions.map(([[_, [value, __]]]) => value.value), ) expect(addedValues.has('b+')).toBe(true) expect(addedValues.has('d+')).toBe(true) // Find the specific removals and additions const bRemoval = removals.find(([[_, [value, __]]]) => value.value === 'b') const dRemoval = removals.find(([[_, [value, __]]]) => value.value === 'd') const bPlusAddition = additions.find( ([[_, [value, __]]]) => value.value === 'b+', ) const dPlusAddition = additions.find( ([[_, [value, __]]]) => value.value === 'd+', ) // The elements reuse their indices expect(bPlusAddition?.[0][1][1]).toBe(bRemoval?.[0][1][1]) expect(dPlusAddition?.[0][1][1]).toBe(dRemoval?.[0][1][1]) // Check that we only emitted changes for the elements that moved const changedIds = new Set() for (const [[_, [value, __]], multiplicity] of changes) { changedIds.add(value.id) } expect(changedIds.size).toBe(2) expect(changedIds.has(2)).toBe(true) expect(changedIds.has(4)).toBe(true) // Reconstruct the current state by applying the changes const currentState = new Map() for (const [[_, [value, index]]] of initialResult) { currentState.set(JSON.stringify(value), [value, index]) } // Apply the changes for (const [[_, [value, index]], multiplicity] of changes) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check const currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) // Check that indices are still in lexicographic order after the changes expect(checkLexicographicOrder(currentStateArray)).toBe(true) }) it('should maintain lexicographic order through multiple updates', () => { const graph = new D2({ initialFrontier: 0 }) const input = graph.newInput<[null, { id: number; value: string }]>() const allMessages: any[] = [] input.pipe( topKWithFractionalIndex((a, b) => a.value.localeCompare(b.value)), output((message) => { if (message.type === MessageType.DATA) { allMessages.push(message.data) } }), ) graph.finalize() // Initial data - a, c, e, g, i input.sendData( 0, new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 3, value: 'c' }], 1], [[null, { id: 5, value: 'e' }], 1], [[null, { id: 7, value: 'g' }], 1], [[null, { id: 9, value: 'i' }], 1], ]), ) input.sendFrontier(1) graph.run() // Initial result should have all elements with fractional indices const initialResult = allMessages[0].collection.getInner() expect(initialResult.length).toBe(5) // Check that indices are in lexicographic order expect(checkLexicographicOrder(initialResult)).toBe(true) // Keep track of the current state let currentState = new Map() for (const [[_, [value, index]]] of initialResult) { currentState.set(JSON.stringify(value), [value, index]) } // Update 1: Insert elements between existing ones - b, d, f, h input.sendData( 1, new MultiSet([ [[null, { id: 2, value: 'b' }], 1], [[null, { id: 4, value: 'd' }], 1], [[null, { id: 6, value: 'f' }], 1], [[null, { id: 8, value: 'h' }], 1], ]), ) input.sendFrontier(2) graph.run() // Check the changes const changes1 = allMessages[1].collection.getInner() // We should only emit as many changes as we received // We received 4 changes (4 additions) // We should emit at most 4 changes expect(changes1.length).toBeLessThanOrEqual(4) expect(changes1.length).toBe(4) // 4 additions // Apply the changes to our current state for (const [[_, [value, index]], multiplicity] of changes1) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check let currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) // Check that indices are still in lexicographic order after the changes expect(checkLexicographicOrder(currentStateArray)).toBe(true) // Update 2: Move some elements around input.sendData( 2, new MultiSet([ [[null, { id: 3, value: 'j' }], 1], // Move 'c' to after 'i' [[null, { id: 3, value: 'c' }], -1], // Remove old 'c' [[null, { id: 7, value: 'a-' }], 1], // Move 'g' to before 'a' [[null, { id: 7, value: 'g' }], -1], // Remove old 'g' ]), ) input.sendFrontier(3) graph.run() // Check the changes const changes2 = allMessages[2].collection.getInner() // We should only emit as many changes as we received // We received 4 changes (2 additions, 2 removals) // We should emit at most 4 changes expect(changes2.length).toBeLessThanOrEqual(4) expect(changes2.length).toBe(4) // 2 removals + 2 additions // Apply the changes to our current state for (const [[_, [value, index]], multiplicity] of changes2) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) // Check that indices are still in lexicographic order after the changes expect(checkLexicographicOrder(currentStateArray)).toBe(true) // Update 3: Remove some elements and add new ones input.sendData( 3, new MultiSet([ [[null, { id: 2, value: 'b' }], -1], // Remove 'b' [[null, { id: 4, value: 'd' }], -1], // Remove 'd' [[null, { id: 10, value: 'k' }], 1], // Add 'k' at the end [[null, { id: 11, value: 'c-' }], 1], // Add 'c-' between 'b' and 'd' ]), ) input.sendFrontier(4) graph.run() // Check the changes const changes3 = allMessages[3].collection.getInner() // We should only emit as many changes as we received // We received 4 changes (2 additions, 2 removals) // We should emit at most 4 changes expect(changes3.length).toBeLessThanOrEqual(4) expect(changes3.length).toBe(4) // 2 removals + 2 additions // Apply the changes to our current state for (const [[_, [value, index]], multiplicity] of changes3) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) // Check that indices are still in lexicographic order after all changes expect(checkLexicographicOrder(currentStateArray)).toBe(true) }) it('should maintain correct order when cycling through multiple changes', () => { const graph = new D2({ initialFrontier: 0 }) const input = graph.newInput<[null, { id: number; value: string }]>() const allMessages: any[] = [] input.pipe( topKWithFractionalIndex((a, b) => a.value.localeCompare(b.value)), output((message) => { if (message.type === MessageType.DATA) { allMessages.push(message.data) } }), ) graph.finalize() // Create initial data with 12 items in alphabetical order const initialItems: [[null, { id: number; value: string }], number][] = [] for (let i = 0; i < 12; i++) { const letter = String.fromCharCode(97 + i) // 'a' through 'l' initialItems.push([[null, { id: i + 1, value: letter }], 1]) } // Send initial data input.sendData(0, new MultiSet(initialItems)) input.sendFrontier(1) graph.run() // Initial result should have all 12 elements with fractional indices const initialResult = allMessages[0].collection.getInner() expect(initialResult.length).toBe(12) // Check that indices are in lexicographic order expect(checkLexicographicOrder(initialResult)).toBe(true) // Verify the initial order is a-l verifyOrder(initialResult, [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', ]) // Keep track of the current state let currentState = new Map() for (const [[_, [value, index]]] of initialResult) { currentState.set(JSON.stringify(value), [value, index]) } // Now cycle through 10 changes, moving one item down one position each time // We'll move item 'a' down through the list let currentItem = { id: 1, value: 'a' } let expectedOrder = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', ] for (let i = 0; i < 10; i++) { // Calculate the new position for the item const currentPos = expectedOrder.indexOf(currentItem.value) const newPos = Math.min(currentPos + 1, expectedOrder.length - 1) // Create a new value that will sort to the new position // We'll use the next letter plus the current letter to ensure correct sorting const nextLetter = expectedOrder[newPos] const newValue = nextLetter + currentItem.value // Update the expected order expectedOrder.splice(currentPos, 1) // Remove from current position expectedOrder.splice(newPos, 0, newValue) // Insert at new position // Send the change input.sendData( i + 1, new MultiSet([ [[null, { id: currentItem.id, value: newValue }], 1], // Add with new value [[null, { id: currentItem.id, value: currentItem.value }], -1], // Remove old value ]), ) input.sendFrontier(i + 2) graph.run() // Check the changes const changes = allMessages[i + 1].collection.getInner() // We should only emit as many changes as we received (2) expect(changes.length).toBeLessThanOrEqual(2) expect(changes.length).toBe(2) // 1 removal + 1 addition // Apply the changes to our current state for (const [[_, [value, index]], multiplicity] of changes) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for checks const currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) // Check that indices are still in lexicographic order after the change expect(checkLexicographicOrder(currentStateArray)).toBe(true) // Verify the order matches our expected order verifyOrder(currentStateArray, expectedOrder) // Update the current item for the next iteration currentItem = { id: currentItem.id, value: newValue } } }) it('should handle insertion at the start of the sorted collection', () => { const graph = new D2({ initialFrontier: 0 }) const input = graph.newInput<[null, { id: number; value: string }]>() const allMessages: any[] = [] input.pipe( topKWithFractionalIndex((a, b) => a.value.localeCompare(b.value)), output((message) => { if (message.type === MessageType.DATA) { allMessages.push(message.data) } }), ) graph.finalize() // Initial data - b, c, d, e input.sendData( 0, new MultiSet([ [[null, { id: 2, value: 'b' }], 1], [[null, { id: 3, value: 'c' }], 1], [[null, { id: 4, value: 'd' }], 1], [[null, { id: 5, value: 'e' }], 1], ]), ) input.sendFrontier(1) graph.run() // Initial result should have all elements with fractional indices const initialResult = allMessages[0].collection.getInner() expect(initialResult.length).toBe(4) // Check that indices are in lexicographic order expect(checkLexicographicOrder(initialResult)).toBe(true) // Keep track of the current state let currentState = new Map() for (const [[_, [value, index]]] of initialResult) { currentState.set(JSON.stringify(value), [value, index]) } // Update: Insert element at the start - 'a' input.sendData( 1, new MultiSet([ [[null, { id: 1, value: 'a' }], 1], // This should be inserted at the start ]), ) input.sendFrontier(2) graph.run() // Check the changes const changes = allMessages[1].collection.getInner() // We should only emit as many changes as we received (1 addition) expect(changes.length).toBe(1) // Apply the changes to our current state for (const [[_, [value, index]], multiplicity] of changes) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check let currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) expect(checkLexicographicOrder(currentStateArray)).toBe(true) // Verify the order of elements const expectedOrder = ['a', 'b', 'c', 'd', 'e'] verifyOrder(currentStateArray, expectedOrder) // Check that the new element 'a' has an index that is lexicographically before 'b' const aValue = { id: 1, value: 'a' } const bValue = { id: 2, value: 'b' } const aIndex = currentState.get(JSON.stringify(aValue))[1] const bIndex = currentState.get(JSON.stringify(bValue))[1] // Directly check that 'a' comes before 'b' lexicographically expect(aIndex < bIndex).toBe(true) }) it('should handle multiple insertion at the start of the sorted collection', () => { const graph = new D2({ initialFrontier: 0 }) const input = graph.newInput<[null, { id: number; value: string }]>() const allMessages: any[] = [] input.pipe( topKWithFractionalIndex((a, b) => a.value.localeCompare(b.value)), output((message) => { if (message.type === MessageType.DATA) { allMessages.push(message.data) } }), ) graph.finalize() // Initial data - b, c, d, e input.sendData( 0, new MultiSet([ [[null, { id: 3, value: 'c' }], 1], [[null, { id: 4, value: 'd' }], 1], [[null, { id: 5, value: 'e' }], 1], [[null, { id: 6, value: 'f' }], 1], ]), ) input.sendFrontier(1) graph.run() // Initial result should have all elements with fractional indices const initialResult = allMessages[0].collection.getInner() expect(initialResult.length).toBe(4) // Check that indices are in lexicographic order expect(checkLexicographicOrder(initialResult)).toBe(true) // Keep track of the current state let currentState = new Map() for (const [[_, [value, index]]] of initialResult) { currentState.set(JSON.stringify(value), [value, index]) } // Update: Insert element at the start - 'a' input.sendData( 1, new MultiSet([ [[null, { id: 1, value: 'a' }], 1], // This should be inserted at the start [[null, { id: 2, value: 'b' }], 1], // This should be inserted at the start ]), ) input.sendFrontier(2) graph.run() // Check the changes const changes = allMessages[1].collection.getInner() // We should only emit as many changes as we received (1 addition) expect(changes.length).toBe(2) // Apply the changes to our current state for (const [[_, [value, index]], multiplicity] of changes) { if (multiplicity < 0) { // Remove currentState.delete(JSON.stringify(value)) } else { // Add currentState.set(JSON.stringify(value), [value, index]) } } // Convert to array for lexicographic order check let currentStateArray = Array.from(currentState.values()).map( ([value, index]) => [[null, [value, index]], 1], ) expect(checkLexicographicOrder(currentStateArray)).toBe(true) // Verify the order of elements const expectedOrder = ['a', 'b', 'c', 'd', 'e', 'f'] verifyOrder(currentStateArray, expectedOrder) }) }