diff --git a/.changeset/eager-wings-jog.md b/.changeset/eager-wings-jog.md new file mode 100644 index 000000000..535d59359 --- /dev/null +++ b/.changeset/eager-wings-jog.md @@ -0,0 +1,5 @@ +--- +"@tanstack/electric-db-collection": patch +--- + +The awaitTxId utility now resolves transaction IDs based on snapshot-end message metadata (xmin, xmax, xip_list) in addition to explicit txid arrays, enabling matching on the initial snapshot at the start of a new shape. diff --git a/packages/electric-db-collection/package.json b/packages/electric-db-collection/package.json index 172b3e5a0..a5ea12d0e 100644 --- a/packages/electric-db-collection/package.json +++ b/packages/electric-db-collection/package.json @@ -3,8 +3,8 @@ "description": "ElectricSQL collection for TanStack DB", "version": "0.1.28", "dependencies": { - "@standard-schema/spec": "^1.0.0", "@electric-sql/client": "^1.0.14", + "@standard-schema/spec": "^1.0.0", "@tanstack/db": "workspace:*", "@tanstack/store": "^0.7.7", "debug": "^4.4.3" diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index b4c507093..3dcb54b64 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -2,6 +2,7 @@ import { ShapeStream, isChangeMessage, isControlMessage, + isVisibleInSnapshot, } from "@electric-sql/client" import { Store } from "@tanstack/store" import DebugModule from "debug" @@ -27,6 +28,7 @@ import type { ControlMessage, GetExtensions, Message, + PostgresSnapshot, Row, ShapeStreamOptions, } from "@electric-sql/client" @@ -38,6 +40,23 @@ const debug = DebugModule.debug(`ts/db:electric`) */ export type Txid = number +/** + * Type representing the result of an insert, update, or delete handler + */ +type MaybeTxId = + | { + txid?: Txid | Array + } + | undefined + | null + +/** + * Type representing a snapshot end message + */ +type SnapshotEndMessage = ControlMessage & { + headers: { control: `snapshot-end` } +} + // The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package // but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row` // This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema @@ -80,6 +99,20 @@ function isMustRefetchMessage>( return isControlMessage(message) && message.headers.control === `must-refetch` } +function isSnapshotEndMessage>( + message: Message +): message is SnapshotEndMessage { + return isControlMessage(message) && message.headers.control === `snapshot-end` +} + +function parseSnapshotMessage(message: SnapshotEndMessage): PostgresSnapshot { + return { + xmin: message.headers.xmin, + xmax: message.headers.xmax, + xip_list: message.headers.xip_list, + } +} + // Check if a message contains txids in its headers function hasTxids>( message: Message @@ -139,8 +172,10 @@ export function electricCollectionOptions( schema?: any } { const seenTxids = new Store>(new Set([])) + const seenSnapshots = new Store>([]) const sync = createElectricSync(config.shapeOptions, { seenTxids, + seenSnapshots, }) /** @@ -158,20 +193,46 @@ export function electricCollectionOptions( throw new ExpectedNumberInAwaitTxIdError(typeof txId) } + // First check if the txid is in the seenTxids store const hasTxid = seenTxids.state.has(txId) if (hasTxid) return true + // Then check if the txid is in any of the seen snapshots + const hasSnapshot = seenSnapshots.state.some((snapshot) => + isVisibleInSnapshot(txId, snapshot) + ) + if (hasSnapshot) return true + return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { - unsubscribe() + unsubscribeSeenTxids() + unsubscribeSeenSnapshots() reject(new TimeoutWaitingForTxIdError(txId)) }, timeout) - const unsubscribe = seenTxids.subscribe(() => { + const unsubscribeSeenTxids = seenTxids.subscribe(() => { if (seenTxids.state.has(txId)) { debug(`awaitTxId found match for txid %o`, txId) clearTimeout(timeoutId) - unsubscribe() + unsubscribeSeenTxids() + unsubscribeSeenSnapshots() + resolve(true) + } + }) + + const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => { + const visibleSnapshot = seenSnapshots.state.find((snapshot) => + isVisibleInSnapshot(txId, snapshot) + ) + if (visibleSnapshot) { + debug( + `awaitTxId found match for txid %o in snapshot %o`, + txId, + visibleSnapshot + ) + clearTimeout(timeoutId) + unsubscribeSeenSnapshots() + unsubscribeSeenTxids() resolve(true) } }) @@ -183,8 +244,9 @@ export function electricCollectionOptions( ? async (params: InsertMutationFnParams) => { // Runtime check (that doesn't follow type) - const handlerResult = (await config.onInsert!(params)) ?? {} - const txid = (handlerResult as { txid?: Txid | Array }).txid + const handlerResult = + ((await config.onInsert!(params)) as MaybeTxId) ?? {} + const txid = handlerResult.txid if (!txid) { throw new ElectricInsertHandlerMustReturnTxIdError() @@ -205,8 +267,9 @@ export function electricCollectionOptions( ? async (params: UpdateMutationFnParams) => { // Runtime check (that doesn't follow type) - const handlerResult = (await config.onUpdate!(params)) ?? {} - const txid = (handlerResult as { txid?: Txid | Array }).txid + const handlerResult = + ((await config.onUpdate!(params)) as MaybeTxId) ?? {} + const txid = handlerResult.txid if (!txid) { throw new ElectricUpdateHandlerMustReturnTxIdError() @@ -269,9 +332,11 @@ function createElectricSync>( shapeOptions: ShapeStreamOptions>, options: { seenTxids: Store> + seenSnapshots: Store> } ): SyncConfig { const { seenTxids } = options + const { seenSnapshots } = options // Store for the relation schema information const relationSchema = new Store(undefined) @@ -342,6 +407,7 @@ function createElectricSync>( }) let transactionStarted = false const newTxids = new Set() + const newSnapshots: Array = [] unsubscribeStream = stream.subscribe((messages: Array>) => { let hasUpToDate = false @@ -373,6 +439,8 @@ function createElectricSync>( ...message.headers, }, }) + } else if (isSnapshotEndMessage(message)) { + newSnapshots.push(parseSnapshotMessage(message)) } else if (isUpToDateMessage(message)) { hasUpToDate = true } else if (isMustRefetchMessage(message)) { @@ -413,6 +481,16 @@ function createElectricSync>( newTxids.clear() return clonedSeen }) + + // Always commit snapshots when we receive up-to-date, regardless of transaction state + seenSnapshots.setState((currentSnapshots) => { + const seen = [...currentSnapshots, ...newSnapshots] + newSnapshots.forEach((snapshot) => + debug(`new snapshot synced from pg %o`, snapshot) + ) + newSnapshots.length = 0 + return seen + }) } }) diff --git a/packages/electric-db-collection/tests/electric.test.ts b/packages/electric-db-collection/tests/electric.test.ts index 617416759..ba21e5c6d 100644 --- a/packages/electric-db-collection/tests/electric.test.ts +++ b/packages/electric-db-collection/tests/electric.test.ts @@ -1031,6 +1031,222 @@ describe(`Electric Integration`, () => { await expect(testCollection.utils.awaitTxId(400)).resolves.toBe(true) }) + it(`should handle snapshot-end messages and match txids via snapshot metadata`, async () => { + const config = { + id: `snapshot-test`, + shapeOptions: { + url: `http://test-url`, + params: { + table: `test_table`, + }, + }, + getKey: (item: Row) => item.id as number, + startSync: true, + } + + const testCollection = createCollection(electricCollectionOptions(config)) + + // Send snapshot-end message with PostgresSnapshot metadata + // xmin=100, xmax=150, xip_list=[120, 130] + // Visible: txid < 100 (committed before snapshot) OR (100 <= txid < 150 AND txid NOT IN [120, 130]) + // Not visible: txid >= 150 (not yet assigned) OR txid IN [120, 130] (in-progress) + subscriber([ + { + key: `1`, + value: { id: 1, name: `Test User` }, + headers: { operation: `insert` }, + }, + { + headers: { + control: `snapshot-end`, + xmin: `100`, + xmax: `150`, + xip_list: [`120`, `130`], + }, + }, + { + headers: { control: `up-to-date` }, + }, + ]) + + // Txids that are visible in the snapshot should resolve + // Txids < xmin are committed and visible + await expect(testCollection.utils.awaitTxId(50)).resolves.toBe(true) + await expect(testCollection.utils.awaitTxId(99)).resolves.toBe(true) + + // Txids in range [xmin, xmax) not in xip_list are visible + await expect(testCollection.utils.awaitTxId(100)).resolves.toBe(true) + await expect(testCollection.utils.awaitTxId(110)).resolves.toBe(true) + await expect(testCollection.utils.awaitTxId(121)).resolves.toBe(true) + await expect(testCollection.utils.awaitTxId(125)).resolves.toBe(true) + await expect(testCollection.utils.awaitTxId(131)).resolves.toBe(true) + await expect(testCollection.utils.awaitTxId(149)).resolves.toBe(true) + + // Txids in xip_list (in-progress transactions) should NOT resolve + await expect(testCollection.utils.awaitTxId(120, 100)).rejects.toThrow( + `Timeout waiting for txId: 120` + ) + await expect(testCollection.utils.awaitTxId(130, 100)).rejects.toThrow( + `Timeout waiting for txId: 130` + ) + + // Txids >= xmax should NOT resolve (not yet assigned) + await expect(testCollection.utils.awaitTxId(150, 100)).rejects.toThrow( + `Timeout waiting for txId: 150` + ) + await expect(testCollection.utils.awaitTxId(200, 100)).rejects.toThrow( + `Timeout waiting for txId: 200` + ) + }) + + it(`should await for txid that arrives later via snapshot-end`, async () => { + const config = { + id: `snapshot-await-test`, + shapeOptions: { + url: `http://test-url`, + params: { + table: `test_table`, + }, + }, + getKey: (item: Row) => item.id as number, + startSync: true, + } + + const testCollection = createCollection(electricCollectionOptions(config)) + + // Start waiting for a txid before snapshot arrives + const txidToAwait = 105 + const promise = testCollection.utils.awaitTxId(txidToAwait, 2000) + + // Send snapshot-end message after a delay + setTimeout(() => { + subscriber([ + { + headers: { + control: `snapshot-end`, + xmin: `100`, + xmax: `110`, + xip_list: [], + }, + }, + { + headers: { control: `up-to-date` }, + }, + ]) + }, 50) + + // The promise should resolve when the snapshot arrives + await expect(promise).resolves.toBe(true) + }) + + it(`should handle multiple snapshots and track all of them`, async () => { + const config = { + id: `multiple-snapshots-test`, + shapeOptions: { + url: `http://test-url`, + params: { + table: `test_table`, + }, + }, + getKey: (item: Row) => item.id as number, + startSync: true, + } + + const testCollection = createCollection(electricCollectionOptions(config)) + + // Send first snapshot: visible txids < 110 + subscriber([ + { + headers: { + control: `snapshot-end`, + xmin: `100`, + xmax: `110`, + xip_list: [], + }, + }, + { + headers: { control: `up-to-date` }, + }, + ]) + + // Send second snapshot: visible txids < 210 + subscriber([ + { + headers: { + control: `snapshot-end`, + xmin: `200`, + xmax: `210`, + xip_list: [], + }, + }, + { + headers: { control: `up-to-date` }, + }, + ]) + + // Txids visible in first snapshot + await expect(testCollection.utils.awaitTxId(105)).resolves.toBe(true) + + // Txids visible in second snapshot + await expect(testCollection.utils.awaitTxId(205)).resolves.toBe(true) + + // Txid 150 is visible in second snapshot (< xmin=200 means committed) + await expect(testCollection.utils.awaitTxId(150)).resolves.toBe(true) + + // Txids >= second snapshot's xmax should timeout (not yet assigned) + await expect(testCollection.utils.awaitTxId(210, 100)).rejects.toThrow( + `Timeout waiting for txId: 210` + ) + await expect(testCollection.utils.awaitTxId(300, 100)).rejects.toThrow( + `Timeout waiting for txId: 300` + ) + }) + + it(`should prefer explicit txids over snapshot matching when both are available`, async () => { + const config = { + id: `explicit-txid-priority-test`, + shapeOptions: { + url: `http://test-url`, + params: { + table: `test_table`, + }, + }, + getKey: (item: Row) => item.id as number, + startSync: true, + } + + const testCollection = createCollection(electricCollectionOptions(config)) + + // Send message with explicit txid and snapshot + subscriber([ + { + key: `1`, + value: { id: 1, name: `Test User` }, + headers: { + operation: `insert`, + txids: [500], + }, + }, + { + headers: { + control: `snapshot-end`, + xmin: `100`, + xmax: `110`, + xip_list: [], + }, + }, + { + headers: { control: `up-to-date` }, + }, + ]) + + // Explicit txid should resolve + await expect(testCollection.utils.awaitTxId(500)).resolves.toBe(true) + + // Snapshot txid should also resolve + await expect(testCollection.utils.awaitTxId(105)).resolves.toBe(true) + }) + it(`should resync after garbage collection and new subscription`, () => { // Use fake timers for this test vi.useFakeTimers() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1fb31265..7d4e38307 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,10 +197,10 @@ importers: version: 5.90.2 '@tanstack/query-db-collection': specifier: ^0.2.25 - version: link:../../../packages/query-db-collection + version: 0.2.25(@tanstack/query-core@5.90.2)(typescript@5.9.3) '@tanstack/react-db': specifier: ^0.1.26 - version: link:../../../packages/react-db + version: 0.1.26(react@19.2.0)(typescript@5.9.3) '@tanstack/react-router': specifier: ^1.132.41 version: 1.132.41(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -451,16 +451,16 @@ importers: dependencies: '@tanstack/electric-db-collection': specifier: ^0.1.28 - version: link:../../../packages/electric-db-collection + version: 0.1.28(typescript@5.9.3) '@tanstack/query-core': specifier: ^5.90.2 version: 5.90.2 '@tanstack/query-db-collection': specifier: ^0.2.25 - version: link:../../../packages/query-db-collection + version: 0.2.25(@tanstack/query-core@5.90.2)(typescript@5.9.3) '@tanstack/solid-db': specifier: ^0.1.26 - version: link:../../../packages/solid-db + version: 0.1.26(solid-js@1.9.9)(typescript@5.9.3) '@tanstack/solid-router': specifier: ^1.132.41 version: 1.132.41(solid-js@1.9.9) @@ -469,7 +469,7 @@ importers: version: 1.132.43(@tanstack/react-router@1.132.41(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(solid-js@1.9.9)(vite-plugin-solid@2.11.9(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@6.3.6(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@6.3.6(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.6)(yaml@2.8.1)) '@tanstack/trailbase-db-collection': specifier: ^0.1.26 - version: link:../../../packages/trailbase-db-collection + version: 0.1.26(typescript@5.9.3) cors: specifier: ^2.8.5 version: 2.8.5 @@ -478,7 +478,7 @@ importers: version: 0.44.6(@types/pg@8.15.5)(kysely@0.28.7)(pg@8.16.3)(postgres@3.4.7) drizzle-zod: specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.44.6(@types/pg@8.15.5)(kysely@0.28.7)(pg@8.16.3)(postgres@3.4.7))(zod@3.25.76) + version: 0.8.3(drizzle-orm@0.44.6(@types/pg@8.15.5)(kysely@0.28.7)(pg@8.16.3)(postgres@3.4.7))(zod@4.1.11) express: specifier: ^4.21.2 version: 4.21.2 @@ -3395,12 +3395,25 @@ packages: resolution: {integrity: sha512-RDz7vI1FSkocPda882nhEQoshU5F2bB5hTV/gXtB6krm/LTqMpK18ngvKtI1gbSd2RbsKCFtpJxqag3lvPgzgg==} engines: {node: '>=18'} + '@tanstack/db-ivm@0.1.9': + resolution: {integrity: sha512-EfrtTn34SQ6HOw1ZI4O1DKsrCSCzMufIYMpAYlO6kE7/DMZ7M9N8tQdXYyN2ZasL0zR8zjB1AHj4tMqcmPIt7w==} + peerDependencies: + typescript: '>=4.7' + + '@tanstack/db@0.4.4': + resolution: {integrity: sha512-G+abJtW6jBjAwMSgbaSYuwUJFUaxTY7kb+PyT3Y6r2pr795v+QQNHe6pniQeqrLp3M5nnx3rws+XfeqoDgN/VQ==} + peerDependencies: + typescript: '>=4.7' + '@tanstack/directive-functions-plugin@1.132.42': resolution: {integrity: sha512-GIPaal17gt/huxIJ3k5nsNkKHR/vv+PMtNtwsZYNT0aw27bXwPsLoNiFE+L+qjmm8hg4o0uPRzL2yKs57W8Oow==} engines: {node: '>=12'} peerDependencies: vite: '>=6.0.0 || >=7.0.0' + '@tanstack/electric-db-collection@0.1.28': + resolution: {integrity: sha512-5ZuGhgDN5QivpWpze159NeYjNveo+VaIBLiCmkW+g0zaU7mcrudMTzE4j4p8moJrzu2q2vfSBEJteLgeWwJGSQ==} + '@tanstack/eslint-config@0.3.2': resolution: {integrity: sha512-2g+PuGR3GuvvCiR3xZs+IMqAvnYU9bvH+jRml0BFBSxHBj22xFCTNvJWhvgj7uICFF9IchDkFUto91xDPMu5cg==} engines: {node: '>=18'} @@ -3416,6 +3429,17 @@ packages: '@tanstack/query-core@5.90.2': resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + '@tanstack/query-db-collection@0.2.25': + resolution: {integrity: sha512-Io07hIX7VLg7cO2/+Y6c9Bf2Y8xWtxt31i2lLswW2lx0sTZvK6sIpLvld8SxbMeRJHrwtXra++V0O6a1Y2ALGA==} + peerDependencies: + '@tanstack/query-core': ^5.0.0 + typescript: '>=4.7' + + '@tanstack/react-db@0.1.26': + resolution: {integrity: sha512-8itSp4bd+PU7/yImMn3eAcMq2AEzDVuPhJ2K5Pyqh7f9qhXlMDdxcm1K9BpZpaQJOJEcLYls6FDnaNNkyc5/hQ==} + peerDependencies: + react: '>=16.8.0' + '@tanstack/react-query@5.90.2': resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} peerDependencies: @@ -3523,6 +3547,11 @@ packages: resolution: {integrity: sha512-wwEuLlh7ty+L6jGV8FXKO50MMpDXtpAdDdrjfHBl4ekRQUFbegkpRRYFiYwUVz3ZDVZSypsKGgvdJzyxo+W/5w==} engines: {node: '>=12'} + '@tanstack/solid-db@0.1.26': + resolution: {integrity: sha512-m+rYZxvfeU1royIUTU2E5vkJNzezKVP6ag9gts8vFcN+k70NRxgAQQBih/NxDigK3nLGacOescb5RiB/KvkNcA==} + peerDependencies: + solid-js: '>=1.9.0' + '@tanstack/solid-router@1.132.41': resolution: {integrity: sha512-fZzTQyZD0xYTCPBEp0zZmrQjNMEbocwt6QhwT1tX+Uo2eVqlPHIm7kf5yaT9IhcVZvwFwIzPLvBxqUvGGQANZA==} engines: {node: '>=12'} @@ -3577,6 +3606,11 @@ packages: '@tanstack/store@0.7.7': resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tanstack/trailbase-db-collection@0.1.26': + resolution: {integrity: sha512-UjtldOrm/8+rCrEL3gKA6/lx19R4QEBwU9lVCuOOC8VYXhCFEgVQt3dXy8mvUM/LVGujDyi0fUDv9LXrLgUcig==} + peerDependencies: + typescript: '>=4.7' + '@tanstack/typedoc-config@0.2.1': resolution: {integrity: sha512-3miLBNiyWX54bQKBNnh7Fj6otWX8ZDiU6/ffOsNnikwBdKjFkA7ddrBtC5/JQkLCE6CBIqcJvtNIwI+DZu4y1Q==} engines: {node: '>=18'} @@ -11146,6 +11180,18 @@ snapshots: - typescript - vite + '@tanstack/db-ivm@0.1.9(typescript@5.9.3)': + dependencies: + fractional-indexing: 3.2.0 + sorted-btree: 1.8.1 + typescript: 5.9.3 + + '@tanstack/db@0.4.4(typescript@5.9.3)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@tanstack/db-ivm': 0.1.9(typescript@5.9.3) + typescript: 5.9.3 + '@tanstack/directive-functions-plugin@1.132.42(vite@6.3.6(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/code-frame': 7.27.1 @@ -11174,6 +11220,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/electric-db-collection@0.1.28(typescript@5.9.3)': + dependencies: + '@electric-sql/client': 1.0.14 + '@standard-schema/spec': 1.0.0 + '@tanstack/db': 0.4.4(typescript@5.9.3) + '@tanstack/store': 0.7.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + - typescript + '@tanstack/eslint-config@0.3.2(@typescript-eslint/utils@8.45.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint/js': 9.37.0 @@ -11203,6 +11260,21 @@ snapshots: '@tanstack/query-core@5.90.2': {} + '@tanstack/query-db-collection@0.2.25(@tanstack/query-core@5.90.2)(typescript@5.9.3)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@tanstack/db': 0.4.4(typescript@5.9.3) + '@tanstack/query-core': 5.90.2 + typescript: 5.9.3 + + '@tanstack/react-db@0.1.26(react@19.2.0)(typescript@5.9.3)': + dependencies: + '@tanstack/db': 0.4.4(typescript@5.9.3) + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + transitivePeerDependencies: + - typescript + '@tanstack/react-query@5.90.2(react@19.2.0)': dependencies: '@tanstack/query-core': 5.90.2 @@ -11437,6 +11509,14 @@ snapshots: - supports-color - vite + '@tanstack/solid-db@0.1.26(solid-js@1.9.9)(typescript@5.9.3)': + dependencies: + '@solid-primitives/map': 0.7.2(solid-js@1.9.9) + '@tanstack/db': 0.4.4(typescript@5.9.3) + solid-js: 1.9.9 + transitivePeerDependencies: + - typescript + '@tanstack/solid-router@1.132.41(solid-js@1.9.9)': dependencies: '@solid-devtools/logger': 0.9.11(solid-js@1.9.9) @@ -11585,6 +11665,17 @@ snapshots: '@tanstack/store@0.7.7': {} + '@tanstack/trailbase-db-collection@0.1.26(typescript@5.9.3)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@tanstack/db': 0.4.4(typescript@5.9.3) + '@tanstack/store': 0.7.7 + debug: 4.4.3 + trailbase: 0.7.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@tanstack/typedoc-config@0.2.1(typescript@5.9.3)': dependencies: typedoc: 0.27.9(typescript@5.9.3) @@ -11772,7 +11863,7 @@ snapshots: '@types/pg@8.15.5': dependencies: - '@types/node': 24.6.2 + '@types/node': 22.18.8 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -12963,11 +13054,6 @@ snapshots: pg: 8.16.3 postgres: 3.4.7 - drizzle-zod@0.8.3(drizzle-orm@0.44.6(@types/pg@8.15.5)(kysely@0.28.7)(pg@8.16.3)(postgres@3.4.7))(zod@3.25.76): - dependencies: - drizzle-orm: 0.44.6(@types/pg@8.15.5)(kysely@0.28.7)(pg@8.16.3)(postgres@3.4.7) - zod: 3.25.76 - drizzle-zod@0.8.3(drizzle-orm@0.44.6(@types/pg@8.15.5)(kysely@0.28.7)(pg@8.16.3)(postgres@3.4.7))(zod@4.1.11): dependencies: drizzle-orm: 0.44.6(@types/pg@8.15.5)(kysely@0.28.7)(pg@8.16.3)(postgres@3.4.7)