Skip to content

Commit 0f5d946

Browse files
authored
refactor: introduce PROJECTION_STATE_CLEAR_DELAY for improved subscription management (#391)
1 parent aaaf328 commit 0f5d946

File tree

3 files changed

+49
-27
lines changed

3 files changed

+49
-27
lines changed

packages/core/src/projection/getProjectionState.test.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {insecureRandomId} from '../utils/ids'
77
import {getProjectionState} from './getProjectionState'
88
import {type ProjectionStoreState} from './projectionStore'
99
import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
10-
import {STABLE_EMPTY_PROJECTION} from './util'
10+
import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION} from './util'
1111

1212
vi.mock('../utils/ids', async (importOriginal) => {
1313
const util = await importOriginal<typeof import('../utils/ids')>()
@@ -30,10 +30,12 @@ describe('getProjectionState', () => {
3030
})
3131

3232
instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
33+
vi.useFakeTimers() // Enable fake timers for each test
3334
})
3435

3536
afterEach(() => {
3637
instance.dispose()
38+
vi.useRealTimers() // Restore real timers after each test
3739
})
3840

3941
it('returns a state source that emits when the projection value changes', () => {
@@ -70,27 +72,33 @@ describe('getProjectionState', () => {
7072
const projectionState = getProjectionState(instance, {projection, ...docHandle})
7173

7274
expect(state.get().subscriptions).toEqual({})
73-
vi.mocked(insecureRandomId)
74-
.mockImplementationOnce(() => 'pseudoRandomId1')
75-
.mockImplementationOnce(() => 'pseudoRandomId2')
75+
vi.mocked(insecureRandomId).mockImplementationOnce(() => 'pseudoRandomId1')
7676

7777
const unsubscribe1 = projectionState.subscribe(vi.fn())
78-
const unsubscribe2 = projectionState.subscribe(vi.fn())
7978

8079
expect(state.get().subscriptions).toEqual({
81-
exampleId: {pseudoRandomId1: true, pseudoRandomId2: true},
80+
exampleId: {pseudoRandomId1: true},
8281
})
8382
expect(state.get().documentProjections).toEqual({
8483
exampleId: projection,
8584
})
8685

87-
unsubscribe2()
86+
// Unsubscribe the last one - state should NOT clear immediately
87+
unsubscribe1()
8888
expect(state.get().subscriptions).toEqual({
8989
exampleId: {pseudoRandomId1: true},
9090
})
91+
// Projection data might also remain initially
92+
expect(state.get().documentProjections).toEqual({
93+
exampleId: projection,
94+
})
9195

92-
unsubscribe1()
96+
// Advance timers past the clear delay
97+
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
98+
99+
// NOW the state related to this document should be cleared
93100
expect(state.get().subscriptions).toEqual({})
101+
expect(state.get().documentProjections).toEqual({exampleId: projection})
94102
})
95103

96104
it('resets to pending false on unsubscribe if the subscription is the last one', () => {
@@ -101,24 +109,35 @@ describe('getProjectionState', () => {
101109
}))
102110

103111
const unsubscribe1 = projectionState.subscribe(vi.fn())
104-
const unsubscribe2 = projectionState.subscribe(vi.fn())
105112

106113
expect(state.get().values[docHandle.documentId]).toEqual({
107114
data: {field: 'Foo'},
108115
isPending: true,
109116
})
110117

118+
// Unsubscribe one - pending state remains
111119
unsubscribe1()
112120
expect(state.get().values[docHandle.documentId]).toEqual({
113121
data: {field: 'Foo'},
114122
isPending: true,
115123
})
116124

117-
unsubscribe2()
118-
expect(state.get().subscriptions).toEqual({})
125+
// Unsubscribe the last one - pending state should NOT reset immediately
126+
expect(Object.keys(state.get().subscriptions['exampleId'] || {}).length).toBeGreaterThan(0)
119127
expect(state.get().values[docHandle.documentId]).toEqual({
120128
data: {field: 'Foo'},
121-
isPending: false,
129+
isPending: true, // Still pending
122130
})
131+
132+
// Advance timers past the clear delay
133+
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
134+
135+
// NOW the pending state should be reset
136+
expect(state.get().values[docHandle.documentId]).toEqual({
137+
data: {field: 'Foo'},
138+
isPending: false, // Reset to false
139+
})
140+
// And subscriptions should be cleared now
141+
expect(state.get().subscriptions).toEqual({})
123142
})
124143
})

packages/core/src/projection/getProjectionState.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
type ProjectionValuePending,
1616
type ValidProjection,
1717
} from './projectionStore'
18-
import {STABLE_EMPTY_PROJECTION, validateProjection} from './util'
18+
import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
1919

2020
interface GetProjectionStateOptions extends DocumentHandle {
2121
projection: ValidProjection
@@ -74,21 +74,23 @@ export const _getProjectionState = bindActionByDataset(
7474
}))
7575

7676
return () => {
77-
state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
78-
const documentSubscriptions = omit(prev.subscriptions[documentId], subscriptionId)
79-
const hasSubscribers = !!Object.keys(documentSubscriptions).length
80-
const prevValue = prev.values[documentId]
81-
const projectionValue = prevValue?.data ? prevValue.data : null
77+
setTimeout(() => {
78+
state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
79+
const documentSubscriptions = omit(prev.subscriptions[documentId], subscriptionId)
80+
const hasSubscribers = !!Object.keys(documentSubscriptions).length
81+
const prevValue = prev.values[documentId]
82+
const projectionValue = prevValue?.data ? prevValue.data : null
8283

83-
return {
84-
subscriptions: hasSubscribers
85-
? {...prev.subscriptions, [documentId]: documentSubscriptions}
86-
: omit(prev.subscriptions, documentId),
87-
values: hasSubscribers
88-
? prev.values
89-
: {...prev.values, [documentId]: {data: projectionValue, isPending: false}},
90-
}
91-
})
84+
return {
85+
subscriptions: hasSubscribers
86+
? {...prev.subscriptions, [documentId]: documentSubscriptions}
87+
: omit(prev.subscriptions, documentId),
88+
values: hasSubscribers
89+
? prev.values
90+
: {...prev.values, [documentId]: {data: projectionValue, isPending: false}},
91+
}
92+
})
93+
}, PROJECTION_STATE_CLEAR_DELAY)
9294
}
9395
},
9496
}),

packages/core/src/projection/util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {type ValidProjection} from './projectionStore'
22

33
export const PROJECTION_TAG = 'sdk.projection'
44
export const PROJECTION_PERSPECTIVE = 'drafts'
5+
export const PROJECTION_STATE_CLEAR_DELAY = 1000
56

67
export const STABLE_EMPTY_PROJECTION = {
78
data: null,

0 commit comments

Comments
 (0)