From 3d9cf1e3f85480c3ab03d8bb3cf8cd2091269476 Mon Sep 17 00:00:00 2001 From: msfstef Date: Tue, 29 Oct 2024 12:44:11 +0200 Subject: [PATCH 1/3] Clear cache when stream is errored --- .changeset/quiet-bees-prove.md | 5 ++ packages/react-hooks/src/react-hooks.tsx | 50 ++++++++++++------- .../react-hooks/test/react-hooks.test.tsx | 31 ++++++++++++ 3 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 .changeset/quiet-bees-prove.md 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..9310c0fd22 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) + const stream = streamCache.get(shapeHash)! as ShapeStream + // if stream is cached but errored/aborted, remove it and related shapes + if (stream.error !== undefined || stream.options.signal?.aborted) { + streamCache.delete(shapeHash) + shapeCache.delete(stream) + } else { + return stream + } + } - streamCache.set(shapeHash, newShapeStream) + const newShapeStream = new ShapeStream(options) - // Return the created shape - return newShapeStream - } + 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) + // if stream is cached but errored/aborted, remove it and related shapes + if ( + shapeStream.error !== undefined || + shapeStream.options.signal?.aborted + ) { + streamCache.delete(sortedOptionsHash(shapeStream.options)) + shapeCache.delete(shapeStream) + } else { + return shapeCache.get(shapeStream)! as Shape + } + } - shapeCache.set(shapeStream, newShape) + const newShape = new Shape(shapeStream) - // Return the created shape - return newShape - } + 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..792432136e 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.only(`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({ From cb77748ff26c23c33b5db9602e9960d73b3f5d28 Mon Sep 17 00:00:00 2001 From: msfstef Date: Tue, 29 Oct 2024 12:47:37 +0200 Subject: [PATCH 2/3] Remove `.only` modifier --- packages/react-hooks/test/react-hooks.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-hooks/test/react-hooks.test.tsx b/packages/react-hooks/test/react-hooks.test.tsx index 792432136e..35c5a3ce7d 100644 --- a/packages/react-hooks/test/react-hooks.test.tsx +++ b/packages/react-hooks/test/react-hooks.test.tsx @@ -60,7 +60,7 @@ describe(`useShape`, () => { ) }) - it.only(`should re-sync a shape after an interrupt`, async ({ + it(`should re-sync a shape after an interrupt`, async ({ aborter, issuesTableUrl, insertIssues, From 03f2125667941675e973e459d5a1aa8d39b66e33 Mon Sep 17 00:00:00 2001 From: msfstef Date: Tue, 29 Oct 2024 17:37:29 +0200 Subject: [PATCH 3/3] Slightly prettier version --- packages/react-hooks/src/react-hooks.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/react-hooks/src/react-hooks.tsx b/packages/react-hooks/src/react-hooks.tsx index 9310c0fd22..24a219ddee 100644 --- a/packages/react-hooks/src/react-hooks.tsx +++ b/packages/react-hooks/src/react-hooks.tsx @@ -35,13 +35,13 @@ export function getShapeStream>( // If the stream is already cached, return it if valid if (streamCache.has(shapeHash)) { const stream = streamCache.get(shapeHash)! as ShapeStream - // if stream is cached but errored/aborted, remove it and related shapes - if (stream.error !== undefined || stream.options.signal?.aborted) { - streamCache.delete(shapeHash) - shapeCache.delete(stream) - } else { + if (stream.error === undefined && !stream.options.signal?.aborted) { return stream } + + // if stream is cached but errored/aborted, remove it and related shapes + streamCache.delete(shapeHash) + shapeCache.delete(stream) } const newShapeStream = new ShapeStream(options) @@ -57,16 +57,16 @@ export function getShape>( ): Shape { // If the stream is already cached, return it if valid if (shapeCache.has(shapeStream)) { - // if stream is cached but errored/aborted, remove it and related shapes if ( - shapeStream.error !== undefined || - shapeStream.options.signal?.aborted + shapeStream.error === undefined && + !shapeStream.options.signal?.aborted ) { - streamCache.delete(sortedOptionsHash(shapeStream.options)) - shapeCache.delete(shapeStream) - } else { return shapeCache.get(shapeStream)! as Shape } + + // 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)