@tanstack/db-sqlite-persistence-core 0.4.8 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/cjs/broadcast-coordinator.cjs +117 -29
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
- package/dist/cjs/broadcast-coordinator.d.cts +16 -8
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +1408 -278
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +62 -15
- package/dist/cjs/remote-subset-wire.cjs +11 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.d.cts +1 -0
- package/dist/cjs/sqlite-core-adapter.cjs +623 -73
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +46 -12
- package/dist/esm/broadcast-coordinator.d.ts +16 -8
- package/dist/esm/broadcast-coordinator.js +118 -30
- package/dist/esm/broadcast-coordinator.js.map +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/persisted.d.ts +62 -15
- package/dist/esm/persisted.js +1409 -279
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.d.ts +1 -0
- package/dist/esm/remote-subset-wire.js +11 -0
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.d.ts +46 -12
- package/dist/esm/sqlite-core-adapter.js +626 -76
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/broadcast-coordinator.ts +153 -23
- package/src/persisted.ts +1973 -322
- package/src/remote-subset-wire.ts +12 -0
- package/src/sqlite-core-adapter.ts +760 -77
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
IR,
|
|
3
3
|
compareTemporalValues,
|
|
4
4
|
compileSingleRowExpression,
|
|
5
|
+
safeRandomUUID,
|
|
5
6
|
toBooleanPredicate,
|
|
6
7
|
} from '@tanstack/db'
|
|
7
8
|
import {
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
createPersistedTableName,
|
|
15
16
|
decodePersistedStorageKey,
|
|
16
17
|
encodePersistedStorageKey,
|
|
18
|
+
resolvePersistedStorageTarget,
|
|
17
19
|
} from './persisted'
|
|
18
20
|
import {
|
|
19
21
|
PERSISTED_TYPE_TAG,
|
|
@@ -29,10 +31,12 @@ import type { LoadSubsetOptions } from '@tanstack/db'
|
|
|
29
31
|
import type {
|
|
30
32
|
CommittedTxAnchor,
|
|
31
33
|
HydrationPersistenceAdapter,
|
|
34
|
+
PersistedCacheGenerationClaim,
|
|
32
35
|
PersistedIndexSpec,
|
|
33
36
|
PersistedKeySetEvidence,
|
|
34
37
|
PersistedRowScanOptions,
|
|
35
38
|
PersistedScannedRow,
|
|
39
|
+
PersistedStorageTarget,
|
|
36
40
|
PersistedTx,
|
|
37
41
|
PersistenceAdapter,
|
|
38
42
|
ReconciledCommittedTx,
|
|
@@ -78,6 +82,10 @@ export type SQLiteCoreAdapterOptions = {
|
|
|
78
82
|
appliedTxPruneMaxRows?: number
|
|
79
83
|
appliedTxPruneMaxAgeSeconds?: number
|
|
80
84
|
pullSinceReloadThreshold?: number
|
|
85
|
+
/** Milliseconds without renewal before a sync run loses cache access. */
|
|
86
|
+
cacheGenerationClaimTtlMs?: number
|
|
87
|
+
/** Host clock, injectable for expiry histories. */
|
|
88
|
+
now?: () => number
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
export type SQLitePullSinceResult<TKey extends string | number> =
|
|
@@ -248,6 +256,17 @@ function observeSharedLogicalSchedulingSupport(
|
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
const DEFAULT_SCHEMA_VERSION = 1
|
|
259
|
+
const CACHE_GENERATION_ID_PREFIX = `tanstack-db-cache:`
|
|
260
|
+
const LEGACY_CACHE_GENERATION_ID_PREFIX = `\u0000tanstack-db-cache:`
|
|
261
|
+
|
|
262
|
+
function hasCacheGenerationIdPrefix(collectionId: string): boolean {
|
|
263
|
+
return (
|
|
264
|
+
collectionId.startsWith(CACHE_GENERATION_ID_PREFIX) ||
|
|
265
|
+
collectionId.startsWith(LEGACY_CACHE_GENERATION_ID_PREFIX)
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS = 5 * 60_000
|
|
251
270
|
const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128
|
|
252
271
|
|
|
253
272
|
/**
|
|
@@ -1622,6 +1641,8 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1622
1641
|
private readonly appliedTxPruneMaxAgeSeconds: number | undefined
|
|
1623
1642
|
private readonly pullSinceReloadThreshold: number
|
|
1624
1643
|
private readonly replacementBatchSize: number
|
|
1644
|
+
private readonly cacheGenerationClaimTtlMs: number
|
|
1645
|
+
private readonly now: () => number
|
|
1625
1646
|
|
|
1626
1647
|
private initialized = false
|
|
1627
1648
|
private readonly collectionTableCache = new Map<
|
|
@@ -1633,6 +1654,290 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1633
1654
|
Promise<CollectionTableMapping>
|
|
1634
1655
|
>()
|
|
1635
1656
|
|
|
1657
|
+
claimCacheGeneration(
|
|
1658
|
+
collectionId: string,
|
|
1659
|
+
): Promise<PersistedCacheGenerationClaim> {
|
|
1660
|
+
return this.runRegular(async () => {
|
|
1661
|
+
await this.ensureInitialized()
|
|
1662
|
+
return this.driver.transaction(async (driver) => {
|
|
1663
|
+
const expiresAtMs = this.now() + this.cacheGenerationClaimTtlMs
|
|
1664
|
+
// The first generation must not reuse the legacy collection ID: an older
|
|
1665
|
+
// adapter can still write that ID without a claim after an upgrade.
|
|
1666
|
+
const initialPhysicalId = `${CACHE_GENERATION_ID_PREFIX}${safeRandomUUID()}`
|
|
1667
|
+
await driver.run(
|
|
1668
|
+
`INSERT INTO cache_generation (logical_id, generation, physical_id, retired)
|
|
1669
|
+
SELECT ?, 0, ?, 0
|
|
1670
|
+
WHERE NOT EXISTS (
|
|
1671
|
+
SELECT 1 FROM cache_generation WHERE logical_id = ?
|
|
1672
|
+
)`,
|
|
1673
|
+
[collectionId, initialPhysicalId, collectionId],
|
|
1674
|
+
)
|
|
1675
|
+
const head = await driver.query<{ physical_id: string }>(
|
|
1676
|
+
`SELECT physical_id FROM cache_generation
|
|
1677
|
+
WHERE logical_id = ? AND retired = 0`,
|
|
1678
|
+
[collectionId],
|
|
1679
|
+
)
|
|
1680
|
+
const storageCollectionId = head[0]?.physical_id
|
|
1681
|
+
if (!storageCollectionId) {
|
|
1682
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1683
|
+
`No active persisted cache generation for collection "${collectionId}"`,
|
|
1684
|
+
)
|
|
1685
|
+
}
|
|
1686
|
+
const claimId = safeRandomUUID()
|
|
1687
|
+
await driver.run(
|
|
1688
|
+
`INSERT INTO cache_generation_claim
|
|
1689
|
+
(claim_id, logical_id, physical_id, expires_at_ms)
|
|
1690
|
+
VALUES (?, ?, ?, ?)`,
|
|
1691
|
+
[claimId, collectionId, storageCollectionId, expiresAtMs],
|
|
1692
|
+
)
|
|
1693
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1694
|
+
return { storageCollectionId, claimId, expiresAtMs }
|
|
1695
|
+
})
|
|
1696
|
+
})
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
rotateCacheGeneration(
|
|
1700
|
+
collectionId: string,
|
|
1701
|
+
claimId: string,
|
|
1702
|
+
resetMetadata?: { key: string; value: unknown },
|
|
1703
|
+
expectedStorageCollectionId?: string,
|
|
1704
|
+
): Promise<PersistedCacheGenerationClaim> {
|
|
1705
|
+
return this.runRegular(async () => {
|
|
1706
|
+
await this.ensureInitialized()
|
|
1707
|
+
return this.driver.transaction(async (driver) => {
|
|
1708
|
+
const now = this.now()
|
|
1709
|
+
const expiresAtMs = now + this.cacheGenerationClaimTtlMs
|
|
1710
|
+
const claimed = await driver.query<{
|
|
1711
|
+
physical_id: string
|
|
1712
|
+
expires_at_ms: number
|
|
1713
|
+
}>(
|
|
1714
|
+
`SELECT physical_id, expires_at_ms
|
|
1715
|
+
FROM cache_generation_claim
|
|
1716
|
+
WHERE claim_id = ? AND logical_id = ?`,
|
|
1717
|
+
[claimId, collectionId],
|
|
1718
|
+
)
|
|
1719
|
+
const claimedPhysicalId =
|
|
1720
|
+
claimed[0] && claimed[0].expires_at_ms > now
|
|
1721
|
+
? claimed[0].physical_id
|
|
1722
|
+
: undefined
|
|
1723
|
+
if (
|
|
1724
|
+
claimedPhysicalId &&
|
|
1725
|
+
expectedStorageCollectionId &&
|
|
1726
|
+
claimedPhysicalId !== expectedStorageCollectionId
|
|
1727
|
+
) {
|
|
1728
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1729
|
+
`Persisted cache claim changed generations before recovery`,
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
const head = await driver.query<{
|
|
1733
|
+
generation: number
|
|
1734
|
+
physical_id: string
|
|
1735
|
+
}>(
|
|
1736
|
+
`SELECT generation, physical_id FROM cache_generation
|
|
1737
|
+
WHERE logical_id = ? AND retired = 0`,
|
|
1738
|
+
[collectionId],
|
|
1739
|
+
)
|
|
1740
|
+
const active = head[0]
|
|
1741
|
+
if (!active) {
|
|
1742
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1743
|
+
`No active persisted cache generation for collection "${collectionId}"`,
|
|
1744
|
+
)
|
|
1745
|
+
}
|
|
1746
|
+
await driver.run(
|
|
1747
|
+
`INSERT INTO cache_generation_sequence (logical_id, next_generation)
|
|
1748
|
+
SELECT ?, COALESCE(MAX(generation), -1) + 1
|
|
1749
|
+
FROM cache_generation WHERE logical_id = ?
|
|
1750
|
+
ON CONFLICT(logical_id) DO NOTHING`,
|
|
1751
|
+
[collectionId, collectionId],
|
|
1752
|
+
)
|
|
1753
|
+
const nextRows = await driver.query<{ next_generation: number }>(
|
|
1754
|
+
`SELECT next_generation FROM cache_generation_sequence
|
|
1755
|
+
WHERE logical_id = ?`,
|
|
1756
|
+
[collectionId],
|
|
1757
|
+
)
|
|
1758
|
+
const nextGeneration = nextRows[0]!.next_generation
|
|
1759
|
+
await driver.run(
|
|
1760
|
+
`UPDATE cache_generation_sequence SET next_generation = ?
|
|
1761
|
+
WHERE logical_id = ?`,
|
|
1762
|
+
[nextGeneration + 1, collectionId],
|
|
1763
|
+
)
|
|
1764
|
+
// An expired claim no longer owns the head, even when it remembers
|
|
1765
|
+
// the current storage ID. Its recovery is private so another run's
|
|
1766
|
+
// valid current cache remains claimable.
|
|
1767
|
+
const ownedHead = claimedPhysicalId === active.physical_id
|
|
1768
|
+
const storageCollectionId = `${CACHE_GENERATION_ID_PREFIX}${safeRandomUUID()}`
|
|
1769
|
+
if (ownedHead) {
|
|
1770
|
+
await driver.run(
|
|
1771
|
+
`UPDATE cache_generation SET retired = 1
|
|
1772
|
+
WHERE logical_id = ? AND generation = ?`,
|
|
1773
|
+
[collectionId, active.generation],
|
|
1774
|
+
)
|
|
1775
|
+
}
|
|
1776
|
+
await driver.run(
|
|
1777
|
+
`INSERT INTO cache_generation (logical_id, generation, physical_id, retired)
|
|
1778
|
+
VALUES (?, ?, ?, ?)`,
|
|
1779
|
+
[
|
|
1780
|
+
collectionId,
|
|
1781
|
+
nextGeneration,
|
|
1782
|
+
storageCollectionId,
|
|
1783
|
+
ownedHead ? 0 : 1,
|
|
1784
|
+
],
|
|
1785
|
+
)
|
|
1786
|
+
const nextClaimId = claimed[0] ? claimId : safeRandomUUID()
|
|
1787
|
+
if (claimed[0]) {
|
|
1788
|
+
await driver.run(
|
|
1789
|
+
`UPDATE cache_generation_claim
|
|
1790
|
+
SET physical_id = ?, expires_at_ms = ? WHERE claim_id = ?`,
|
|
1791
|
+
[storageCollectionId, expiresAtMs, claimId],
|
|
1792
|
+
)
|
|
1793
|
+
} else {
|
|
1794
|
+
await driver.run(
|
|
1795
|
+
`INSERT INTO cache_generation_claim
|
|
1796
|
+
(claim_id, logical_id, physical_id, expires_at_ms)
|
|
1797
|
+
VALUES (?, ?, ?, ?)`,
|
|
1798
|
+
[nextClaimId, collectionId, storageCollectionId, expiresAtMs],
|
|
1799
|
+
)
|
|
1800
|
+
}
|
|
1801
|
+
await driver.run(
|
|
1802
|
+
`INSERT INTO collection_version (
|
|
1803
|
+
collection_id, latest_row_version,
|
|
1804
|
+
key_set_evidence_available, key_set_evidence_incompatible
|
|
1805
|
+
) VALUES (?, 0, 1, 1)`,
|
|
1806
|
+
[storageCollectionId],
|
|
1807
|
+
)
|
|
1808
|
+
if (resetMetadata) {
|
|
1809
|
+
await driver.run(
|
|
1810
|
+
`INSERT INTO collection_metadata (collection_id, key, value, updated_at)
|
|
1811
|
+
VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`,
|
|
1812
|
+
[
|
|
1813
|
+
storageCollectionId,
|
|
1814
|
+
resetMetadata.key,
|
|
1815
|
+
serializePersistedRowValue(resetMetadata.value),
|
|
1816
|
+
],
|
|
1817
|
+
)
|
|
1818
|
+
}
|
|
1819
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1820
|
+
return { storageCollectionId, claimId: nextClaimId, expiresAtMs }
|
|
1821
|
+
})
|
|
1822
|
+
})
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
releaseCacheGenerationClaim(claimId: string): Promise<void> {
|
|
1826
|
+
return this.runRegular(async () => {
|
|
1827
|
+
await this.ensureInitialized()
|
|
1828
|
+
await this.runInTransaction(async (driver) => {
|
|
1829
|
+
await driver.run(
|
|
1830
|
+
`DELETE FROM cache_generation_claim WHERE claim_id = ?`,
|
|
1831
|
+
[claimId],
|
|
1832
|
+
)
|
|
1833
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1834
|
+
})
|
|
1835
|
+
})
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
renewCacheGenerationClaim(
|
|
1839
|
+
storageCollectionId: string,
|
|
1840
|
+
claimId: string,
|
|
1841
|
+
): Promise<number | undefined> {
|
|
1842
|
+
return this.runRegular(() =>
|
|
1843
|
+
this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId),
|
|
1844
|
+
)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
private async renewCacheGenerationClaimUnscheduled(
|
|
1848
|
+
storageCollectionId: string,
|
|
1849
|
+
claimId: string,
|
|
1850
|
+
): Promise<number | undefined> {
|
|
1851
|
+
await this.ensureInitialized()
|
|
1852
|
+
return this.runInTransaction(async (driver) => {
|
|
1853
|
+
const now = this.now()
|
|
1854
|
+
const rows = await driver.query<{ claim_id: string }>(
|
|
1855
|
+
`SELECT claim_id FROM cache_generation_claim
|
|
1856
|
+
WHERE claim_id = ? AND physical_id = ? AND expires_at_ms > ?`,
|
|
1857
|
+
[claimId, storageCollectionId, now],
|
|
1858
|
+
)
|
|
1859
|
+
if (!rows[0]) {
|
|
1860
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1861
|
+
return undefined
|
|
1862
|
+
}
|
|
1863
|
+
const expiresAtMs = now + this.cacheGenerationClaimTtlMs
|
|
1864
|
+
await driver.run(
|
|
1865
|
+
`UPDATE cache_generation_claim SET expires_at_ms = ?
|
|
1866
|
+
WHERE claim_id = ?`,
|
|
1867
|
+
[expiresAtMs, claimId],
|
|
1868
|
+
)
|
|
1869
|
+
return expiresAtMs
|
|
1870
|
+
})
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
private async collectRetiredCacheGenerations(
|
|
1874
|
+
driver: SQLiteDriver,
|
|
1875
|
+
): Promise<void> {
|
|
1876
|
+
await driver.run(
|
|
1877
|
+
`DELETE FROM cache_generation_claim WHERE expires_at_ms <= ?`,
|
|
1878
|
+
[this.now()],
|
|
1879
|
+
)
|
|
1880
|
+
const retired = await driver.query<{
|
|
1881
|
+
physical_id: string
|
|
1882
|
+
table_name: string | null
|
|
1883
|
+
tombstone_table_name: string | null
|
|
1884
|
+
}>(
|
|
1885
|
+
`SELECT generation.physical_id, registry.table_name,
|
|
1886
|
+
registry.tombstone_table_name
|
|
1887
|
+
FROM cache_generation AS generation
|
|
1888
|
+
LEFT JOIN collection_registry AS registry
|
|
1889
|
+
ON registry.collection_id = generation.physical_id
|
|
1890
|
+
WHERE generation.retired = 1
|
|
1891
|
+
AND NOT EXISTS (
|
|
1892
|
+
SELECT 1 FROM cache_generation_claim AS claim
|
|
1893
|
+
WHERE claim.physical_id = generation.physical_id
|
|
1894
|
+
)`,
|
|
1895
|
+
)
|
|
1896
|
+
for (const generation of retired) {
|
|
1897
|
+
if (generation.table_name) {
|
|
1898
|
+
await driver.exec(
|
|
1899
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(generation.table_name)}`,
|
|
1900
|
+
)
|
|
1901
|
+
}
|
|
1902
|
+
if (generation.tombstone_table_name) {
|
|
1903
|
+
await driver.exec(
|
|
1904
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(generation.tombstone_table_name)}`,
|
|
1905
|
+
)
|
|
1906
|
+
}
|
|
1907
|
+
for (const table of [
|
|
1908
|
+
`applied_tx`,
|
|
1909
|
+
`collection_expected_keys`,
|
|
1910
|
+
`collection_metadata`,
|
|
1911
|
+
`persisted_index_registry`,
|
|
1912
|
+
`leader_term`,
|
|
1913
|
+
]) {
|
|
1914
|
+
await driver.run(`DELETE FROM ${table} WHERE collection_id = ?`, [
|
|
1915
|
+
generation.physical_id,
|
|
1916
|
+
])
|
|
1917
|
+
}
|
|
1918
|
+
await driver.run(
|
|
1919
|
+
`DELETE FROM collection_version WHERE collection_id = ?`,
|
|
1920
|
+
[generation.physical_id],
|
|
1921
|
+
)
|
|
1922
|
+
await driver.run(
|
|
1923
|
+
`DELETE FROM collection_reset_epoch WHERE collection_id = ?`,
|
|
1924
|
+
[generation.physical_id],
|
|
1925
|
+
)
|
|
1926
|
+
await driver.run(
|
|
1927
|
+
`DELETE FROM collection_registry WHERE collection_id = ?`,
|
|
1928
|
+
[generation.physical_id],
|
|
1929
|
+
)
|
|
1930
|
+
await driver.run(`DELETE FROM cache_generation WHERE physical_id = ?`, [
|
|
1931
|
+
generation.physical_id,
|
|
1932
|
+
])
|
|
1933
|
+
this.collectionTableCache.delete(generation.physical_id)
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
getCacheGenerationNow(): number {
|
|
1938
|
+
return this.now()
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1636
1941
|
constructor(options: SQLiteCoreAdapterOptions) {
|
|
1637
1942
|
const maxBoundParameters = options.driver.maxBoundParameters
|
|
1638
1943
|
if (
|
|
@@ -1648,6 +1953,18 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1648
1953
|
? REPLACEMENT_BATCH_SIZE
|
|
1649
1954
|
: Math.min(REPLACEMENT_BATCH_SIZE, Math.floor(maxBoundParameters / 4))
|
|
1650
1955
|
|
|
1956
|
+
this.cacheGenerationClaimTtlMs =
|
|
1957
|
+
options.cacheGenerationClaimTtlMs ?? DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS
|
|
1958
|
+
if (
|
|
1959
|
+
!Number.isSafeInteger(this.cacheGenerationClaimTtlMs) ||
|
|
1960
|
+
this.cacheGenerationClaimTtlMs <= 0
|
|
1961
|
+
) {
|
|
1962
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1963
|
+
`SQLite adapter cacheGenerationClaimTtlMs must be a positive safe integer`,
|
|
1964
|
+
)
|
|
1965
|
+
}
|
|
1966
|
+
this.now = options.now ?? Date.now
|
|
1967
|
+
|
|
1651
1968
|
const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION
|
|
1652
1969
|
if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
|
|
1653
1970
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -1716,20 +2033,22 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1716
2033
|
this.loadResumeSnapshotUnscheduled(collectionId, context),
|
|
1717
2034
|
applyCommittedTx: (collectionId, tx) =>
|
|
1718
2035
|
this.applyCommittedTxUnscheduled(collectionId, tx),
|
|
2036
|
+
renewCacheGenerationClaim: (storageCollectionId, claimId) =>
|
|
2037
|
+
this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId),
|
|
1719
2038
|
reconcileCommittedTx: (collectionId, tx, anchor) =>
|
|
1720
2039
|
this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
|
|
1721
|
-
loadCollectionMetadata: (collectionId) =>
|
|
1722
|
-
this.loadCollectionMetadataUnscheduled(collectionId),
|
|
1723
|
-
scanRows: (collectionId, scanOptions) =>
|
|
1724
|
-
this.scanRowsUnscheduled(collectionId, scanOptions),
|
|
1725
|
-
ensureIndex: (collectionId, signature, spec) =>
|
|
1726
|
-
this.ensureIndexUnscheduled(collectionId, signature, spec),
|
|
1727
|
-
markIndexRemoved: (collectionId, signature) =>
|
|
1728
|
-
this.markIndexRemovedUnscheduled(collectionId, signature),
|
|
1729
|
-
getStreamPosition: (collectionId) =>
|
|
1730
|
-
this.getStreamPositionUnscheduled(collectionId),
|
|
1731
|
-
pullSince: (collectionId, fromRowVersion) =>
|
|
1732
|
-
this.pullSinceUnscheduled(collectionId, fromRowVersion),
|
|
2040
|
+
loadCollectionMetadata: (collectionId, context) =>
|
|
2041
|
+
this.loadCollectionMetadataUnscheduled(collectionId, context),
|
|
2042
|
+
scanRows: (collectionId, scanOptions, context) =>
|
|
2043
|
+
this.scanRowsUnscheduled(collectionId, scanOptions, context),
|
|
2044
|
+
ensureIndex: (collectionId, signature, spec, ctx) =>
|
|
2045
|
+
this.ensureIndexUnscheduled(collectionId, signature, spec, ctx),
|
|
2046
|
+
markIndexRemoved: (collectionId, signature, ctx) =>
|
|
2047
|
+
this.markIndexRemovedUnscheduled(collectionId, signature, ctx),
|
|
2048
|
+
getStreamPosition: (collectionId, ctx) =>
|
|
2049
|
+
this.getStreamPositionUnscheduled(collectionId, ctx),
|
|
2050
|
+
pullSince: (collectionId, fromRowVersion, ctx) =>
|
|
2051
|
+
this.pullSinceUnscheduled(collectionId, fromRowVersion, ctx),
|
|
1733
2052
|
runInHydrationScope: async (task) => task(this.hydrationAdapter),
|
|
1734
2053
|
}
|
|
1735
2054
|
}
|
|
@@ -1809,10 +2128,48 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1809
2128
|
}
|
|
1810
2129
|
}
|
|
1811
2130
|
|
|
2131
|
+
private async assertCacheGenerationReadClaim(
|
|
2132
|
+
driver: SQLiteDriver,
|
|
2133
|
+
storageCollectionId: string,
|
|
2134
|
+
claimId?: string,
|
|
2135
|
+
): Promise<void> {
|
|
2136
|
+
await this.ensureInitialized()
|
|
2137
|
+
if (!claimId) {
|
|
2138
|
+
if (!hasCacheGenerationIdPrefix(storageCollectionId)) return
|
|
2139
|
+
const generation = await driver.query<{ physical_id: string }>(
|
|
2140
|
+
`SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
|
|
2141
|
+
[storageCollectionId],
|
|
2142
|
+
)
|
|
2143
|
+
if (generation[0]) {
|
|
2144
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2145
|
+
`Persisted cache claim is required for collection "${storageCollectionId}"`,
|
|
2146
|
+
)
|
|
2147
|
+
}
|
|
2148
|
+
return
|
|
2149
|
+
}
|
|
2150
|
+
const rows = await driver.query<{ claim_id: string }>(
|
|
2151
|
+
`SELECT claim.claim_id
|
|
2152
|
+
FROM cache_generation_claim AS claim
|
|
2153
|
+
JOIN cache_generation AS generation
|
|
2154
|
+
ON generation.physical_id = claim.physical_id
|
|
2155
|
+
WHERE claim.claim_id = ? AND claim.physical_id = ?
|
|
2156
|
+
AND claim.expires_at_ms > ?`,
|
|
2157
|
+
[claimId, storageCollectionId, this.now()],
|
|
2158
|
+
)
|
|
2159
|
+
if (!rows[0]) {
|
|
2160
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2161
|
+
`Persisted cache claim is no longer active for collection "${storageCollectionId}"`,
|
|
2162
|
+
)
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1812
2166
|
loadSubset(
|
|
1813
|
-
|
|
2167
|
+
target: PersistedStorageTarget,
|
|
1814
2168
|
options: LoadSubsetOptions,
|
|
1815
|
-
ctx?: {
|
|
2169
|
+
ctx?: {
|
|
2170
|
+
requiredIndexSignatures?: ReadonlyArray<string>
|
|
2171
|
+
cacheGenerationClaimId?: string
|
|
2172
|
+
},
|
|
1816
2173
|
): Promise<
|
|
1817
2174
|
Array<{
|
|
1818
2175
|
key: string | number
|
|
@@ -1821,14 +2178,17 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1821
2178
|
}>
|
|
1822
2179
|
> {
|
|
1823
2180
|
return this.runRegular(() =>
|
|
1824
|
-
this.loadSubsetUnscheduled(
|
|
2181
|
+
this.loadSubsetUnscheduled(target, options, ctx),
|
|
1825
2182
|
)
|
|
1826
2183
|
}
|
|
1827
2184
|
|
|
1828
2185
|
private async loadSubsetUnscheduled(
|
|
1829
|
-
|
|
2186
|
+
target: PersistedStorageTarget,
|
|
1830
2187
|
options: LoadSubsetOptions,
|
|
1831
|
-
ctx?: {
|
|
2188
|
+
ctx?: {
|
|
2189
|
+
requiredIndexSignatures?: ReadonlyArray<string>
|
|
2190
|
+
cacheGenerationClaimId?: string
|
|
2191
|
+
},
|
|
1832
2192
|
): Promise<
|
|
1833
2193
|
Array<{
|
|
1834
2194
|
key: string | number
|
|
@@ -1836,8 +2196,29 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1836
2196
|
metadata?: unknown
|
|
1837
2197
|
}>
|
|
1838
2198
|
> {
|
|
1839
|
-
const
|
|
2199
|
+
const access = resolvePersistedStorageTarget(
|
|
2200
|
+
target,
|
|
2201
|
+
ctx?.cacheGenerationClaimId,
|
|
2202
|
+
)
|
|
2203
|
+
const collectionId = access.collectionId
|
|
2204
|
+
ctx = access.claimId
|
|
2205
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2206
|
+
: ctx
|
|
2207
|
+
await this.assertCacheGenerationReadClaim(
|
|
2208
|
+
this.driver,
|
|
2209
|
+
collectionId,
|
|
2210
|
+
ctx?.cacheGenerationClaimId,
|
|
2211
|
+
)
|
|
2212
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2213
|
+
collectionId,
|
|
2214
|
+
ctx?.cacheGenerationClaimId,
|
|
2215
|
+
)
|
|
1840
2216
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2217
|
+
await this.assertCacheGenerationReadClaim(
|
|
2218
|
+
transactionDriver,
|
|
2219
|
+
collectionId,
|
|
2220
|
+
ctx?.cacheGenerationClaimId,
|
|
2221
|
+
)
|
|
1841
2222
|
await this.assertCurrentSchemaVersion(
|
|
1842
2223
|
collectionId,
|
|
1843
2224
|
transactionDriver,
|
|
@@ -1911,22 +2292,24 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1911
2292
|
}
|
|
1912
2293
|
|
|
1913
2294
|
loadResumeSnapshot(
|
|
1914
|
-
|
|
2295
|
+
target: PersistedStorageTarget,
|
|
1915
2296
|
ctx?: {
|
|
1916
2297
|
requiredIndexSignatures?: ReadonlyArray<string>
|
|
1917
2298
|
includeRows?: boolean
|
|
2299
|
+
cacheGenerationClaimId?: string
|
|
1918
2300
|
},
|
|
1919
2301
|
) {
|
|
1920
2302
|
return this.runRegular(() =>
|
|
1921
|
-
this.loadResumeSnapshotUnscheduled(
|
|
2303
|
+
this.loadResumeSnapshotUnscheduled(target, ctx),
|
|
1922
2304
|
)
|
|
1923
2305
|
}
|
|
1924
2306
|
|
|
1925
2307
|
private async loadResumeSnapshotUnscheduled(
|
|
1926
|
-
|
|
2308
|
+
target: PersistedStorageTarget,
|
|
1927
2309
|
ctx?: {
|
|
1928
2310
|
requiredIndexSignatures?: ReadonlyArray<string>
|
|
1929
2311
|
includeRows?: boolean
|
|
2312
|
+
cacheGenerationClaimId?: string
|
|
1930
2313
|
},
|
|
1931
2314
|
): Promise<{
|
|
1932
2315
|
rows: Array<{
|
|
@@ -1941,10 +2324,31 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1941
2324
|
latestRowVersion: number
|
|
1942
2325
|
resetEpoch: number
|
|
1943
2326
|
}> {
|
|
1944
|
-
const
|
|
2327
|
+
const access = resolvePersistedStorageTarget(
|
|
2328
|
+
target,
|
|
2329
|
+
ctx?.cacheGenerationClaimId,
|
|
2330
|
+
)
|
|
2331
|
+
const collectionId = access.collectionId
|
|
2332
|
+
ctx = access.claimId
|
|
2333
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2334
|
+
: ctx
|
|
2335
|
+
await this.assertCacheGenerationReadClaim(
|
|
2336
|
+
this.driver,
|
|
2337
|
+
collectionId,
|
|
2338
|
+
ctx?.cacheGenerationClaimId,
|
|
2339
|
+
)
|
|
2340
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2341
|
+
collectionId,
|
|
2342
|
+
ctx?.cacheGenerationClaimId,
|
|
2343
|
+
)
|
|
1945
2344
|
const includeRows = ctx?.includeRows !== false
|
|
1946
2345
|
|
|
1947
2346
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2347
|
+
await this.assertCacheGenerationReadClaim(
|
|
2348
|
+
transactionDriver,
|
|
2349
|
+
collectionId,
|
|
2350
|
+
ctx?.cacheGenerationClaimId,
|
|
2351
|
+
)
|
|
1948
2352
|
await this.assertCurrentSchemaVersion(
|
|
1949
2353
|
collectionId,
|
|
1950
2354
|
transactionDriver,
|
|
@@ -2005,31 +2409,32 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2005
2409
|
})
|
|
2006
2410
|
}
|
|
2007
2411
|
|
|
2008
|
-
applyCommittedTx(
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2412
|
+
applyCommittedTx(
|
|
2413
|
+
target: PersistedStorageTarget,
|
|
2414
|
+
tx: PersistedTx,
|
|
2415
|
+
): Promise<void> {
|
|
2416
|
+
return this.runRegular(() => this.applyCommittedTxUnscheduled(target, tx))
|
|
2012
2417
|
}
|
|
2013
2418
|
|
|
2014
2419
|
reconcileCommittedTx(
|
|
2015
|
-
|
|
2420
|
+
target: PersistedStorageTarget,
|
|
2016
2421
|
tx: PersistedTx,
|
|
2017
2422
|
anchor: CommittedTxAnchor,
|
|
2018
2423
|
): Promise<ReconciledCommittedTx> {
|
|
2019
2424
|
return this.runRegular(() =>
|
|
2020
|
-
this.reconcileCommittedTxUnscheduled(
|
|
2425
|
+
this.reconcileCommittedTxUnscheduled(target, tx, anchor),
|
|
2021
2426
|
)
|
|
2022
2427
|
}
|
|
2023
2428
|
|
|
2024
2429
|
private async applyCommittedTxUnscheduled(
|
|
2025
|
-
|
|
2430
|
+
target: PersistedStorageTarget,
|
|
2026
2431
|
tx: PersistedTx,
|
|
2027
2432
|
): Promise<void> {
|
|
2028
|
-
await this.applyCommittedTxInternal(
|
|
2433
|
+
await this.applyCommittedTxInternal(target, tx)
|
|
2029
2434
|
}
|
|
2030
2435
|
|
|
2031
2436
|
private reconcileCommittedTxUnscheduled(
|
|
2032
|
-
|
|
2437
|
+
target: PersistedStorageTarget,
|
|
2033
2438
|
tx: PersistedTx,
|
|
2034
2439
|
anchor: CommittedTxAnchor,
|
|
2035
2440
|
): Promise<ReconciledCommittedTx> {
|
|
@@ -2038,19 +2443,38 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2038
2443
|
`Cannot reconcile a committed transaction without a valid durable anchor`,
|
|
2039
2444
|
)
|
|
2040
2445
|
}
|
|
2041
|
-
return this.applyCommittedTxInternal(
|
|
2446
|
+
return this.applyCommittedTxInternal(target, tx, anchor)
|
|
2042
2447
|
}
|
|
2043
2448
|
|
|
2044
2449
|
private async applyCommittedTxInternal(
|
|
2045
|
-
|
|
2450
|
+
target: PersistedStorageTarget,
|
|
2046
2451
|
tx: PersistedTx,
|
|
2047
2452
|
anchor?: CommittedTxAnchor,
|
|
2048
2453
|
): Promise<ReconciledCommittedTx> {
|
|
2049
|
-
const
|
|
2454
|
+
const access = resolvePersistedStorageTarget(
|
|
2455
|
+
target,
|
|
2456
|
+
tx.cacheGenerationClaimId,
|
|
2457
|
+
)
|
|
2458
|
+
const collectionId = access.collectionId
|
|
2459
|
+
tx = { ...tx, cacheGenerationClaimId: access.claimId }
|
|
2460
|
+
await this.assertCacheGenerationReadClaim(
|
|
2461
|
+
this.driver,
|
|
2462
|
+
collectionId,
|
|
2463
|
+
tx.cacheGenerationClaimId,
|
|
2464
|
+
)
|
|
2465
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2466
|
+
collectionId,
|
|
2467
|
+
tx.cacheGenerationClaimId,
|
|
2468
|
+
)
|
|
2050
2469
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2051
2470
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
|
|
2052
2471
|
|
|
2053
2472
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2473
|
+
await this.assertCacheGenerationReadClaim(
|
|
2474
|
+
transactionDriver,
|
|
2475
|
+
collectionId,
|
|
2476
|
+
tx.cacheGenerationClaimId,
|
|
2477
|
+
)
|
|
2054
2478
|
const versionRows = await transactionDriver.query<{
|
|
2055
2479
|
latest_row_version: number
|
|
2056
2480
|
key_set_evidence_available: number
|
|
@@ -2464,18 +2888,38 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2464
2888
|
}
|
|
2465
2889
|
|
|
2466
2890
|
loadCollectionMetadata(
|
|
2467
|
-
|
|
2891
|
+
target: PersistedStorageTarget,
|
|
2892
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2468
2893
|
): Promise<Array<{ key: string; value: unknown }>> {
|
|
2469
2894
|
return this.runRegular(() =>
|
|
2470
|
-
this.loadCollectionMetadataUnscheduled(
|
|
2895
|
+
this.loadCollectionMetadataUnscheduled(target, ctx),
|
|
2471
2896
|
)
|
|
2472
2897
|
}
|
|
2473
2898
|
|
|
2474
2899
|
private async loadCollectionMetadataUnscheduled(
|
|
2475
|
-
|
|
2900
|
+
target: PersistedStorageTarget,
|
|
2901
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2476
2902
|
): Promise<Array<{ key: string; value: unknown }>> {
|
|
2477
|
-
|
|
2903
|
+
const access = resolvePersistedStorageTarget(
|
|
2904
|
+
target,
|
|
2905
|
+
ctx?.cacheGenerationClaimId,
|
|
2906
|
+
)
|
|
2907
|
+
const collectionId = access.collectionId
|
|
2908
|
+
ctx = access.claimId
|
|
2909
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2910
|
+
: ctx
|
|
2911
|
+
await this.assertCacheGenerationReadClaim(
|
|
2912
|
+
this.driver,
|
|
2913
|
+
collectionId,
|
|
2914
|
+
ctx?.cacheGenerationClaimId,
|
|
2915
|
+
)
|
|
2916
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
2478
2917
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2918
|
+
await this.assertCacheGenerationReadClaim(
|
|
2919
|
+
transactionDriver,
|
|
2920
|
+
collectionId,
|
|
2921
|
+
ctx?.cacheGenerationClaimId,
|
|
2922
|
+
)
|
|
2479
2923
|
await this.assertCurrentSchemaVersion(
|
|
2480
2924
|
collectionId,
|
|
2481
2925
|
transactionDriver,
|
|
@@ -2499,21 +2943,42 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2499
2943
|
}
|
|
2500
2944
|
|
|
2501
2945
|
scanRows(
|
|
2502
|
-
|
|
2946
|
+
target: PersistedStorageTarget,
|
|
2503
2947
|
options?: PersistedRowScanOptions,
|
|
2948
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2504
2949
|
): Promise<Array<PersistedScannedRow>> {
|
|
2505
|
-
return this.runRegular(() =>
|
|
2506
|
-
this.scanRowsUnscheduled(collectionId, options),
|
|
2507
|
-
)
|
|
2950
|
+
return this.runRegular(() => this.scanRowsUnscheduled(target, options, ctx))
|
|
2508
2951
|
}
|
|
2509
2952
|
|
|
2510
2953
|
private async scanRowsUnscheduled(
|
|
2511
|
-
|
|
2954
|
+
target: PersistedStorageTarget,
|
|
2512
2955
|
options?: PersistedRowScanOptions,
|
|
2956
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2513
2957
|
): Promise<Array<PersistedScannedRow>> {
|
|
2514
|
-
const
|
|
2958
|
+
const access = resolvePersistedStorageTarget(
|
|
2959
|
+
target,
|
|
2960
|
+
ctx?.cacheGenerationClaimId,
|
|
2961
|
+
)
|
|
2962
|
+
const collectionId = access.collectionId
|
|
2963
|
+
ctx = access.claimId
|
|
2964
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2965
|
+
: ctx
|
|
2966
|
+
await this.assertCacheGenerationReadClaim(
|
|
2967
|
+
this.driver,
|
|
2968
|
+
collectionId,
|
|
2969
|
+
ctx?.cacheGenerationClaimId,
|
|
2970
|
+
)
|
|
2971
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2972
|
+
collectionId,
|
|
2973
|
+
ctx?.cacheGenerationClaimId,
|
|
2974
|
+
)
|
|
2515
2975
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2516
2976
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2977
|
+
await this.assertCacheGenerationReadClaim(
|
|
2978
|
+
transactionDriver,
|
|
2979
|
+
collectionId,
|
|
2980
|
+
ctx?.cacheGenerationClaimId,
|
|
2981
|
+
)
|
|
2517
2982
|
await this.assertCurrentSchemaVersion(
|
|
2518
2983
|
collectionId,
|
|
2519
2984
|
transactionDriver,
|
|
@@ -2537,21 +3002,39 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2537
3002
|
}
|
|
2538
3003
|
|
|
2539
3004
|
ensureIndex(
|
|
2540
|
-
|
|
3005
|
+
target: PersistedStorageTarget,
|
|
2541
3006
|
signature: string,
|
|
2542
3007
|
spec: PersistedIndexSpec,
|
|
3008
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2543
3009
|
): Promise<void> {
|
|
2544
3010
|
return this.runRegular(() =>
|
|
2545
|
-
this.ensureIndexUnscheduled(
|
|
3011
|
+
this.ensureIndexUnscheduled(target, signature, spec, ctx),
|
|
2546
3012
|
)
|
|
2547
3013
|
}
|
|
2548
3014
|
|
|
2549
3015
|
private async ensureIndexUnscheduled(
|
|
2550
|
-
|
|
3016
|
+
target: PersistedStorageTarget,
|
|
2551
3017
|
signature: string,
|
|
2552
3018
|
spec: PersistedIndexSpec,
|
|
3019
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2553
3020
|
): Promise<void> {
|
|
2554
|
-
const
|
|
3021
|
+
const access = resolvePersistedStorageTarget(
|
|
3022
|
+
target,
|
|
3023
|
+
ctx?.cacheGenerationClaimId,
|
|
3024
|
+
)
|
|
3025
|
+
const collectionId = access.collectionId
|
|
3026
|
+
ctx = access.claimId
|
|
3027
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3028
|
+
: ctx
|
|
3029
|
+
await this.assertCacheGenerationReadClaim(
|
|
3030
|
+
this.driver,
|
|
3031
|
+
collectionId,
|
|
3032
|
+
ctx?.cacheGenerationClaimId,
|
|
3033
|
+
)
|
|
3034
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
3035
|
+
collectionId,
|
|
3036
|
+
ctx?.cacheGenerationClaimId,
|
|
3037
|
+
)
|
|
2555
3038
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2556
3039
|
const indexName = buildIndexName(collectionId, signature)
|
|
2557
3040
|
const indexNameSql = quoteIdentifier(indexName)
|
|
@@ -2566,6 +3049,11 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2566
3049
|
const persistedWhereSql = whereSql ?? null
|
|
2567
3050
|
|
|
2568
3051
|
await this.runInTransaction(async (transactionDriver) => {
|
|
3052
|
+
await this.assertCacheGenerationReadClaim(
|
|
3053
|
+
transactionDriver,
|
|
3054
|
+
collectionId,
|
|
3055
|
+
ctx?.cacheGenerationClaimId,
|
|
3056
|
+
)
|
|
2569
3057
|
await this.assertCurrentSchemaVersion(
|
|
2570
3058
|
collectionId,
|
|
2571
3059
|
transactionDriver,
|
|
@@ -2639,18 +3127,41 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2639
3127
|
})
|
|
2640
3128
|
}
|
|
2641
3129
|
|
|
2642
|
-
markIndexRemoved(
|
|
3130
|
+
markIndexRemoved(
|
|
3131
|
+
target: PersistedStorageTarget,
|
|
3132
|
+
signature: string,
|
|
3133
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3134
|
+
): Promise<void> {
|
|
2643
3135
|
return this.runRegular(() =>
|
|
2644
|
-
this.markIndexRemovedUnscheduled(
|
|
3136
|
+
this.markIndexRemovedUnscheduled(target, signature, ctx),
|
|
2645
3137
|
)
|
|
2646
3138
|
}
|
|
2647
3139
|
|
|
2648
3140
|
private async markIndexRemovedUnscheduled(
|
|
2649
|
-
|
|
3141
|
+
target: PersistedStorageTarget,
|
|
2650
3142
|
signature: string,
|
|
3143
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2651
3144
|
): Promise<void> {
|
|
2652
|
-
|
|
3145
|
+
const access = resolvePersistedStorageTarget(
|
|
3146
|
+
target,
|
|
3147
|
+
ctx?.cacheGenerationClaimId,
|
|
3148
|
+
)
|
|
3149
|
+
const collectionId = access.collectionId
|
|
3150
|
+
ctx = access.claimId
|
|
3151
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3152
|
+
: ctx
|
|
3153
|
+
await this.assertCacheGenerationReadClaim(
|
|
3154
|
+
this.driver,
|
|
3155
|
+
collectionId,
|
|
3156
|
+
ctx?.cacheGenerationClaimId,
|
|
3157
|
+
)
|
|
3158
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
2653
3159
|
await this.runInTransaction(async (transactionDriver) => {
|
|
3160
|
+
await this.assertCacheGenerationReadClaim(
|
|
3161
|
+
transactionDriver,
|
|
3162
|
+
collectionId,
|
|
3163
|
+
ctx?.cacheGenerationClaimId,
|
|
3164
|
+
)
|
|
2654
3165
|
await this.assertCurrentSchemaVersion(
|
|
2655
3166
|
collectionId,
|
|
2656
3167
|
transactionDriver,
|
|
@@ -2683,27 +3194,47 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2683
3194
|
}
|
|
2684
3195
|
|
|
2685
3196
|
reserveLeadershipTerm(
|
|
2686
|
-
|
|
3197
|
+
target: PersistedStorageTarget,
|
|
2687
3198
|
observedTerm: number,
|
|
3199
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2688
3200
|
): Promise<{
|
|
2689
3201
|
latestTerm: number
|
|
2690
3202
|
latestSeq: number
|
|
2691
3203
|
latestRowVersion: number
|
|
2692
3204
|
}> {
|
|
2693
3205
|
// Election may run while hydration waits for its first writer route.
|
|
2694
|
-
return this.reserveLeadershipTermUnscheduled(
|
|
3206
|
+
return this.reserveLeadershipTermUnscheduled(target, observedTerm, ctx)
|
|
2695
3207
|
}
|
|
2696
3208
|
|
|
2697
3209
|
private async reserveLeadershipTermUnscheduled(
|
|
2698
|
-
|
|
3210
|
+
target: PersistedStorageTarget,
|
|
2699
3211
|
observedTerm: number,
|
|
3212
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2700
3213
|
): Promise<{
|
|
2701
3214
|
latestTerm: number
|
|
2702
3215
|
latestSeq: number
|
|
2703
3216
|
latestRowVersion: number
|
|
2704
3217
|
}> {
|
|
2705
|
-
|
|
3218
|
+
const access = resolvePersistedStorageTarget(
|
|
3219
|
+
target,
|
|
3220
|
+
ctx?.cacheGenerationClaimId,
|
|
3221
|
+
)
|
|
3222
|
+
const collectionId = access.collectionId
|
|
3223
|
+
ctx = access.claimId
|
|
3224
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3225
|
+
: ctx
|
|
3226
|
+
await this.assertCacheGenerationReadClaim(
|
|
3227
|
+
this.driver,
|
|
3228
|
+
collectionId,
|
|
3229
|
+
ctx?.cacheGenerationClaimId,
|
|
3230
|
+
)
|
|
3231
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
2706
3232
|
return this.runInTransaction(async (transactionDriver) => {
|
|
3233
|
+
await this.assertCacheGenerationReadClaim(
|
|
3234
|
+
transactionDriver,
|
|
3235
|
+
collectionId,
|
|
3236
|
+
ctx?.cacheGenerationClaimId,
|
|
3237
|
+
)
|
|
2707
3238
|
await this.assertCurrentSchemaVersion(
|
|
2708
3239
|
collectionId,
|
|
2709
3240
|
transactionDriver,
|
|
@@ -2724,23 +3255,47 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2724
3255
|
})
|
|
2725
3256
|
}
|
|
2726
3257
|
|
|
2727
|
-
getStreamPosition(
|
|
3258
|
+
getStreamPosition(
|
|
3259
|
+
target: PersistedStorageTarget,
|
|
3260
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3261
|
+
): Promise<{
|
|
2728
3262
|
latestTerm: number
|
|
2729
3263
|
latestSeq: number
|
|
2730
3264
|
latestRowVersion: number
|
|
2731
3265
|
}> {
|
|
2732
3266
|
// Election must not queue behind a hydrate awaiting its first writer route.
|
|
2733
3267
|
// The stream-position snapshot still uses the driver's transaction admission.
|
|
2734
|
-
return this.getStreamPositionUnscheduled(
|
|
3268
|
+
return this.getStreamPositionUnscheduled(target, ctx)
|
|
2735
3269
|
}
|
|
2736
3270
|
|
|
2737
|
-
private async getStreamPositionUnscheduled(
|
|
3271
|
+
private async getStreamPositionUnscheduled(
|
|
3272
|
+
target: PersistedStorageTarget,
|
|
3273
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3274
|
+
): Promise<{
|
|
2738
3275
|
latestTerm: number
|
|
2739
3276
|
latestSeq: number
|
|
2740
3277
|
latestRowVersion: number
|
|
2741
3278
|
}> {
|
|
2742
|
-
|
|
3279
|
+
const access = resolvePersistedStorageTarget(
|
|
3280
|
+
target,
|
|
3281
|
+
ctx?.cacheGenerationClaimId,
|
|
3282
|
+
)
|
|
3283
|
+
const collectionId = access.collectionId
|
|
3284
|
+
ctx = access.claimId
|
|
3285
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3286
|
+
: ctx
|
|
3287
|
+
await this.assertCacheGenerationReadClaim(
|
|
3288
|
+
this.driver,
|
|
3289
|
+
collectionId,
|
|
3290
|
+
ctx?.cacheGenerationClaimId,
|
|
3291
|
+
)
|
|
3292
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
2743
3293
|
return this.runInTransaction(async (transactionDriver) => {
|
|
3294
|
+
await this.assertCacheGenerationReadClaim(
|
|
3295
|
+
transactionDriver,
|
|
3296
|
+
collectionId,
|
|
3297
|
+
ctx?.cacheGenerationClaimId,
|
|
3298
|
+
)
|
|
2744
3299
|
await this.assertCurrentSchemaVersion(
|
|
2745
3300
|
collectionId,
|
|
2746
3301
|
transactionDriver,
|
|
@@ -2837,23 +3392,46 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2837
3392
|
}
|
|
2838
3393
|
|
|
2839
3394
|
pullSince(
|
|
2840
|
-
|
|
3395
|
+
target: PersistedStorageTarget,
|
|
2841
3396
|
fromRowVersion: number,
|
|
3397
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2842
3398
|
): Promise<SQLitePullSinceResult<string | number>> {
|
|
2843
3399
|
return this.runRegular(() =>
|
|
2844
|
-
this.pullSinceUnscheduled(
|
|
3400
|
+
this.pullSinceUnscheduled(target, fromRowVersion, ctx),
|
|
2845
3401
|
)
|
|
2846
3402
|
}
|
|
2847
3403
|
|
|
2848
3404
|
private async pullSinceUnscheduled(
|
|
2849
|
-
|
|
3405
|
+
target: PersistedStorageTarget,
|
|
2850
3406
|
fromRowVersion: number,
|
|
3407
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2851
3408
|
): Promise<SQLitePullSinceResult<string | number>> {
|
|
2852
|
-
const
|
|
3409
|
+
const access = resolvePersistedStorageTarget(
|
|
3410
|
+
target,
|
|
3411
|
+
ctx?.cacheGenerationClaimId,
|
|
3412
|
+
)
|
|
3413
|
+
const collectionId = access.collectionId
|
|
3414
|
+
ctx = access.claimId
|
|
3415
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3416
|
+
: ctx
|
|
3417
|
+
await this.assertCacheGenerationReadClaim(
|
|
3418
|
+
this.driver,
|
|
3419
|
+
collectionId,
|
|
3420
|
+
ctx?.cacheGenerationClaimId,
|
|
3421
|
+
)
|
|
3422
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
3423
|
+
collectionId,
|
|
3424
|
+
ctx?.cacheGenerationClaimId,
|
|
3425
|
+
)
|
|
2853
3426
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2854
3427
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
|
|
2855
3428
|
|
|
2856
3429
|
return this.runInTransaction(async (transactionDriver) => {
|
|
3430
|
+
await this.assertCacheGenerationReadClaim(
|
|
3431
|
+
transactionDriver,
|
|
3432
|
+
collectionId,
|
|
3433
|
+
ctx?.cacheGenerationClaimId,
|
|
3434
|
+
)
|
|
2857
3435
|
await this.assertCurrentSchemaVersion(
|
|
2858
3436
|
collectionId,
|
|
2859
3437
|
transactionDriver,
|
|
@@ -3178,6 +3756,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3178
3756
|
|
|
3179
3757
|
private async ensureCollectionReady(
|
|
3180
3758
|
collectionId: string,
|
|
3759
|
+
cacheGenerationClaimId?: string,
|
|
3181
3760
|
): Promise<CollectionTableMapping> {
|
|
3182
3761
|
await this.ensureInitialized()
|
|
3183
3762
|
|
|
@@ -3191,7 +3770,10 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3191
3770
|
return inFlight
|
|
3192
3771
|
}
|
|
3193
3772
|
|
|
3194
|
-
const loadPromise = this.ensureCollectionReadyInternal(
|
|
3773
|
+
const loadPromise = this.ensureCollectionReadyInternal(
|
|
3774
|
+
collectionId,
|
|
3775
|
+
cacheGenerationClaimId,
|
|
3776
|
+
)
|
|
3195
3777
|
this.collectionTableLoads.set(collectionId, loadPromise)
|
|
3196
3778
|
|
|
3197
3779
|
try {
|
|
@@ -3226,24 +3808,43 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3226
3808
|
|
|
3227
3809
|
private async ensureCollectionReadyInternal(
|
|
3228
3810
|
collectionId: string,
|
|
3811
|
+
cacheGenerationClaimId?: string,
|
|
3229
3812
|
): Promise<CollectionTableMapping> {
|
|
3813
|
+
// Record whether registration began for a managed physical ID. Collection
|
|
3814
|
+
// may remove that catalog row while this operation waits on external DDL.
|
|
3815
|
+
const managedAtAdmission =
|
|
3816
|
+
cacheGenerationClaimId !== undefined ||
|
|
3817
|
+
(hasCacheGenerationIdPrefix(collectionId) &&
|
|
3818
|
+
(
|
|
3819
|
+
await this.driver.query<{ physical_id: string }>(
|
|
3820
|
+
`SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
|
|
3821
|
+
[collectionId],
|
|
3822
|
+
)
|
|
3823
|
+
)[0] !== undefined)
|
|
3230
3824
|
let registration = await this.loadCollectionRegistration(collectionId)
|
|
3231
3825
|
|
|
3232
3826
|
if (!registration) {
|
|
3233
3827
|
const tableName = createPersistedTableName(collectionId, `c`)
|
|
3234
3828
|
const tombstoneTableName = createPersistedTableName(collectionId, `t`)
|
|
3235
|
-
await this.
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3829
|
+
await this.runInTransaction(async (transactionDriver) => {
|
|
3830
|
+
await this.assertCacheGenerationReadClaim(
|
|
3831
|
+
transactionDriver,
|
|
3832
|
+
collectionId,
|
|
3833
|
+
cacheGenerationClaimId,
|
|
3834
|
+
)
|
|
3835
|
+
await transactionDriver.run(
|
|
3836
|
+
`INSERT INTO collection_registry (
|
|
3837
|
+
collection_id,
|
|
3838
|
+
table_name,
|
|
3839
|
+
tombstone_table_name,
|
|
3840
|
+
schema_version,
|
|
3841
|
+
updated_at
|
|
3842
|
+
)
|
|
3843
|
+
VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
3844
|
+
ON CONFLICT DO NOTHING`,
|
|
3845
|
+
[collectionId, tableName, tombstoneTableName, this.schemaVersion],
|
|
3846
|
+
)
|
|
3847
|
+
})
|
|
3247
3848
|
|
|
3248
3849
|
registration = await this.loadCollectionRegistration(collectionId)
|
|
3249
3850
|
if (!registration) {
|
|
@@ -3262,6 +3863,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3262
3863
|
this.schemaVersion,
|
|
3263
3864
|
tableName,
|
|
3264
3865
|
tombstoneTableName,
|
|
3866
|
+
cacheGenerationClaimId,
|
|
3265
3867
|
)
|
|
3266
3868
|
}
|
|
3267
3869
|
|
|
@@ -3310,6 +3912,38 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3310
3912
|
[collectionId],
|
|
3311
3913
|
)
|
|
3312
3914
|
await this.ensureCollectionKeyEvidenceTriggers(collectionId, tableName)
|
|
3915
|
+
if (managedAtAdmission) {
|
|
3916
|
+
// Registration is outside the read/write transaction. If collection
|
|
3917
|
+
// retired this generation while the DDL was in flight, remove any
|
|
3918
|
+
// empty table recreated after collection instead of caching it.
|
|
3919
|
+
const generation = await this.driver.query<{ physical_id: string }>(
|
|
3920
|
+
`SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
|
|
3921
|
+
[collectionId],
|
|
3922
|
+
)
|
|
3923
|
+
if (!generation[0]) {
|
|
3924
|
+
await this.driver.exec(
|
|
3925
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}`,
|
|
3926
|
+
)
|
|
3927
|
+
await this.driver.exec(
|
|
3928
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(tombstoneTableName)}`,
|
|
3929
|
+
)
|
|
3930
|
+
await this.driver.run(
|
|
3931
|
+
`DELETE FROM collection_registry WHERE collection_id = ?`,
|
|
3932
|
+
[collectionId],
|
|
3933
|
+
)
|
|
3934
|
+
await this.driver.run(
|
|
3935
|
+
`DELETE FROM collection_version WHERE collection_id = ?`,
|
|
3936
|
+
[collectionId],
|
|
3937
|
+
)
|
|
3938
|
+
await this.driver.run(
|
|
3939
|
+
`DELETE FROM collection_reset_epoch WHERE collection_id = ?`,
|
|
3940
|
+
[collectionId],
|
|
3941
|
+
)
|
|
3942
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
3943
|
+
`Persisted cache generation was collected before registration finished`,
|
|
3944
|
+
)
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3313
3947
|
const mapping = {
|
|
3314
3948
|
tableName,
|
|
3315
3949
|
tombstoneTableName,
|
|
@@ -3391,6 +4025,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3391
4025
|
nextSchemaVersion: number,
|
|
3392
4026
|
tableName: string,
|
|
3393
4027
|
tombstoneTableName: string,
|
|
4028
|
+
cacheGenerationClaimId?: string,
|
|
3394
4029
|
): Promise<void> {
|
|
3395
4030
|
if (this.schemaMismatchPolicy === `sync-absent-error`) {
|
|
3396
4031
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -3403,6 +4038,11 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3403
4038
|
const tombstoneTableSql = quoteIdentifier(tombstoneTableName)
|
|
3404
4039
|
|
|
3405
4040
|
await this.runInTransaction(async (transactionDriver) => {
|
|
4041
|
+
await this.assertCacheGenerationReadClaim(
|
|
4042
|
+
transactionDriver,
|
|
4043
|
+
collectionId,
|
|
4044
|
+
cacheGenerationClaimId,
|
|
4045
|
+
)
|
|
3406
4046
|
const currentSchemaRows = await transactionDriver.query<{
|
|
3407
4047
|
schema_version: number
|
|
3408
4048
|
}>(
|
|
@@ -3497,6 +4137,49 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3497
4137
|
return
|
|
3498
4138
|
}
|
|
3499
4139
|
|
|
4140
|
+
await this.driver.exec(
|
|
4141
|
+
`CREATE TABLE IF NOT EXISTS cache_generation (
|
|
4142
|
+
logical_id TEXT NOT NULL,
|
|
4143
|
+
generation INTEGER NOT NULL,
|
|
4144
|
+
physical_id TEXT NOT NULL UNIQUE,
|
|
4145
|
+
retired INTEGER NOT NULL DEFAULT 0,
|
|
4146
|
+
PRIMARY KEY (logical_id, generation)
|
|
4147
|
+
)`,
|
|
4148
|
+
)
|
|
4149
|
+
await this.driver.exec(
|
|
4150
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS cache_generation_head
|
|
4151
|
+
ON cache_generation (logical_id) WHERE retired = 0`,
|
|
4152
|
+
)
|
|
4153
|
+
await this.driver.exec(
|
|
4154
|
+
`CREATE TABLE IF NOT EXISTS cache_generation_sequence (
|
|
4155
|
+
logical_id TEXT PRIMARY KEY,
|
|
4156
|
+
next_generation INTEGER NOT NULL
|
|
4157
|
+
)`,
|
|
4158
|
+
)
|
|
4159
|
+
await this.driver.exec(
|
|
4160
|
+
`CREATE TABLE IF NOT EXISTS cache_generation_claim (
|
|
4161
|
+
claim_id TEXT PRIMARY KEY,
|
|
4162
|
+
logical_id TEXT NOT NULL,
|
|
4163
|
+
physical_id TEXT NOT NULL,
|
|
4164
|
+
expires_at_ms INTEGER NOT NULL
|
|
4165
|
+
)`,
|
|
4166
|
+
)
|
|
4167
|
+
const claimColumns = await this.driver.query<{ name: string }>(
|
|
4168
|
+
`PRAGMA table_info(cache_generation_claim)`,
|
|
4169
|
+
)
|
|
4170
|
+
if (!claimColumns.some(({ name }) => name === `logical_id`)) {
|
|
4171
|
+
await this.driver.exec(
|
|
4172
|
+
`ALTER TABLE cache_generation_claim
|
|
4173
|
+
ADD COLUMN logical_id TEXT NOT NULL DEFAULT ''`,
|
|
4174
|
+
)
|
|
4175
|
+
}
|
|
4176
|
+
if (!claimColumns.some(({ name }) => name === `expires_at_ms`)) {
|
|
4177
|
+
await this.driver.exec(
|
|
4178
|
+
`ALTER TABLE cache_generation_claim
|
|
4179
|
+
ADD COLUMN expires_at_ms INTEGER NOT NULL DEFAULT 0`,
|
|
4180
|
+
)
|
|
4181
|
+
}
|
|
4182
|
+
|
|
3500
4183
|
await this.driver.exec(
|
|
3501
4184
|
`CREATE TABLE IF NOT EXISTS collection_registry (
|
|
3502
4185
|
collection_id TEXT PRIMARY KEY,
|