Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/persisted-metadata-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/db': patch
'@tanstack/db-sqlite-persisted-collection-core': patch
'@tanstack/electric-db-collection': patch
---

fix(persistence): harden persisted startup, truncate metadata semantics, and resume identity matching

- Restore persisted wrapper `markReady` fallback behavior so startup failures do not leave collections stuck in loading state
- Replace load cancellation reference identity tracking with deterministic load keys for `loadSubset` / `unloadSubset`
- Document intentional truncate behavior where collection-scoped metadata writes are preserved across truncate transactions
- Tighten SQLite `applied_tx` migration handling to only ignore duplicate-column add errors
- Stabilize Electric shape identity serialization so persisted resume compatibility does not depend on object key insertion order
48 changes: 48 additions & 0 deletions packages/db-electron-sqlite-persisted-collection/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ type ElectronMainPersistenceAdapter = PersistenceAdapter<
ElectronPersistedRow,
ElectronPersistedKey
> & {
loadCollectionMetadata?: (
collectionId: string,
) => Promise<Array<{ key: string; value: unknown }>>
scanRows?: (
collectionId: string,
options?: { metadataOnly?: boolean },
) => Promise<
Array<{
key: ElectronPersistedKey
value: ElectronPersistedRow
metadata?: unknown
}>
>
pullSince?: (
collectionId: string,
fromRowVersion: number,
Expand Down Expand Up @@ -110,6 +123,41 @@ async function executeRequestAgainstAdapter(
}
}

case `loadCollectionMetadata`: {
if (!adapter.loadCollectionMetadata) {
throw new InvalidPersistedCollectionConfigError(
`loadCollectionMetadata is not supported by the configured electron persistence adapter`,
)
}
const result = await adapter.loadCollectionMetadata(request.collectionId)
return {
v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
requestId: request.requestId,
method: request.method,
ok: true,
result,
}
}

case `scanRows`: {
if (!adapter.scanRows) {
throw new InvalidPersistedCollectionConfigError(
`scanRows is not supported by the configured electron persistence adapter`,
)
}
const result = await adapter.scanRows(
request.collectionId,
request.payload.options,
)
return {
v: ELECTRON_PERSISTENCE_PROTOCOL_VERSION,
requestId: request.requestId,
method: request.method,
ok: true,
result,
}
}

case `applyCommittedTx`: {
await adapter.applyCommittedTx(request.collectionId, request.payload.tx)
return {
Expand Down
14 changes: 14 additions & 0 deletions packages/db-electron-sqlite-persisted-collection/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type ElectronPersistenceResolution = {

export type ElectronPersistenceMethod =
| `loadSubset`
| `loadCollectionMetadata`
| `scanRows`
| `applyCommittedTx`
| `ensureIndex`
| `markIndexRemoved`
Expand All @@ -30,6 +32,12 @@ export type ElectronPersistencePayloadMap = {
options: LoadSubsetOptions
ctx?: { requiredIndexSignatures?: ReadonlyArray<string> }
}
loadCollectionMetadata: {}
scanRows: {
options?: {
metadataOnly?: boolean
}
}
applyCommittedTx: {
tx: PersistedTx<ElectronPersistedRow, ElectronPersistedKey>
}
Expand All @@ -48,6 +56,12 @@ export type ElectronPersistencePayloadMap = {

export type ElectronPersistenceResultMap = {
loadSubset: Array<{ key: ElectronPersistedKey; value: ElectronPersistedRow }>
loadCollectionMetadata: Array<{ key: string; value: unknown }>
scanRows: Array<{
key: ElectronPersistedKey
value: ElectronPersistedRow
metadata?: unknown
}>
applyCommittedTx: null
ensureIndex: null
markIndexRemoved: null
Expand Down
29 changes: 29 additions & 0 deletions packages/db-electron-sqlite-persisted-collection/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ type ElectronRendererResolvedAdapter<
T extends object,
TKey extends string | number = string | number,
> = PersistedCollectionPersistence<T, TKey>[`adapter`] & {
loadCollectionMetadata: (
collectionId: string,
) => Promise<Array<{ key: string; value: unknown }>>
scanRows: (
collectionId: string,
options?: { metadataOnly?: boolean },
) => Promise<Array<{ key: TKey; value: T; metadata?: unknown }>>
pullSince: (
collectionId: string,
fromRowVersion: number,
Expand Down Expand Up @@ -202,6 +209,28 @@ function createResolvedRendererAdapter<
resolution,
)
},
loadCollectionMetadata: async (
collectionId: string,
): Promise<Array<{ key: string; value: unknown }>> => {
return executeRequest(
`loadCollectionMetadata`,
collectionId,
{},
resolution,
)
},
scanRows: async (
collectionId: string,
options?: { metadataOnly?: boolean },
): Promise<Array<{ key: TKey; value: T; metadata?: unknown }>> => {
const result = await executeRequest(
`scanRows`,
collectionId,
{ options },
resolution,
)
return result as Array<{ key: TKey; value: T; metadata?: unknown }>
},
ensureIndex: async (
collectionId: string,
signature: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ test(`renderer persistence requires invoke transport`, () => {
latestRowVersion: 0,
},
})
case `loadCollectionMetadata`:
return Promise.resolve({
v: 1,
requestId: request.requestId,
method: request.method,
ok: true,
result: [],
})
case `scanRows`:
return Promise.resolve({
v: 1,
requestId: request.requestId,
method: request.method,
ok: true,
result: [],
})
default:
return Promise.resolve({
v: 1,
Expand Down
Loading
Loading