Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/electric-cleanup-stream-listeners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/electric-db-collection': patch
---

Release the shape stream's visibility listener when a sync run is cleaned up. The Electric client registers a `visibilitychange` listener for each `ShapeStream` (an `AppState` subscription on React Native, from `@electric-sql/client` 1.5.23) and removes it only in `unsubscribeAll()`, so aborting the stream left the listener behind, and the listener kept the stream reachable. An app that cleans up and recreates collections, or whose on-demand cache rotates its provider session, gained one retained stream each time.
2 changes: 1 addition & 1 deletion docs/contributing/oracle-coverage.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/db/tests/oracle-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const staticOracleProperties = [
`electric.persistence-interleaving`,
`electric.sdk-snapshot-delivery`,
`electric.sdk-dnf-membership`,
`electric.sdk-source-release`,
`electric-recovery.publication-stream-convergence`,
`sqlite-resume.startup-generation`,
`sqlite-resume.managed-claim-read`,
Expand Down
2 changes: 1 addition & 1 deletion packages/db/tests/oracle-replay-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const ownerGroups: ReadonlyArray<readonly [string, string, string]> = [
[
`electric-db-collection/tests/electric-sdk-delivery-oracle.property.test.ts`,
`electric`,
`sdk-snapshot-delivery sdk-dnf-membership`,
`sdk-snapshot-delivery sdk-dnf-membership sdk-source-release`,
],
[
`db/tests/collection-sync-reentrancy-oracle.test.ts`,
Expand Down
5 changes: 5 additions & 0 deletions packages/electric-db-collection/src/electric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2756,6 +2756,11 @@ function createElectricSync<T extends Row<unknown>>(
shapeOptions.signal?.removeEventListener(`abort`, forwardExternalAbort)
// Unsubscribe from the stream
unsubscribeStream()
// Aborting the stream does not remove the visibility listener it
// registered when it was constructed, and that listener keeps the
// stream reachable. The stream is private to this session, so
// unsubscribeAll affects no other consumer.
stream.unsubscribeAll()
// Abort the abort controller to stop the stream
abortController.abort()
if (transactionStarted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ vi.mock(`@electric-sql/client`, async () => {
...actual,
ShapeStream: vi.fn(() => ({
subscribe: mockSubscribe,
unsubscribeAll: vi.fn(),
fetchSnapshot: mockFetchSnapshot,
requestSnapshot: vi.fn(),
})),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ vi.mock(`@electric-sql/client`, async () => {
onStreamSubscribed?.()
return unsubscribe
},
unsubscribeAll: vi.fn(),
// ShapeStream.requestSnapshot is legal only in changes_only mode.
// The installed-SDK delivery oracle is the receiving witness.
requestSnapshot: vi.fn(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function expectNoRepeatedSnapshotRequests() {

const mockStream = {
subscribe: mockSubscribe,
unsubscribeAll: vi.fn(),
fetchSnapshot: mockFetchSnapshot,
requestSnapshot: async (...args: any) => {
const result = await mockRequestSnapshot(...args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ const mockSubscribe = vi.fn()
const shapeId = `{"params":{"table":"test_table"},"url":"http://test-url"}`
const mockStream = {
subscribe: mockSubscribe,
unsubscribeAll: vi.fn(),
requestSnapshot: vi.fn().mockResolvedValue(undefined),
fetchSnapshot: vi.fn().mockResolvedValue({ metadata: {}, data: [] }),
forceDisconnectAndRefresh: vi.fn().mockResolvedValue(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ vi.mock(`@electric-sql/client`, async () => ({
...(await vi.importActual(`@electric-sql/client`)),
ShapeStream: vi.fn(() => ({
subscribe: mockSubscribe,
unsubscribeAll: vi.fn(),
requestSnapshot: vi.fn().mockResolvedValue(undefined),
fetchSnapshot: vi.fn().mockResolvedValue({ metadata: {}, data: [] }),
forceDisconnectAndRefresh: vi.fn().mockResolvedValue(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ vi.mock(`@electric-sql/client`, async () => ({
...(await vi.importActual(`@electric-sql/client`)),
ShapeStream: vi.fn(() => ({
subscribe: mockSubscribe,
unsubscribeAll: vi.fn(),
requestSnapshot: vi.fn(() => snapshotRequest?.() ?? Promise.resolve()),
fetchSnapshot: vi.fn().mockResolvedValue({ metadata: {}, data: [] }),
forceDisconnectAndRefresh: vi.fn().mockResolvedValue(undefined),
Expand Down
Loading