Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions .changeset/expo-refresh-db-upgrades.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@party-stack/better-auth": patch
"@party-stack/blobs": patch
"@party-stack/connections": patch
"@party-stack/db-indexeddb-persistence": patch
"@party-stack/durable-object-ontology": patch
"@party-stack/expo-runtime": patch
"@party-stack/foundry-ontology": minor
"@party-stack/node-runtime": patch
"@party-stack/oauth": patch
"@party-stack/ontology": minor
"@party-stack/ontology-devtools": patch
"@party-stack/remote-ontology": patch
"@party-stack/runtime": patch
"@party-stack/salesforce-ontology": patch
"@party-stack/sqlite-ontology": patch
"@party-stack/web-runtime": patch
---

Align Expo runtime dependencies on SDK 57 and provide a required Expo Blob/File polyfill. Hash Expo storage names, and verify idempotent cleanup closes SQLite before deletion.

Expose optional awaitable collection refresh utilities and implement full Foundry snapshot refresh, preserving subscriptions and propagating failures. Use fresh committed snapshots for operation acknowledgement when edit history is unavailable.

Upgrade TanStack DB and integrations to their latest releases and adapt persistence adapters and coordination to explicit storage targets.
18 changes: 18 additions & 0 deletions .changeset/indexeddb-managed-cache-generations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@party-stack/db-indexeddb-persistence": minor
---

Implement managed on-demand cache generations with the same claim, rotation,
renewal, expiry, release, and private recovery semantics as upstream SQLite.
Validate claims transactionally on storage operations and collect retired data
only after its last live claim ends. Upgrade IndexedDB storage to version 4 while
preserving existing local-only/eager rows and metadata.

Derive adapter options from SQLiteCoreAdapterOptions without its driver and add
only databaseName. Remove unused notification callback options while retaining
automatic connection closing for database upgrades. Direct adapter operations
now require explicit eager or managed storage targets, matching upstream.

Port managed-cache contracts and receiving recovery histories from TanStack DB,
including the complete finite retired-storage matrix, and run them against
SQLite, fake IndexedDB, and native Chromium IndexedDB.
4 changes: 2 additions & 2 deletions apps/issue-tracker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"@party-stack/remote-ontology": "workspace:*",
"@party-stack/sqlite-ontology": "workspace:*",
"@party-stack/web-runtime": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/react-db": "0.5.5",
"@tanstack/db": "0.14.0",
"@tanstack/react-db": "0.5.9",
"@tanstack/react-devtools": "^0.10.9",
"@tanstack/react-router": "^1.168.7",
"@tanstack/react-start": "^1.167.12",
Expand Down
6 changes: 3 additions & 3 deletions apps/ontology-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"@base-ui/react": "^1.8.0",
"@party-stack/foundry-ontology": "workspace:*",
"@party-stack/ontology": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/db": "0.14.0",
"@tanstack/query-core": "^5.102.8",
"@tanstack/query-db-collection": "1.4.0",
"@tanstack/react-db": "0.5.5",
"@tanstack/query-db-collection": "1.4.4",
"@tanstack/react-db": "0.5.9",
"@tanstack/react-router": "^1.168.7",
"@tanstack/react-start": "^1.167.12",
"@tanstack/react-table": "^9.2.8",
Expand Down
6 changes: 3 additions & 3 deletions apps/remote-notes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"@party-stack/remote-ontology": "workspace:*",
"@party-stack/sqlite-ontology": "workspace:*",
"@party-stack/web-runtime": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/db": "0.14.0",
"@tanstack/query-core": "^5.102.8",
"@tanstack/query-db-collection": "1.4.0",
"@tanstack/react-db": "0.5.5",
"@tanstack/query-db-collection": "1.4.4",
"@tanstack/react-db": "0.5.9",
"@tanstack/react-devtools": "^0.10.9",
"@tanstack/react-router": "^1.168.7",
"@tanstack/react-start": "^1.167.12",
Expand Down
4 changes: 2 additions & 2 deletions apps/salesforce-task-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"@party-stack/ontology-devtools": "workspace:*",
"@party-stack/salesforce-ontology": "workspace:*",
"@party-stack/web-runtime": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/react-db": "0.5.5",
"@tanstack/db": "0.14.0",
"@tanstack/react-db": "0.5.9",
"@tanstack/react-devtools": "^0.10.9",
"@tanstack/react-router": "^1.168.7",
"@tanstack/react-start": "^1.167.12",
Expand Down
4 changes: 2 additions & 2 deletions packages/better-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
},
"devDependencies": {
"@party-stack/build-config": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/db": "0.14.0",
"better-auth": "^1.7.1",
"typescript": "~7.0.2",
"vite-plus": "catalog:"
},
"peerDependencies": {
"@tanstack/db": "0.12.1",
"@tanstack/db": "0.14.0",
"better-auth": "^1.7.1"
}
}
4 changes: 2 additions & 2 deletions packages/blobs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
},
"devDependencies": {
"@party-stack/build-config": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/db": "0.14.0",
"typescript": "~7.0.2",
"vite-plus": "catalog:"
},
"peerDependencies": {
"@tanstack/db": "0.12.1"
"@tanstack/db": "0.14.0"
}
}
4 changes: 2 additions & 2 deletions packages/connections/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
},
"devDependencies": {
"@party-stack/build-config": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/db": "0.14.0",
"typescript": "~7.0.2",
"vite-plus": "catalog:"
},
"peerDependencies": {
"@tanstack/db": "0.12.1"
"@tanstack/db": "0.14.0"
}
}
37 changes: 33 additions & 4 deletions packages/db-indexeddb-persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ for shared options, schema versioning, coordination, and lifecycle behavior.
## Installation

