diff --git a/.changeset/fix-optimistic-collection-order.md b/.changeset/fix-optimistic-collection-order.md new file mode 100644 index 000000000..f2d86bf82 --- /dev/null +++ b/.changeset/fix-optimistic-collection-order.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Preserve a collection's configured sort order while optimistic inserts and updates are visible. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index e286b4f08..75c57de67 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -31,7 +31,7 @@ comment and the current API/architecture contract before extending its model. | Ordered relations and BTree | [top-K relation oracle](../../packages/db-ivm/tests/operators/topk-relation-oracle.test.ts), [BTree/Map](../../packages/db/tests/btree-map-oracle.test.ts), [incrementalization laws](../../packages/db-ivm/tests/incrementalization-law.property.test.ts) | Independent ordered relations and cumulative signed output. Top-K consolidation compares same-key values without hashing, including cyclic replacements and fresh transient cancellation. Other hash-based operators retain hashing's declared domain. Algebra does not specify client readiness. | | Includes and publication | [cross-formulation](../../packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts), [temporal](../../packages/db/tests/query/includes-temporal-oracle.test.ts), [Collection includes](../../packages/db/tests/query/includes-collection-oracle.property.test.ts), [architecture and complete suite map](../../packages/db/src/query/live/ARCHITECTURE.md#executable-contracts) | Per-parent/flat-join/partition relations, callback-time rows, nested values, and route histories. Observe raw promised order; fresh queries do not establish continuous publication safety. | | Collection lifecycle | [history](../../packages/db/tests/collection-subscription-lifecycle-history.property.test.ts), [publication](../../packages/db/tests/collection-subscription-lifecycle-publication.property.test.ts), [replay](../../packages/db/tests/collection-subscription-replay-oracle.property.test.ts), [effect disposal](../../packages/db/tests/effect-disposal-oracle.test.ts) | Ownership and phase histories, exact caller/error/publication evidence, late completion and restart. Effect self-dependent disposal remains a separate contract question. | -| Optimistic state | [history model](../../packages/db/tests/optimistic-history-oracle.ts), [generated histories](../../packages/db/tests/optimistic-transaction-oracle.property.test.ts), [outcomes](../../packages/db/tests/optimistic-history-outcomes.test.ts), [publication](../../packages/db/tests/optimistic-history-publication.test.ts) | Independent whole-row snapshots, rollback dependencies, metadata and prior-value events. Never rebase a pending snapshot merely to simplify the model. | +| Optimistic state | [history model](../../packages/db/tests/optimistic-history-oracle.ts), [generated histories](../../packages/db/tests/optimistic-transaction-oracle.property.test.ts), [outcomes](../../packages/db/tests/optimistic-history-outcomes.test.ts), [publication](../../packages/db/tests/optimistic-history-publication.test.ts), [visible order](../../packages/db/tests/collection-visible-order-oracle.test.ts) | Independent whole-row snapshots, rollback dependencies, metadata, prior-value events, and declared Collection order over the complete visible optimistic overlay. Visible-order coverage spans Collection iteration surfaces; it does not promise change-message order or inherited live-query order. Never rebase a pending snapshot merely to simplify the model. | | Drafts and native values | [proxy](../../packages/db/tests/proxy.test.ts), [detachment](../../packages/db/tests/proxy-detachment-contract.test.ts), [iteration](../../packages/db/tests/proxy-iteration-contract.test.ts) | Native-operation controls, exact patches and actual stored rows; alias/cycle/adversarial-key histories. General native-mutator and symbol-write support is not established by a plain-object oracle. | | Query DB and observer | [ownership](../../packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts), [load lifecycle](../../packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts), [observer histories](../../packages/db/tests/live-query-observer-history.property.test.ts) | Real QueryClient boundary and a per-listener eligibility ledger, not a duplicate dispatch queue. Check reentry, peer survival, FIFO and disposal independently of final rows. | | Ordered acquisition | [pagination](../../packages/db/tests/query/pagination-oracle.property.test.ts), [ordered work](../../packages/db/tests/query/ordered-work-oracle.property.test.ts), [ordered lifecycle](../../packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts) | Complete finite provider results, pending windows, ties/nulls, ownership and documented repair timing. Request completion is not proof of unrequested source extent. | diff --git a/packages/db/package.json b/packages/db/package.json index 18857f854..132d25709 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -22,7 +22,7 @@ "lint": "eslint . --fix", "test": "vitest --run", "test:facade-retention": "node --expose-gc --import tsx tests/facade-retention.probe.ts", - "test:oracles": "vitest --run tests/collection-cleanup-restart-oracle.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/optimistic-settlement-boundaries.test.ts tests/optimistic-history-publication.test.ts tests/optimistic-history-outcomes.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-lifecycle-publication.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-observer-history.property.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts", + "test:oracles": "vitest --run tests/collection-cleanup-restart-oracle.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/optimistic-settlement-boundaries.test.ts tests/optimistic-history-publication.test.ts tests/optimistic-history-outcomes.test.ts tests/collection-visible-order-oracle.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-lifecycle-publication.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-observer-history.property.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts", "bench:nested-includes": "vitest bench tests/query/includes-performance.bench.ts --run" }, "type": "module", diff --git a/packages/db/src/collection/state.ts b/packages/db/src/collection/state.ts index 48d24a376..0bca56659 100644 --- a/packages/db/src/collection/state.ts +++ b/packages/db/src/collection/state.ts @@ -1,3 +1,4 @@ +import { compareKeys } from '@tanstack/db-ivm' import { deepEquals } from '../utils' import { SortedMap } from '../SortedMap' import { enrichRowWithVirtualProps } from '../virtual-props.js' @@ -414,20 +415,64 @@ export class CollectionStateManager< */ public *keys(): IterableIterator { const { syncedData, optimisticDeletes, optimisticUpserts } = this - // Yield keys from synced data, skipping any that are deleted. - for (const key of syncedData.keys()) { - if (!optimisticDeletes.has(key)) { - yield key + const compare = this.config.compare + + if (!compare || optimisticUpserts.size === 0) { + // Yield keys from synced data, skipping any that are deleted. + for (const key of syncedData.keys()) { + if (!optimisticDeletes.has(key)) { + yield key + } + } + // Without a comparator, preserve insertion order for optimistic keys. + for (const key of optimisticUpserts.keys()) { + if (!syncedData.has(key) && !optimisticDeletes.has(key)) { + yield key + } + } + return + } + + const compareEntries = ( + [leftKey, leftValue]: [TKey, TOutput], + [rightKey, rightValue]: [TKey, TOutput], + ) => compare(leftValue, rightValue) || compareKeys(leftKey, rightKey) + const optimisticEntries = [...optimisticUpserts.entries()] + .filter(([key]) => !optimisticDeletes.has(key)) + .sort(compareEntries) + const syncedEntries = syncedData.entries() + let syncedEntry = syncedEntries.next() + const advanceSynced = () => { + while ( + !syncedEntry.done && + (optimisticDeletes.has(syncedEntry.value[0]) || + optimisticUpserts.has(syncedEntry.value[0])) + ) { + syncedEntry = syncedEntries.next() } } - // Yield keys from upserts that were not already in synced data. - for (const key of optimisticUpserts.keys()) { - if (!syncedData.has(key) && !optimisticDeletes.has(key)) { - // The optimisticDeletes check is technically redundant if inserts/updates always remove from deletes, - // but it's safer to keep it. - yield key + advanceSynced() + + let optimisticIndex = 0 + while (!syncedEntry.done && optimisticIndex < optimisticEntries.length) { + const optimisticEntry = optimisticEntries[optimisticIndex]! + if (compareEntries(optimisticEntry, syncedEntry.value) < 0) { + yield optimisticEntry[0] + optimisticIndex++ + } else { + yield syncedEntry.value[0] + syncedEntry = syncedEntries.next() + advanceSynced() } } + while (!syncedEntry.done) { + yield syncedEntry.value[0] + syncedEntry = syncedEntries.next() + advanceSynced() + } + while (optimisticIndex < optimisticEntries.length) { + yield optimisticEntries[optimisticIndex++]![0] + } } /** diff --git a/packages/db/tests/collection-sync-reentrancy.test.ts b/packages/db/tests/collection-sync-reentrancy.test.ts index bf0e66d31..c23dda7ab 100644 --- a/packages/db/tests/collection-sync-reentrancy.test.ts +++ b/packages/db/tests/collection-sync-reentrancy.test.ts @@ -856,7 +856,7 @@ describe(`sync publication reentrancy`, () => { }, ) - it(`honors a parked layout mark when truncate drains its causal prefix`, async () => { + it(`keeps a parked layout mark hidden when truncate preserves visible order`, async () => { const updatePersistence = createDeferred() const insertPersistence = createDeferred() let sync!: OrderedSync @@ -930,22 +930,22 @@ describe(`sync publication reentrancy`, () => { const truncateReceipt = sync.commit() expect(truncateReceipt).toBe(true) - expect([...collection.keys()]).toEqual([2, 1, 3]) + expect([...collection.keys()]).toEqual([1, 2, 3]) expect(collection.toArray.map(({ value }) => value)).toEqual([ - `two`, `optimistic-one`, + `two`, `optimistic-three`, ]) - expect(collection._layoutRevision).toBe(revisionBeforeDrain + 1) + expect(collection._layoutRevision).toBe(revisionBeforeDrain) expect(callbacks).toEqual([ { // Delete the prior public layout [1, 2, 3], not the unpublished - // rank update's intermediate [2, 1, 3], then replay whole snapshots. + // server-only rank update, then replay whole visible snapshots. changes: [1, 2, 3, 1, 3, 1, 2], - keys: [2, 1, 3], - values: [`two`, `optimistic-one`, `optimistic-three`], + keys: [1, 2, 3], + values: [`optimistic-one`, `two`, `optimistic-three`], markedReceiptSettled: false, - revision: revisionBeforeDrain + 1, + revision: revisionBeforeDrain, }, ]) if (firstReceipt !== true) await firstReceipt diff --git a/packages/db/tests/collection-visible-order-oracle.test.ts b/packages/db/tests/collection-visible-order-oracle.test.ts new file mode 100644 index 000000000..9aa4c5822 --- /dev/null +++ b/packages/db/tests/collection-visible-order-oracle.test.ts @@ -0,0 +1,390 @@ +import { describe, expect, it } from 'vitest' +import { createCollection } from '../src/collection/index.js' +import { createDeferred } from '../src/deferred.js' +import type { Collection } from '../src/collection/index.js' +import type { SyncConfig } from '../src/types.js' + +type RankedRow = { id: string; rank: string } +type RankedCollection = Collection +type RankedSync = Parameters[`sync`]>[0] + +const compareByRank = (left: RankedRow, right: RankedRow): number => + left.rank.localeCompare(right.rank) + +const orderedIds = (rows: ReadonlyArray): Array => + [...rows] + .sort( + (left, right) => + left.rank.localeCompare(right.rank) || left.id.localeCompare(right.id), + ) + .map(({ id }) => id) + +const visibleOrders = (collection: RankedCollection) => ({ + keys: [...collection.keys()], + values: [...collection.values()].map(({ id }) => id), + entries: [...collection.entries()].map(([key]) => key), + iterator: [...collection].map(([key]) => key), + state: [...collection.state.keys()], + toArray: collection.toArray.map(({ id }) => id), + changes: collection.currentStateAsChanges()!.map(({ key }) => key), +}) + +const expectedVisibleOrders = (expected: ReadonlyArray) => ({ + keys: expected, + values: expected, + entries: expected, + iterator: expected, + state: expected, + toArray: expected, + changes: expected, +}) + +const expectVisibleOrder = ( + collection: RankedCollection, + expected: ReadonlyArray, +) => { + expect(visibleOrders(collection)).toEqual(expectedVisibleOrders(expected)) +} + +let collectionId = 0 + +const setupCollection = async ( + rows: ReadonlyArray, + compare?: (left: RankedRow, right: RankedRow) => number, +) => { + let sync!: RankedSync + const persistence = createDeferred() + const collection = createCollection({ + id: `visible-order-oracle-${collectionId++}`, + getKey: (row) => row.id, + compare, + startSync: true, + sync: { + sync: (actions) => { + sync = actions + actions.begin() + for (const row of rows) + actions.write({ type: `insert`, value: { ...row } }) + actions.commit() + actions.markReady() + }, + }, + onInsert: () => persistence.promise, + onUpdate: () => persistence.promise, + onDelete: () => persistence.promise, + }) + await collection.preload() + return { collection, persistence, sync } +} + +const confirm = async ( + fixture: Awaited>, + transaction: ReturnType, + change: { type: `insert` | `update` | `delete`; value: RankedRow }, +) => { + fixture.sync.begin() + fixture.sync.write(change) + const authoritative = fixture.sync.commit() + expect( + authoritative, + `authoritative row waits for the pending transaction`, + ).toBeInstanceOf(Promise) + fixture.persistence.resolve() + await transaction.isPersisted.promise + await authoritative +} + +describe(`Collection visible ordering`, () => { + it(`distinguishes declared order from append-after-authoritative order`, () => { + const rows = [ + { id: `a`, rank: `a1` }, + { id: `c`, rank: `a3` }, + { id: `b`, rank: `a2` }, + ] + + expect(rows.map(({ id }) => id)).toEqual([`a`, `c`, `b`]) + expect(orderedIds(rows)).toEqual([`a`, `b`, `c`]) + }) + + it(`orders a pending optimistic insert on every public iteration surface`, async () => { + const fixture = await setupCollection( + [ + { id: `a`, rank: `a1` }, + { id: `c`, rank: `a3` }, + ], + compareByRank, + ) + const callbackOrders: Array> = [] + const subscription = fixture.collection.subscribeChanges( + () => callbackOrders.push(visibleOrders(fixture.collection)), + { includeInitialState: false }, + ) + const transaction = fixture.collection.insert({ id: `b`, rank: `a2` }) + + try { + expect(fixture.collection.get(`b`)?.$synced, `optimistic path`).toBe( + false, + ) + expectVisibleOrder(fixture.collection, [`a`, `b`, `c`]) + expect(callbackOrders, `one complete callback-time view`).toEqual([ + expectedVisibleOrders([`a`, `b`, `c`]), + ]) + + await confirm(fixture, transaction, { + type: `insert`, + value: { id: `b`, rank: `a2` }, + }) + expectVisibleOrder(fixture.collection, [`a`, `b`, `c`]) + } finally { + fixture.persistence.resolve() + await transaction.isPersisted.promise.catch(() => undefined) + subscription.unsubscribe() + await fixture.collection.cleanup() + } + }) + + it(`moves an optimistically updated row using its visible value`, async () => { + const fixture = await setupCollection( + [ + { id: `a`, rank: `a1` }, + { id: `b`, rank: `a2` }, + { id: `c`, rank: `a3` }, + ], + compareByRank, + ) + const transaction = fixture.collection.update(`c`, (draft) => { + draft.rank = `a0` + }) + + try { + expectVisibleOrder(fixture.collection, [`c`, `a`, `b`]) + await confirm(fixture, transaction, { + type: `update`, + value: { id: `c`, rank: `a0` }, + }) + expectVisibleOrder(fixture.collection, [`c`, `a`, `b`]) + } finally { + fixture.persistence.resolve() + await transaction.isPersisted.promise.catch(() => undefined) + await fixture.collection.cleanup() + } + }) + + it.each([`rollback`, `reject`] as const)( + `restores ordered public surfaces after optimistic update %s`, + async (outcome) => { + const fixture = await setupCollection( + [ + { id: `a`, rank: `a1` }, + { id: `b`, rank: `a2` }, + { id: `c`, rank: `a3` }, + ], + compareByRank, + ) + const callbackOrders: Array> = [] + const subscription = fixture.collection.subscribeChanges( + () => callbackOrders.push(visibleOrders(fixture.collection)), + { includeInitialState: false }, + ) + const transaction = fixture.collection.update(`c`, (draft) => { + draft.rank = `a0` + }) + + try { + expectVisibleOrder(fixture.collection, [`c`, `a`, `b`]) + if (outcome === `rollback`) { + transaction.rollback() + fixture.persistence.resolve() + } else { + fixture.persistence.reject(new Error(`rejected update`)) + } + await transaction.isPersisted.promise.catch(() => undefined) + + expectVisibleOrder(fixture.collection, [`a`, `b`, `c`]) + expect( + callbackOrders, + `optimistic and restored callback views`, + ).toEqual([ + expectedVisibleOrders([`c`, `a`, `b`]), + expectedVisibleOrders([`a`, `b`, `c`]), + ]) + } finally { + fixture.persistence.resolve() + await transaction.isPersisted.promise.catch(() => undefined) + subscription.unsubscribe() + await fixture.collection.cleanup() + } + }, + ) + + it(`omits an optimistically deleted row from the ordered overlay`, async () => { + const fixture = await setupCollection( + [ + { id: `a`, rank: `a1` }, + { id: `b`, rank: `a2` }, + { id: `c`, rank: `a3` }, + ], + compareByRank, + ) + const transaction = fixture.collection.delete(`b`) + + try { + expectVisibleOrder(fixture.collection, [`a`, `c`]) + await confirm(fixture, transaction, { + type: `delete`, + value: { id: `b`, rank: `a2` }, + }) + expectVisibleOrder(fixture.collection, [`a`, `c`]) + } finally { + fixture.persistence.resolve() + await transaction.isPersisted.promise.catch(() => undefined) + await fixture.collection.cleanup() + } + }) + + it(`preserves negative and positive comparator results before key ties`, async () => { + const observed = new Set() + const compare = (left: RankedRow, right: RankedRow) => { + const comparison = + left.rank < right.rank ? -7 : left.rank > right.rank ? 11 : 0 + observed.add(comparison) + return comparison + } + const fixture = await setupCollection([{ id: `m`, rank: `b` }], compare) + const transactions = [ + fixture.collection.insert({ id: `z`, rank: `a` }), + fixture.collection.insert({ id: `a`, rank: `c` }), + ] + + try { + // Both keys conflict with their value order, so falling back to the key + // comparator for either non-zero result would reverse the visible rows. + expectVisibleOrder(fixture.collection, [`z`, `m`, `a`]) + expect(observed.has(-7), `negative comparator result`).toBe(true) + expect(observed.has(11), `positive comparator result`).toBe(true) + } finally { + fixture.persistence.resolve() + await Promise.all( + transactions.map((transaction) => + transaction.isPersisted.promise.catch(() => undefined), + ), + ) + await fixture.collection.cleanup() + } + }) + + it.each([0, -0, Number.NaN])( + `uses the existing key tie-break for comparator result %s`, + async (comparison) => { + const fixture = await setupCollection( + [ + { id: `a`, rank: `same` }, + { id: `c`, rank: `same` }, + ], + () => comparison, + ) + const transaction = fixture.collection.insert({ id: `b`, rank: `same` }) + + try { + expectVisibleOrder(fixture.collection, [`a`, `b`, `c`]) + await confirm(fixture, transaction, { + type: `insert`, + value: { id: `b`, rank: `same` }, + }) + } finally { + fixture.persistence.resolve() + await transaction.isPersisted.promise.catch(() => undefined) + await fixture.collection.cleanup() + } + }, + ) + + it(`preserves append-after-authoritative order without a comparator`, async () => { + const fixture = await setupCollection([ + { id: `a`, rank: `a1` }, + { id: `c`, rank: `a3` }, + ]) + const transaction = fixture.collection.insert({ id: `b`, rank: `a2` }) + + try { + expectVisibleOrder(fixture.collection, [`a`, `c`, `b`]) + await confirm(fixture, transaction, { + type: `insert`, + value: { id: `b`, rank: `a2` }, + }) + } finally { + fixture.persistence.resolve() + await transaction.isPersisted.promise.catch(() => undefined) + await fixture.collection.cleanup() + } + }) + + it(`keeps one optimistic upsert within linear comparison work`, async () => { + let comparisons = 0 + const compare = (left: RankedRow, right: RankedRow) => { + comparisons++ + return left.rank.localeCompare(right.rank) + } + const rows = Array.from({ length: 128 }, (_, index) => ({ + id: `base-${index.toString().padStart(3, `0`)}`, + rank: index.toString().padStart(3, `0`), + })) + const fixture = await setupCollection(rows, compare) + const transaction = fixture.collection.insert({ + id: `optimistic`, + rank: `064.5`, + }) + + try { + comparisons = 0 + const keys = [...fixture.collection.keys()] + expect(keys.indexOf(`optimistic`), `visible merged position`).toBe(65) + expect(comparisons, `linear merge comparison bound`).toBeLessThanOrEqual( + rows.length + 1, + ) + } finally { + fixture.persistence.resolve() + await transaction.isPersisted.promise.catch(() => undefined) + await fixture.collection.cleanup() + } + }) + + it(`sorts multiple optimistic upserts without quadratic comparison work`, async () => { + let comparisons = 0 + const compare = (left: RankedRow, right: RankedRow) => { + comparisons++ + return left.rank.localeCompare(right.rank) + } + const rows = Array.from({ length: 128 }, (_, index) => ({ + id: `base-${index.toString().padStart(3, `0`)}`, + rank: (index * 2).toString().padStart(3, `0`), + })) + const optimisticRows = Array.from({ length: 16 }, (_, index) => ({ + id: `optimistic-${index.toString().padStart(2, `0`)}`, + rank: (31 - index * 2).toString().padStart(3, `0`), + })) + const fixture = await setupCollection(rows, compare) + const transactions = optimisticRows.map((row) => + fixture.collection.insert(row), + ) + + try { + comparisons = 0 + expect([...fixture.collection.keys()]).toEqual( + orderedIds([...rows, ...optimisticRows]), + ) + expect( + comparisons, + `optimistic-side comparison bound`, + ).toBeLessThanOrEqual(64) + } finally { + fixture.persistence.resolve() + await Promise.all( + transactions.map((transaction) => + transaction.isPersisted.promise.catch(() => undefined), + ), + ) + await fixture.collection.cleanup() + } + }) +})