From c4a29c20e1fb4edec167fce5acc25b729d064424 Mon Sep 17 00:00:00 2001 From: dfliess Date: Sat, 26 Sep 2026 09:09:52 +0200 Subject: [PATCH 1/2] fix(canvas): release the markdown template's subscription to the canvas spec getResolveTemplatedStringQueryOptions subscribed to an inner derived store on every filter change without returning the unsubscriber, leaving one more live subscription to the parent canvas spec each time. --- .../canvas/components/markdown/util.spec.ts | 52 +++++++++++++++++++ .../canvas/components/markdown/util.ts | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 web-common/src/features/canvas/components/markdown/util.spec.ts diff --git a/web-common/src/features/canvas/components/markdown/util.spec.ts b/web-common/src/features/canvas/components/markdown/util.spec.ts new file mode 100644 index 000000000000..0dff6a12cedf --- /dev/null +++ b/web-common/src/features/canvas/components/markdown/util.spec.ts @@ -0,0 +1,52 @@ +import type { MarkdownCanvasComponent } from "@rilldata/web-common/features/canvas/components/markdown"; +import { getResolveTemplatedStringQueryOptions } from "@rilldata/web-common/features/canvas/components/markdown/util"; +import type { V1Expression } from "@rilldata/web-common/runtime-client"; +import { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; +import { writable } from "svelte/store"; +import { describe, expect, it } from "vitest"; + +describe("getResolveTemplatedStringQueryOptions", () => { + it("Should release the parent spec once unsubscribed, however often the filters changed", () => { + const exprByMetricsView = writable>({}); + + // Counts the live subscriptions to the parent canvas spec. + let parentSpecSubscribers = 0; + const parentSpec = writable({ data: { metricsViews: {} } }); + const parentSpecStore = { + subscribe: (run: (value: unknown) => void) => { + parentSpecSubscribers++; + const unsubscribe = parentSpec.subscribe(run); + return () => { + parentSpecSubscribers--; + unsubscribe(); + }; + }, + }; + + const component = { + specStore: writable({ content: "{{ metrics_sql 'select 1' }}" }), + timeAndFilterStore: writable({ timeRange: undefined }), + parent: { + specStore: parentSpecStore, + expressionFilterManager: { + exprByMetricsViewStore: exprByMetricsView, + }, + timeManager: { hasTimeSeriesStore: writable(false) }, + }, + } as unknown as MarkdownCanvasComponent; + + const optionsStore = getResolveTemplatedStringQueryOptions( + component, + new RuntimeClient({ host: "http://localhost", instanceId: "test" }), + ); + + const unsubscribe = optionsStore.subscribe(() => {}); + // Every filter change re-runs the outer derived, which builds a new inner one. + exprByMetricsView.set({ mv: { ident: "a" } }); + exprByMetricsView.set({ mv: { ident: "b" } }); + expect(parentSpecSubscribers).toBe(1); + + unsubscribe(); + expect(parentSpecSubscribers).toBe(0); + }); +}); diff --git a/web-common/src/features/canvas/components/markdown/util.ts b/web-common/src/features/canvas/components/markdown/util.ts index 0bb22347f512..832924f2ccaf 100644 --- a/web-common/src/features/canvas/components/markdown/util.ts +++ b/web-common/src/features/canvas/components/markdown/util.ts @@ -127,7 +127,7 @@ export function getResolveTemplatedStringQueryOptions( return derived( [component.parent.expressionFilterManager.exprByMetricsViewStore], ([exprByMetricsView], set) => { - derived( + return derived( [ component.specStore, component.timeAndFilterStore, From 3eec1bc720a48722c8dff54bbf4a0e069876aedc Mon Sep 17 00:00:00 2001 From: dfliess Date: Tue, 6 Oct 2026 10:40:49 +0200 Subject: [PATCH 2/2] fix: return the inner subscriptions in the column selectors and unfiltered totals useAllModelColumns, useAllSourceColumns and createUnfilteredTotalsForMeasure subscribed to an inner store inside a derived callback without returning the unsubscriber, so every re-run left the previous subscription alive. --- .../src/features/dashboards/time-series/totals-data-store.ts | 2 +- web-common/src/features/models/selectors.ts | 2 +- web-common/src/features/sources/selectors.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web-common/src/features/dashboards/time-series/totals-data-store.ts b/web-common/src/features/dashboards/time-series/totals-data-store.ts index 0b677d741908..531b3d2c460e 100644 --- a/web-common/src/features/dashboards/time-series/totals-data-store.ts +++ b/web-common/src/features/dashboards/time-series/totals-data-store.ts @@ -58,7 +58,7 @@ export function createUnfilteredTotalsForMeasure( const updatedFilter = getFiltersForOtherDimensions(filter, dimensionName); - createQueryServiceMetricsViewAggregation( + return createQueryServiceMetricsViewAggregation( ctx.runtimeClient, { metricsView: metricsViewName, diff --git a/web-common/src/features/models/selectors.ts b/web-common/src/features/models/selectors.ts index b58dd37fd71d..ceec98488691 100644 --- a/web-common/src/features/models/selectors.ts +++ b/web-common/src/features/models/selectors.ts @@ -36,7 +36,7 @@ export function useAllModelColumns( return; } - derived( + return derived( allModels.data.map((r) => createTableColumnsWithName( queryClient, diff --git a/web-common/src/features/sources/selectors.ts b/web-common/src/features/sources/selectors.ts index 7469e7016120..33a27b3de36f 100644 --- a/web-common/src/features/sources/selectors.ts +++ b/web-common/src/features/sources/selectors.ts @@ -84,7 +84,7 @@ export function useAllSourceColumns( return; } - derived( + return derived( allSources.data.map((r) => createTableColumnsWithName( queryClient,