```sh
pnpm add @party-stack/db-indexeddb-persistence @tanstack/db@0.12.1 @tanstack/db-sqlite-persistence-core@0.4.5
pnpm add @party-stack/db-indexeddb-persistence @tanstack/db@0.14.0 @tanstack/db-sqlite-persistence-core@0.4.9
```

Requires IndexedDB. The current release uses persistence core 0.4.5 and expects
TanStack DB 0.12.1.
Requires IndexedDB. The current release uses persistence core 0.4.9 and expects
TanStack DB 0.14.0.

## Persist a Query Collection

Expand All @@ -23,7 +23,7 @@ restores the persisted data.
For this example, also install the Query Collection adapter and TanStack Query:

```sh
pnpm add @tanstack/query-db-collection@1.4.0 @tanstack/query-core@^5.102.8
pnpm add @tanstack/query-db-collection@1.4.4 @tanstack/query-core@^5.102.8
```

```ts
Expand Down Expand Up @@ -188,3 +188,32 @@ mobile OS process-kill behavior.

See [the contract audit and measurements](persistence-contract-audit.md) for
coverage and limits.

## Managed on-demand caches

Synced collections with `syncMode: "on-demand"` use the same managed cache
protocol as upstream SQLite. A logical collection such as `tasks` maps to a
physical cache generation. Each active sync run claims that generation; recovery
can select fresh storage while delayed work remains bound to the older one.
Valid peers can continue using a retired generation until their claims end.
Retired rows, indexes, metadata, and transaction history are collected when no
live claims remain. The current shared generation remains available without
active claims; an expired claimant recovers privately without displacing it.

`IndexedDBPersistenceAdapterOptions` extends
`Omit<SQLiteCoreAdapterOptions, "driver">`, adding only `databaseName`.
`createIndexedDBPersistence` also accepts the existing optional `coordinator`.
`cacheGenerationClaimTtlMs` defaults to five minutes and `now` defaults to
`Date.now`, matching SQLite. Upstream's persistence wrapper renews live claims
and handles source restart and recovery. Database upgrades automatically close
older connections when requested by another connection.

Direct adapter calls require explicit `eagerStorageTarget(collectionId)` or
`managedStorageTarget(storageCollectionId, claimId)` targets, as upstream does.
The wrapper supplies these automatically. An eager target describes direct
storage access; it does not require the collection's sync mode to be eager.

The database upgrades from version 3 to 4 without clearing existing data.
Managed caches initially use fresh physical IDs rather than trusting legacy
unclaimed storage. Local-only/eager rows, including pending actions, remain
accessible at their original IDs.
8 changes: 4 additions & 4 deletions packages/db-indexeddb-persistence/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
}
},
"dependencies": {
"@tanstack/db-sqlite-persistence-core": "0.4.5",
"@tanstack/db-sqlite-persistence-core": "0.4.9",
"idb": "^8.0.3",
"temporal-polyfill": "^0.3.0"
},
"devDependencies": {
"@party-stack/build-config": "workspace:*",
"@tanstack/db": "0.12.1",
"@tanstack/node-db-sqlite-persistence": "0.2.30",
"@tanstack/db": "0.14.0",
"@tanstack/node-db-sqlite-persistence": "0.2.34",
"@types/better-sqlite3": "^7.6.13",
"@vitest/browser-playwright": "catalog:",
"better-sqlite3": "^12.9.0",
Expand All @@ -30,6 +30,6 @@
"vitest": "catalog:"
},
"peerDependencies": {
"@tanstack/db": "0.12.1"
"@tanstack/db": "0.14.0"
}
}
15 changes: 15 additions & 0 deletions packages/db-indexeddb-persistence/persistence-contract-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,18 @@ persistence requires a supplied cross-tab coordinator for notifications. Manual
storage edits remain unsupported; they bypass notification, index, and replay
maintenance. Temporal query literals still have upstream remote-subset wire
restrictions, separate from row serialization.

## Managed cache update — 2026-10-14

The adapter now implements upstream core 0.4.9's complete managed cache lifecycle:
claim, rotate, renew, release, clock, transactional claim validation, and retired
storage collection. Shared options derive from the core interface. Targets are
explicit and database version 4 preserves legacy eager/local-only data.

The additional source pin, ported test inventory, backend-specific adaptations,
and exclusions are documented in [the contract notice](src/contracts/NOTICE.md).
Managed contracts run against SQLite as a control, fake IndexedDB, and native
Chromium IndexedDB. IndexedDB-specific race histories additionally cover separate
connections, revoked/expired claims at registration and access boundaries, and
schema-upgrade preservation. Test-only contract modules are excluded from
published library outputs.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
not,
or,
} from "@tanstack/db";
import { eagerStorageTarget } from "@tanstack/db-sqlite-persistence-core";
import { afterAll, describe, test } from "vite-plus/test";
import { IndexedDBPersistenceAdapter } from "./IndexedDBPersistenceAdapter.js";
import type { LoadSubsetOptions } from "@tanstack/db";
Expand Down Expand Up @@ -121,12 +122,12 @@ const prefixLike = queryOptions((query) => query.where(({ item }) => like(item.n
const adapter = new IndexedDBPersistenceAdapter({
databaseName: `runtime-browser-bench-${crypto.randomUUID()}`,
});
await adapter.applyCommittedTx("items", seedTransaction(buildRows()));
await adapter.applyCommittedTx(eagerStorageTarget("items"), seedTransaction(buildRows()));
await Promise.all([
adapter.ensureIndex("items", "category-index", indexSpec("category")),
adapter.ensureIndex("items", "priority-index", indexSpec("priority")),
adapter.ensureIndex("items", "name-index", indexSpec("name")),
adapter.ensureIndex("items", "status-index", indexSpec("status")),
adapter.ensureIndex(eagerStorageTarget("items"), "category-index", indexSpec("category")),
adapter.ensureIndex(eagerStorageTarget("items"), "priority-index", indexSpec("priority")),
adapter.ensureIndex(eagerStorageTarget("items"), "name-index", indexSpec("name")),
adapter.ensureIndex(eagerStorageTarget("items"), "status-index", indexSpec("status")),
]);

const expectedCounts: Array<[string, LoadSubsetOptions, number]> = [
Expand All @@ -140,7 +141,7 @@ const expectedCounts: Array<[string, LoadSubsetOptions, number]> = [
["LIKE", prefixLike, 500],
];
for (const [name, options, expected] of expectedCounts) {
const rows = await adapter.loadSubset("items", options);
const rows = await adapter.loadSubset(eagerStorageTarget("items"), options);
if (rows.length !== expected) {
throw new Error(`${name} expected ${expected} candidate rows, received ${rows.length}.`);
}
Expand All @@ -149,49 +150,49 @@ for (const [name, options, expected] of expectedCounts) {
describe("IndexedDBPersistenceAdapter browser query planning", () => {
test("full collection fallback: 10,000 rows loaded", async ({ bench }) => {
await bench("full collection fallback: 10,000 rows loaded", async () => {
await adapter.loadSubset("items", unplannableOr);
await adapter.loadSubset(eagerStorageTarget("items"), unplannableOr);
}).run(BENCH_OPTIONS);
});

test("equality index: 100 entries scanned / rows loaded", async ({ bench }) => {
await bench("equality index: 100 entries scanned / rows loaded", async () => {
await adapter.loadSubset("items", categoryEquals);
await adapter.loadSubset(eagerStorageTarget("items"), categoryEquals);
}).run(BENCH_OPTIONS);
});

test("range index: 1,000 entries scanned / rows loaded", async ({ bench }) => {
await bench("range index: 1,000 entries scanned / rows loaded", async () => {
await adapter.loadSubset("items", highPriority);
await adapter.loadSubset(eagerStorageTarget("items"), highPriority);
}).run(BENCH_OPTIONS);
});

test("indexed AND: 1,100 entries scanned / 10 rows loaded", async ({ bench }) => {
await bench("indexed AND: 1,100 entries scanned / 10 rows loaded", async () => {
await adapter.loadSubset("items", indexedAnd);
await adapter.loadSubset(eagerStorageTarget("items"), indexedAnd);
}).run(BENCH_OPTIONS);
});

test("indexed OR: 1,100 entries scanned / 1,090 rows loaded", async ({ bench }) => {
await bench("indexed OR: 1,100 entries scanned / 1,090 rows loaded", async () => {
await adapter.loadSubset("items", indexedOr);
await adapter.loadSubset(eagerStorageTarget("items"), indexedOr);
}).run(BENCH_OPTIONS);
});

test("AND with residual NOT: 100 entries scanned / rows loaded", async ({ bench }) => {
await bench("AND with residual NOT: 100 entries scanned / rows loaded", async () => {
await adapter.loadSubset("items", residualAnd);
await adapter.loadSubset(eagerStorageTarget("items"), residualAnd);
}).run(BENCH_OPTIONS);
});

test("IN with three values: 300 entries scanned / rows loaded", async ({ bench }) => {
await bench("IN with three values: 300 entries scanned / rows loaded", async () => {
await adapter.loadSubset("items", categoryIn);
await adapter.loadSubset(eagerStorageTarget("items"), categoryIn);
}).run(BENCH_OPTIONS);
});

test("prefix LIKE: 500 entries scanned / rows loaded", async ({ bench }) => {
await bench("prefix LIKE: 500 entries scanned / rows loaded", async () => {
await adapter.loadSubset("items", prefixLike);
await adapter.loadSubset(eagerStorageTarget("items"), prefixLike);
}).run(BENCH_OPTIONS);
});
});
Expand All @@ -201,37 +202,37 @@ describe("IndexedDBPersistenceAdapter browser query planning", () => {
const fallbackAdapter = new IndexedDBPersistenceAdapter({
databaseName: `bench-fallback-${crypto.randomUUID()}`,
});
await fallbackAdapter.applyCommittedTx("items", seedTransaction(buildRows()));
await fallbackAdapter.applyCommittedTx(eagerStorageTarget("items"), seedTransaction(buildRows()));
const page = queryOptions((query) => query.orderBy(({ item }) => item.priority, "asc").limit(10));
const emptyAnd = queryOptions((query) =>
query.where(({ item }) => and(eq(item.category, "absent"), gte(item.priority, 900)))
);
const indexedPage = await adapter.loadSubset("items", page);
const fallbackPage = await fallbackAdapter.loadSubset("items", page);
const indexedPage = await adapter.loadSubset(eagerStorageTarget("items"), page);
const fallbackPage = await fallbackAdapter.loadSubset(eagerStorageTarget("items"), page);
if (indexedPage.length !== 10 || JSON.stringify(indexedPage) !== JSON.stringify(fallbackPage))
throw new Error("Pagination benchmark paths disagree");
let writeVersion = 1;
describe("IndexedDB persistence pagination and incremental updates", () => {
test("ordered index: first 10 of 10,000 rows", async ({ bench }) => {
await bench("ordered index: first 10 of 10,000 rows", async () => {
await adapter.loadSubset("items", page);
await adapter.loadSubset(eagerStorageTarget("items"), page);
}).run(BENCH_OPTIONS);
});
test("unindexed fallback: same first 10 of 10,000 rows", async ({ bench }) => {
await bench("unindexed fallback: same first 10 of 10,000 rows", async () => {
await fallbackAdapter.loadSubset("items", page);
await fallbackAdapter.loadSubset(eagerStorageTarget("items"), page);
}).run(BENCH_OPTIONS);
});
test("empty AND: absent category intersected with 1,000 priorities", async ({ bench }) => {
await bench("empty AND: absent category intersected with 1,000 priorities", async () => {
if ((await adapter.loadSubset("items", emptyAnd)).length !== 0)
if ((await adapter.loadSubset(eagerStorageTarget("items"), emptyAnd)).length !== 0)
throw new Error("Empty intersection returned rows");
}).run(BENCH_OPTIONS);
});
test("one-row partial update with four maintained indexes", async ({ bench }) => {
await bench("one-row partial update with four maintained indexes", async () => {
const version = ++writeVersion;
await adapter.applyCommittedTx("items", {
await adapter.applyCommittedTx(eagerStorageTarget("items"), {
txId: `update-${version}`,
term: 1,
seq: version,
Expand Down
Loading
Loading