diff --git a/.changeset/quiet-bees-prove.md b/.changeset/quiet-bees-prove.md new file mode 100644 index 0000000000..a0d1832bee --- /dev/null +++ b/.changeset/quiet-bees-prove.md @@ -0,0 +1,5 @@ +--- +"@electric-sql/react": patch +--- + +Clear caches when cached stream is in errored state or is explicitly aborted diff --git a/packages/react-hooks/src/react-hooks.tsx b/packages/react-hooks/src/react-hooks.tsx index adcdb77fb6..24a219ddee 100644 --- a/packages/react-hooks/src/react-hooks.tsx +++ b/packages/react-hooks/src/react-hooks.tsx @@ -32,35 +32,49 @@ export function getShapeStream>( ): ShapeStream { const shapeHash = sortedOptionsHash(options) - // If the stream is already cached, return + // If the stream is already cached, return it if valid if (streamCache.has(shapeHash)) { - // Return the ShapeStream - return streamCache.get(shapeHash)! as ShapeStream - } else { - const newShapeStream = new ShapeStream(options) - - streamCache.set(shapeHash, newShapeStream) + const stream = streamCache.get(shapeHash)! as ShapeStream + if (stream.error === undefined && !stream.options.signal?.aborted) { + return stream + } - // Return the created shape - return newShapeStream + // if stream is cached but errored/aborted, remove it and related shapes + streamCache.delete(shapeHash) + shapeCache.delete(stream) } + + const newShapeStream = new ShapeStream(options) + + streamCache.set(shapeHash, newShapeStream) + + // Return the created shape + return newShapeStream } export function getShape>( shapeStream: ShapeStream ): Shape { - // If the stream is already cached, return + // If the stream is already cached, return it if valid if (shapeCache.has(shapeStream)) { - // Return the ShapeStream - return shapeCache.get(shapeStream)! as Shape - } else { - const newShape = new Shape(shapeStream) - - shapeCache.set(shapeStream, newShape) + if ( + shapeStream.error === undefined && + !shapeStream.options.signal?.aborted + ) { + return shapeCache.get(shapeStream)! as Shape + } - // Return the created shape - return newShape + // if stream is cached but errored/aborted, remove it and related shapes + streamCache.delete(sortedOptionsHash(shapeStream.options)) + shapeCache.delete(shapeStream) } + + const newShape = new Shape(shapeStream) + + shapeCache.set(shapeStream, newShape) + + // Return the created shape + return newShape } export interface UseShapeResult = Row> { diff --git a/packages/react-hooks/test/react-hooks.test.tsx b/packages/react-hooks/test/react-hooks.test.tsx index 84df2860b6..35c5a3ce7d 100644 --- a/packages/react-hooks/test/react-hooks.test.tsx +++ b/packages/react-hooks/test/react-hooks.test.tsx @@ -60,6 +60,37 @@ describe(`useShape`, () => { ) }) + it(`should re-sync a shape after an interrupt`, async ({ + aborter, + issuesTableUrl, + insertIssues, + }) => { + const manualAborter = new AbortController() + renderHook(() => + useShape({ + url: `${BASE_URL}/v1/shape/${issuesTableUrl}`, + signal: manualAborter.signal, + subscribe: false, + }) + ) + + manualAborter.abort() + + const [id] = await insertIssues({ title: `test row` }) + + const { result } = renderHook(() => + useShape({ + url: `${BASE_URL}/v1/shape/${issuesTableUrl}`, + signal: aborter?.signal, + subscribe: false, + }) + ) + + await waitFor(() => + expect(result.current.data).toEqual([{ id: id, title: `test row` }]) + ) + }) + it(`should expose isLoading status`, async ({ issuesTableUrl }) => { const { result } = renderHook(() => useShape({