From 9dc1e9f07c3cd9ea1400028466cbc8db8037beea Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 09:48:45 +0300 Subject: [PATCH 01/11] refactor: vue implementation --- packages/vue-db/package.json | 4 +- packages/vue-db/src/useLiveQuery.ts | 73 +++++++++++++++------- packages/vue-db/src/useStore.ts | 49 +++++++++++++++ packages/vue-db/tests/useLiveQuery.test.ts | 49 +++++++++------ pnpm-lock.yaml | 3 + 5 files changed, 133 insertions(+), 45 deletions(-) create mode 100644 packages/vue-db/src/useStore.ts diff --git a/packages/vue-db/package.json b/packages/vue-db/package.json index acd8bca..30eee4a 100644 --- a/packages/vue-db/package.json +++ b/packages/vue-db/package.json @@ -18,12 +18,12 @@ "packageManager": "pnpm@10.5.2", "dependencies": { "@tanstack/db": "workspace:*", - "@tanstack/vue-store": "^0.7.0" + "@tanstack/store": "^0.7.0" }, "devDependencies": { "@electric-sql/client": "1.0.0", - "@vitest/coverage-istanbul": "^3.0.9", "@vitejs/plugin-vue": "^5.2.4", + "@vitest/coverage-istanbul": "^3.0.9", "vue": "^3.5.13" }, "exports": { diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 0fea147..04503cc 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -1,6 +1,6 @@ -import { computed, toValue, watch } from "vue" -import { useStore } from "@tanstack/vue-store" +import { onWatcherCleanup, shallowRef, toValue, watchEffect } from "vue" import { compileQuery, queryBuilder } from "@tanstack/db" +import { shallow } from "./useStore" import type { Collection, Context, @@ -9,12 +9,12 @@ import type { ResultsFromContext, Schema, } from "@tanstack/db" -import type { ComputedRef, MaybeRefOrGetter } from "vue" +import type { MaybeRefOrGetter, Ref } from "vue" export interface UseLiveQueryReturn { - state: ComputedRef> - data: ComputedRef> - collection: ComputedRef> + state: Ref> + data: Ref> + collection: () => Collection } export function useLiveQuery< @@ -23,38 +23,63 @@ export function useLiveQuery< queryFn: ( q: InitialQueryBuilder> ) => QueryBuilder, - deps: Array> = [] + deps: () => Array> = () => [] ): UseLiveQueryReturn> { - const compiledQuery = computed(() => { - // Just reference deps to make computed reactive to them - deps.forEach((dep) => toValue(dep)) + const compiledQuery = shallowRef() as Ref< + ReturnType> + > + + watchEffect(() => { + toValue(deps) const query = queryFn(queryBuilder()) const compiled = compileQuery(query) compiled.start() - return compiled - }) - const state = computed(() => { - return useStore(compiledQuery.value.results.derivedState).value - }) - const data = computed(() => { - return useStore(compiledQuery.value.results.derivedArray).value + compiledQuery.value = compiled + + onWatcherCleanup(compiled.stop) }) - watch(compiledQuery, (newQuery, oldQuery, onInvalidate) => { - if (newQuery.state === `stopped`) { - newQuery.start() - } + let stateRef: Map> + let dataRef: Array> + const state = shallowRef(stateRef!) + const data = shallowRef(dataRef!) + + watchEffect(() => { + const results = compiledQuery.value.results + const derivedState = results.derivedState + const derivedArray = results.derivedArray + stateRef = derivedState.state + dataRef = derivedArray.state + state.value = stateRef + data.value = dataRef + + const unsubDerivedState = derivedState.subscribe(() => { + const newValue = derivedState.state + if (shallow(stateRef, newValue)) return + + stateRef = newValue + state.value = newValue + }) + + const unsubDerivedArray = derivedArray.subscribe(() => { + const newValue = derivedArray.state + if (shallow(dataRef, newValue)) return + + dataRef = newValue + data.value = newValue + }) - onInvalidate(() => { - oldQuery.stop() + onWatcherCleanup(() => { + unsubDerivedState() + unsubDerivedArray() }) }) return { state, data, - collection: computed(() => compiledQuery.value.results), + collection: () => compiledQuery.value.results, } } diff --git a/packages/vue-db/src/useStore.ts b/packages/vue-db/src/useStore.ts new file mode 100644 index 0000000..15a9651 --- /dev/null +++ b/packages/vue-db/src/useStore.ts @@ -0,0 +1,49 @@ +/** + * @see https://github.com/TanStack/store/blob/cf37b85ddecdcb6f52ad930dcd53e294fb4b03a7/packages/vue-store/src/index.ts#L47 + */ + +export function shallow(objA: T, objB: T) { + if (Object.is(objA, objB)) { + return true + } + + if ( + typeof objA !== `object` || + objA === null || + typeof objB !== `object` || + objB === null + ) { + return false + } + + if (objA instanceof Map && objB instanceof Map) { + if (objA.size !== objB.size) return false + for (const [k, v] of objA) { + if (!objB.has(k) || !Object.is(v, objB.get(k))) return false + } + return true + } + + if (objA instanceof Set && objB instanceof Set) { + if (objA.size !== objB.size) return false + for (const v of objA) { + if (!objB.has(v)) return false + } + return true + } + + const keysA = Object.keys(objA) + if (keysA.length !== Object.keys(objB).length) { + return false + } + + for (const keyA of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, keyA) || + !Object.is(objA[keyA as keyof T], objB[keyA as keyof T]) + ) { + return false + } + } + return true +} diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index e41db5c..43418cf 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -1,7 +1,14 @@ import { describe, expect, it, vi } from "vitest" import mitt from "mitt" import { Collection, createTransaction } from "@tanstack/db" -import { computed, onUnmounted, ref, watch, watchEffect } from "vue" +import { + computed, + onUnmounted, + onWatcherCleanup, + ref, + watch, + watchEffect, +} from "vue" import { useLiveQuery } from "../src/useLiveQuery" import type { Ref } from "vue" import type { @@ -411,15 +418,20 @@ describe(`Query Collections`, () => { })) ) + await waitForChanges() + const minAge = ref(30) - const { state } = useLiveQuery((q) => { - return q - .from({ collection }) - .where(`@age`, `>`, minAge.value) - .keyBy(`@id`) - .select(`@id`, `@name`, `@age`) - }) + const { state } = useLiveQuery( + (q) => { + return q + .from({ collection }) + .where(`@age`, `>`, minAge.value) + .keyBy(`@id`) + .select(`@id`, `@name`, `@age`) + }, + () => [minAge.value] + ) // Initially should return only people older than 30 expect(state.value.size).toBe(1) @@ -495,18 +507,17 @@ describe(`Query Collections`, () => { // Add a custom hook that wraps useLiveQuery to log when queries are created and stopped function useTrackedLiveQuery( queryFn: (q: InitialQueryBuilder>) => any, - deps: Array> + deps: () => Array ): T { const result = useLiveQuery(queryFn, deps) - watch( - () => deps.map((dep) => dep.value).join(`,`), - (updatedDeps, _, fn) => { - console.log(`Creating new query with deps`, updatedDeps) - fn(() => console.log(`Stopping query with deps`, updatedDeps)) - }, - { immediate: true } - ) + watchEffect(() => { + const updatedDeps = deps().join(`,`) + console.log(`Creating new query with deps`, updatedDeps) + onWatcherCleanup(() => + console.log(`Stopping query with deps`, updatedDeps) + ) + }) return result as T } @@ -529,7 +540,7 @@ describe(`Query Collections`, () => { .where(`@age`, `>`, minAge.value) .keyBy(`@id`) .select(`@id`, `@name`), - [minAge] + () => [minAge.value] ) // Initial query should be created @@ -604,7 +615,7 @@ describe(`Query Collections`, () => { // Grouped query derived from initial query const groupedResult = useLiveQuery((q) => q - .from({ queryResult: result.collection.value }) + .from({ queryResult: result.collection() }) .groupBy(`@team`) .keyBy(`@team`) .select(`@team`, { count: { COUNT: `@id` } }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e69fa5d..7c18c0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,6 +286,9 @@ importers: '@tanstack/db': specifier: workspace:* version: link:../db + '@tanstack/store': + specifier: ^0.7.0 + version: 0.7.0 '@tanstack/vue-store': specifier: ^0.7.0 version: 0.7.0(vue@3.5.13(typescript@5.8.2)) From dd7a8dab078315b7cb57d7859a2998ee43ddb957 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 09:53:03 +0300 Subject: [PATCH 02/11] refactor: vue implementation --- packages/vue-db/src/useLiveQuery.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 04503cc..240143a 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -29,6 +29,11 @@ export function useLiveQuery< ReturnType> > + let stateRef: Map> + let dataRef: Array> + const state = shallowRef(stateRef!) + const data = shallowRef(dataRef!) + watchEffect(() => { toValue(deps) @@ -38,16 +43,7 @@ export function useLiveQuery< compiledQuery.value = compiled - onWatcherCleanup(compiled.stop) - }) - - let stateRef: Map> - let dataRef: Array> - const state = shallowRef(stateRef!) - const data = shallowRef(dataRef!) - - watchEffect(() => { - const results = compiledQuery.value.results + const results = compiled.results const derivedState = results.derivedState const derivedArray = results.derivedArray stateRef = derivedState.state @@ -72,6 +68,7 @@ export function useLiveQuery< }) onWatcherCleanup(() => { + compiled.stop() unsubDerivedState() unsubDerivedArray() }) From 1ebf8cc390a58b5ca5e250773d15e9b9bbac4831 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 09:55:46 +0300 Subject: [PATCH 03/11] refactor: vue implementation --- packages/vue-db/src/useLiveQuery.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 240143a..fb4b067 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -29,10 +29,10 @@ export function useLiveQuery< ReturnType> > - let stateRef: Map> - let dataRef: Array> - const state = shallowRef(stateRef!) - const data = shallowRef(dataRef!) + const state = shallowRef() as Ref< + Map> + > + const data = shallowRef() as Ref>> watchEffect(() => { toValue(deps) @@ -46,8 +46,8 @@ export function useLiveQuery< const results = compiled.results const derivedState = results.derivedState const derivedArray = results.derivedArray - stateRef = derivedState.state - dataRef = derivedArray.state + let stateRef = derivedState.state + let dataRef = derivedArray.state state.value = stateRef data.value = dataRef From dcfe6ff21d52bbf2946ca3537145979041fcf363 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 10:00:38 +0300 Subject: [PATCH 04/11] refactor: vue implementation --- packages/vue-db/package.json | 3 +-- packages/vue-db/tsconfig.json | 1 - pnpm-lock.yaml | 36 ----------------------------------- 3 files changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/vue-db/package.json b/packages/vue-db/package.json index 30eee4a..0326cfb 100644 --- a/packages/vue-db/package.json +++ b/packages/vue-db/package.json @@ -17,8 +17,7 @@ ], "packageManager": "pnpm@10.5.2", "dependencies": { - "@tanstack/db": "workspace:*", - "@tanstack/store": "^0.7.0" + "@tanstack/db": "workspace:*" }, "devDependencies": { "@electric-sql/client": "1.0.0", diff --git a/packages/vue-db/tsconfig.json b/packages/vue-db/tsconfig.json index ea04dcd..eb10b7f 100644 --- a/packages/vue-db/tsconfig.json +++ b/packages/vue-db/tsconfig.json @@ -11,7 +11,6 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "paths": { - "@tanstack/store": ["../store/src"], "@tanstack/db": ["../db/src"] } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c18c0d..c231531 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,12 +286,6 @@ importers: '@tanstack/db': specifier: workspace:* version: link:../db - '@tanstack/store': - specifier: ^0.7.0 - version: 0.7.0 - '@tanstack/vue-store': - specifier: ^0.7.0 - version: 0.7.0(vue@3.5.13(typescript@5.8.2)) devDependencies: '@electric-sql/client': specifier: 1.0.0 @@ -1639,15 +1633,6 @@ packages: resolution: {integrity: sha512-G6l2Q4hp/Yj43UyE9APz+Fj5sdC15G2UD2xXOSdQCZ6/4gjYd2c040TLk7ObGhypbeWBYvy93Gg18nS41F6eqg==} engines: {node: '>=18'} - '@tanstack/vue-store@0.7.0': - resolution: {integrity: sha512-oLB/WuD26caR86rxLz39LvS5YdY0KIThJFEHIW/mXujC2+M/z3GxVZFJsZianAzr3tH56sZQ8kkq4NvwwsOBkQ==} - peerDependencies: - '@vue/composition-api': ^1.2.1 - vue: ^2.5.0 || ^3.0.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -4715,17 +4700,6 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue-demi@0.14.10: - resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -6093,12 +6067,6 @@ snapshots: - typescript - vite - '@tanstack/vue-store@0.7.0(vue@3.5.13(typescript@5.8.2))': - dependencies: - '@tanstack/store': 0.7.0 - vue: 3.5.13(typescript@5.8.2) - vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.2)) - '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 @@ -9465,10 +9433,6 @@ snapshots: vscode-uri@3.1.0: {} - vue-demi@0.14.10(vue@3.5.13(typescript@5.8.2)): - dependencies: - vue: 3.5.13(typescript@5.8.2) - vue-eslint-parser@9.4.3(eslint@9.22.0(jiti@2.4.2)): dependencies: debug: 4.4.0 From 8fe743136f37fa72efacf03b5c063bea04642307 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 10:01:49 +0300 Subject: [PATCH 05/11] refactor: vue implementation --- packages/vue-db/tests/useLiveQuery.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 43418cf..534caf4 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -1,16 +1,8 @@ import { describe, expect, it, vi } from "vitest" import mitt from "mitt" import { Collection, createTransaction } from "@tanstack/db" -import { - computed, - onUnmounted, - onWatcherCleanup, - ref, - watch, - watchEffect, -} from "vue" +import { onWatcherCleanup, ref, watchEffect } from "vue" import { useLiveQuery } from "../src/useLiveQuery" -import type { Ref } from "vue" import type { Context, InitialQueryBuilder, From eef26680a866bcb3bc31d2305b4b6a6839bbcbbd Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 10:07:35 +0300 Subject: [PATCH 06/11] refactor: vue implementation --- packages/vue-db/src/useLiveQuery.ts | 20 ++++++++++---------- packages/vue-db/tests/useLiveQuery.test.ts | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index fb4b067..65ad1be 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -12,9 +12,9 @@ import type { import type { MaybeRefOrGetter, Ref } from "vue" export interface UseLiveQueryReturn { - state: Ref> - data: Ref> - collection: () => Collection + state: Readonly>> + data: Readonly>> + collection: Readonly>> } export function useLiveQuery< @@ -25,8 +25,8 @@ export function useLiveQuery< ) => QueryBuilder, deps: () => Array> = () => [] ): UseLiveQueryReturn> { - const compiledQuery = shallowRef() as Ref< - ReturnType> + const results = shallowRef() as Ref< + ReturnType>[`results`] > const state = shallowRef() as Ref< @@ -41,11 +41,11 @@ export function useLiveQuery< const compiled = compileQuery(query) compiled.start() - compiledQuery.value = compiled + const resultsRef = compiled.results + results.value = resultsRef - const results = compiled.results - const derivedState = results.derivedState - const derivedArray = results.derivedArray + const derivedState = resultsRef.derivedState + const derivedArray = resultsRef.derivedArray let stateRef = derivedState.state let dataRef = derivedArray.state state.value = stateRef @@ -77,6 +77,6 @@ export function useLiveQuery< return { state, data, - collection: () => compiledQuery.value.results, + collection: results, } } diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 534caf4..607f244 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest" import mitt from "mitt" import { Collection, createTransaction } from "@tanstack/db" import { onWatcherCleanup, ref, watchEffect } from "vue" -import { useLiveQuery } from "../src/useLiveQuery" +import { useLiveQuery } from "../src/index" import type { Context, InitialQueryBuilder, @@ -607,7 +607,7 @@ describe(`Query Collections`, () => { // Grouped query derived from initial query const groupedResult = useLiveQuery((q) => q - .from({ queryResult: result.collection() }) + .from({ queryResult: result.collection.value }) .groupBy(`@team`) .keyBy(`@team`) .select(`@team`, { count: { COUNT: `@id` } }) From 4947adafcf22747c7f7dee81a4bf4d4f18b93eaf Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 10:11:01 +0300 Subject: [PATCH 07/11] refactor: vue implementation --- packages/vue-db/src/useLiveQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 65ad1be..8a2a440 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -23,7 +23,7 @@ export function useLiveQuery< queryFn: ( q: InitialQueryBuilder> ) => QueryBuilder, - deps: () => Array> = () => [] + deps: () => Array = () => [] ): UseLiveQueryReturn> { const results = shallowRef() as Ref< ReturnType>[`results`] From 9e5caf6bf7053e9d721355da882c2f112466d97c Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 10:12:56 +0300 Subject: [PATCH 08/11] refactor: vue implementation --- packages/vue-db/src/useLiveQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 8a2a440..c8d6665 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -9,7 +9,7 @@ import type { ResultsFromContext, Schema, } from "@tanstack/db" -import type { MaybeRefOrGetter, Ref } from "vue" +import type { Ref } from "vue" export interface UseLiveQueryReturn { state: Readonly>> From 0d7b1a224bf3c8d7289287c0fb94d3bb784c9fc1 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Thu, 15 May 2025 21:23:32 +0300 Subject: [PATCH 09/11] refator: remove deps --- packages/vue-db/src/useLiveQuery.ts | 7 ++----- packages/vue-db/tests/useLiveQuery.test.ts | 19 ++++++++----------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index c8d6665..5c7957c 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -1,4 +1,4 @@ -import { onWatcherCleanup, shallowRef, toValue, watchEffect } from "vue" +import { onWatcherCleanup, shallowRef, watchEffect } from "vue" import { compileQuery, queryBuilder } from "@tanstack/db" import { shallow } from "./useStore" import type { @@ -22,8 +22,7 @@ export function useLiveQuery< >( queryFn: ( q: InitialQueryBuilder> - ) => QueryBuilder, - deps: () => Array = () => [] + ) => QueryBuilder ): UseLiveQueryReturn> { const results = shallowRef() as Ref< ReturnType>[`results`] @@ -35,8 +34,6 @@ export function useLiveQuery< const data = shallowRef() as Ref>> watchEffect(() => { - toValue(deps) - const query = queryFn(queryBuilder()) const compiled = compileQuery(query) compiled.start() diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 607f244..c898a20 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -414,16 +414,13 @@ describe(`Query Collections`, () => { const minAge = ref(30) - const { state } = useLiveQuery( - (q) => { - return q - .from({ collection }) - .where(`@age`, `>`, minAge.value) - .keyBy(`@id`) - .select(`@id`, `@name`, `@age`) - }, - () => [minAge.value] - ) + const { state } = useLiveQuery((q) => { + return q + .from({ collection }) + .where(`@age`, `>`, minAge.value) + .keyBy(`@id`) + .select(`@id`, `@name`, `@age`) + }) // Initially should return only people older than 30 expect(state.value.size).toBe(1) @@ -501,7 +498,7 @@ describe(`Query Collections`, () => { queryFn: (q: InitialQueryBuilder>) => any, deps: () => Array ): T { - const result = useLiveQuery(queryFn, deps) + const result = useLiveQuery(queryFn) watchEffect(() => { const updatedDeps = deps().join(`,`) From efe925017f550635004e15b8673cdb99d76ccde9 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Sat, 17 May 2025 19:55:01 +0300 Subject: [PATCH 10/11] feat: change api --- packages/vue-db/src/useLiveQuery.ts | 24 ++---- packages/vue-db/tests/useLiveQuery.test.ts | 96 +++++++++++----------- 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 5c7957c..9254a52 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -9,12 +9,11 @@ import type { ResultsFromContext, Schema, } from "@tanstack/db" -import type { Ref } from "vue" export interface UseLiveQueryReturn { - state: Readonly>> - data: Readonly>> - collection: Readonly>> + state: () => Readonly> + data: () => Readonly> + collection: () => Collection } export function useLiveQuery< @@ -24,14 +23,9 @@ export function useLiveQuery< q: InitialQueryBuilder> ) => QueryBuilder ): UseLiveQueryReturn> { - const results = shallowRef() as Ref< - ReturnType>[`results`] - > - - const state = shallowRef() as Ref< - Map> - > - const data = shallowRef() as Ref>> + const results = shallowRef() + const state = shallowRef() + const data = shallowRef() watchEffect(() => { const query = queryFn(queryBuilder()) @@ -72,8 +66,8 @@ export function useLiveQuery< }) return { - state, - data, - collection: results, + state: () => state.value, + data: () => data.value, + collection: () => results.value, } } diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index c898a20..d5e6eff 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -118,15 +118,15 @@ describe(`Query Collections`, () => { .orderBy({ "@id": `asc` }) ) - expect(state.value.size).toBe(1) - expect(state.value.get(`3`)).toEqual({ + expect(state().size).toBe(1) + expect(state().get(`3`)).toEqual({ _orderByIndex: 0, id: `3`, name: `John Smith`, }) - expect(data.value.length).toBe(1) - expect(data.value[0]).toEqual({ + expect(data().length).toBe(1) + expect(data()[0]).toEqual({ _orderByIndex: 0, id: `3`, name: `John Smith`, @@ -149,25 +149,25 @@ describe(`Query Collections`, () => { await waitForChanges() - expect(state.value.size).toBe(2) - expect(state.value.get(`3`)).toEqual({ + expect(state().size).toBe(2) + expect(state().get(`3`)).toEqual({ _orderByIndex: 0, id: `3`, name: `John Smith`, }) - expect(state.value.get(`4`)).toEqual({ + expect(state().get(`4`)).toEqual({ _orderByIndex: 1, id: `4`, name: `Kyle Doe`, }) - expect(data.value.length).toBe(2) - expect(data.value).toContainEqual({ + expect(data().length).toBe(2) + expect(data()).toContainEqual({ _orderByIndex: 0, id: `3`, name: `John Smith`, }) - expect(data.value).toContainEqual({ + expect(data()).toContainEqual({ _orderByIndex: 1, id: `4`, name: `Kyle Doe`, @@ -186,15 +186,15 @@ describe(`Query Collections`, () => { await waitForChanges() - expect(state.value.size).toBe(2) - expect(state.value.get(`4`)).toEqual({ + expect(state().size).toBe(2) + expect(state().get(`4`)).toEqual({ _orderByIndex: 1, id: `4`, name: `Kyle Doe 2`, }) - expect(data.value.length).toBe(2) - expect(data.value).toContainEqual({ + expect(data().length).toBe(2) + expect(data()).toContainEqual({ _orderByIndex: 1, id: `4`, name: `Kyle Doe 2`, @@ -210,11 +210,11 @@ describe(`Query Collections`, () => { await waitForChanges() - expect(state.value.size).toBe(1) - expect(state.value.get(`4`)).toBeUndefined() + expect(state().size).toBe(1) + expect(state().get(`4`)).toBeUndefined() - expect(data.value.length).toBe(1) - expect(data.value).toContainEqual({ + expect(data().length).toBe(1) + expect(data()).toContainEqual({ _orderByIndex: 0, id: `3`, name: `John Smith`, @@ -299,21 +299,21 @@ describe(`Query Collections`, () => { await waitForChanges() // Verify that we have the expected joined results - expect(state.value.size).toBe(3) + expect(state().size).toBe(3) - expect(state.value.get(`1`)).toEqual({ + expect(state().get(`1`)).toEqual({ id: `1`, name: `John Doe`, title: `Issue 1`, }) - expect(state.value.get(`2`)).toEqual({ + expect(state().get(`2`)).toEqual({ id: `2`, name: `Jane Doe`, title: `Issue 2`, }) - expect(state.value.get(`3`)).toEqual({ + expect(state().get(`3`)).toEqual({ id: `3`, name: `John Doe`, title: `Issue 3`, @@ -335,8 +335,8 @@ describe(`Query Collections`, () => { await waitForChanges() - expect(state.value.size).toBe(4) - expect(state.value.get(`4`)).toEqual({ + expect(state().size).toBe(4) + expect(state().get(`4`)).toEqual({ id: `4`, name: `Jane Doe`, title: `Issue 4`, @@ -356,7 +356,7 @@ describe(`Query Collections`, () => { await waitForChanges() // The updated title should be reflected in the joined results - expect(state.value.get(`2`)).toEqual({ + expect(state().get(`2`)).toEqual({ id: `2`, name: `Jane Doe`, title: `Updated Issue 2`, @@ -373,7 +373,7 @@ describe(`Query Collections`, () => { await waitForChanges() // After deletion, user 3 should no longer have a joined result - expect(state.value.get(`3`)).toBeUndefined() + expect(state().get(`3`)).toBeUndefined() }) it(`should recompile query when parameters change and change results`, async () => { @@ -423,8 +423,8 @@ describe(`Query Collections`, () => { }) // Initially should return only people older than 30 - expect(state.value.size).toBe(1) - expect(state.value.get(`3`)).toEqual({ + expect(state().size).toBe(1) + expect(state().get(`3`)).toEqual({ id: `3`, name: `John Smith`, age: 35, @@ -436,18 +436,18 @@ describe(`Query Collections`, () => { await waitForChanges() // Now should return all people as they're all older than 20 - expect(state.value.size).toBe(3) - expect(state.value.get(`1`)).toEqual({ + expect(state().size).toBe(3) + expect(state().get(`1`)).toEqual({ id: `1`, name: `John Doe`, age: 30, }) - expect(state.value.get(`2`)).toEqual({ + expect(state().get(`2`)).toEqual({ id: `2`, name: `Jane Doe`, age: 25, }) - expect(state.value.get(`3`)).toEqual({ + expect(state().get(`3`)).toEqual({ id: `3`, name: `John Smith`, age: 35, @@ -459,7 +459,7 @@ describe(`Query Collections`, () => { await waitForChanges() // Should now be empty - expect(state.value.size).toBe(0) + expect(state().size).toBe(0) }) it(`should stop old query when parameters change`, async () => { @@ -604,15 +604,15 @@ describe(`Query Collections`, () => { // Grouped query derived from initial query const groupedResult = useLiveQuery((q) => q - .from({ queryResult: result.collection.value }) + .from({ queryResult: result.collection() }) .groupBy(`@team`) .keyBy(`@team`) .select(`@team`, { count: { COUNT: `@id` } }) ) // Verify initial grouped results - expect(groupedResult.state.value.size).toBe(1) - expect(groupedResult.state.value.get(`team1`)).toEqual({ + expect(groupedResult.state().size).toBe(1) + expect(groupedResult.state().get(`team1`)).toEqual({ team: `team1`, count: 1, }) @@ -648,12 +648,12 @@ describe(`Query Collections`, () => { await waitForChanges() // Verify the grouped results include the new team members - expect(groupedResult.state.value.size).toBe(2) - expect(groupedResult.state.value.get(`team1`)).toEqual({ + expect(groupedResult.state().size).toBe(2) + expect(groupedResult.state().get(`team1`)).toEqual({ team: `team1`, count: 2, }) - expect(groupedResult.state.value.get(`team2`)).toEqual({ + expect(groupedResult.state().get(`team2`)).toEqual({ team: `team2`, count: 1, }) @@ -747,9 +747,9 @@ describe(`Query Collections`, () => { // Track each render state watchEffect(() => { renderStates.push({ - stateSize: state.value.size, - hasTempKey: state.value.has(`temp-key`), - hasPermKey: state.value.has(`4`), + stateSize: state().size, + hasTempKey: state().has(`temp-key`), + hasPermKey: state().has(`4`), timestamp: Date.now(), }) }) @@ -757,7 +757,7 @@ describe(`Query Collections`, () => { await waitForChanges() // Verify initial state - expect(state.value.size).toBe(3) + expect(state().size).toBe(3) // Reset render states array for clarity in the remaining test renderStates.length = 0 @@ -795,8 +795,8 @@ describe(`Query Collections`, () => { ) // Verify optimistic state is immediately reflected - expect(state.value.size).toBe(4) - expect(state.value.get(`temp-key`)).toEqual({ + expect(state().size).toBe(4) + expect(state().get(`temp-key`)).toEqual({ id: `temp-key`, name: `John Doe`, title: `New Issue`, @@ -814,9 +814,9 @@ describe(`Query Collections`, () => { expect(hadFlicker).toBe(false) // Verify the temporary key is replaced by the permanent one - expect(state.value.size).toBe(4) - expect(state.value.get(`temp-key`)).toBeUndefined() - expect(state.value.get(`4`)).toEqual({ + expect(state().size).toBe(4) + expect(state().get(`temp-key`)).toBeUndefined() + expect(state().get(`4`)).toEqual({ id: `4`, name: `John Doe`, title: `New Issue`, From 492596da0ae0b4b945adb0d8ffcc205debd1e330 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Sat, 17 May 2025 21:21:21 +0300 Subject: [PATCH 11/11] wip: experimental computed --- packages/vue-db/src/useLiveQuery.ts | 70 ++++++++++++++++------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 9254a52..5cd8df9 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -1,4 +1,4 @@ -import { onWatcherCleanup, shallowRef, watchEffect } from "vue" +import { computed, onScopeDispose, shallowRef } from "vue" import { compileQuery, queryBuilder } from "@tanstack/db" import { shallow } from "./useStore" import type { @@ -23,51 +23,61 @@ export function useLiveQuery< q: InitialQueryBuilder> ) => QueryBuilder ): UseLiveQueryReturn> { - const results = shallowRef() - const state = shallowRef() - const data = shallowRef() + const NOOP = () => {} + let unsubCompiled = NOOP + let unsubDerivedState = NOOP + let unsubDerivedArray = NOOP - watchEffect(() => { - const query = queryFn(queryBuilder()) - const compiled = compileQuery(query) - compiled.start() + const compiled = computed>>( + () => { + unsubCompiled() + const compiledRef = compileQuery(queryFn(queryBuilder())) + unsubCompiled = compiledRef.start() + return compiledRef + } + ) - const resultsRef = compiled.results - results.value = resultsRef - - const derivedState = resultsRef.derivedState - const derivedArray = resultsRef.derivedArray + const state = computed(() => { + const derivedState = compiled.value.results.derivedState let stateRef = derivedState.state - let dataRef = derivedArray.state - state.value = stateRef - data.value = dataRef + const ret = shallowRef(stateRef) - const unsubDerivedState = derivedState.subscribe(() => { + unsubDerivedState() + unsubDerivedState = derivedState.subscribe(() => { const newValue = derivedState.state if (shallow(stateRef, newValue)) return stateRef = newValue - state.value = newValue + ret.value = newValue }) + return ret + }) - const unsubDerivedArray = derivedArray.subscribe(() => { + const data = computed(() => { + const derivedArray = compiled.value.results.derivedArray + let stateRef = derivedArray.state + const ret = shallowRef(stateRef) + + unsubDerivedArray() + unsubDerivedArray = derivedArray.subscribe(() => { const newValue = derivedArray.state - if (shallow(dataRef, newValue)) return + if (shallow(stateRef, newValue)) return - dataRef = newValue - data.value = newValue + stateRef = newValue + ret.value = newValue }) + return ret + }) - onWatcherCleanup(() => { - compiled.stop() - unsubDerivedState() - unsubDerivedArray() - }) + onScopeDispose(() => { + unsubCompiled() + unsubDerivedState() + unsubDerivedArray() }) return { - state: () => state.value, - data: () => data.value, - collection: () => results.value, + state: () => state.value.value, + data: () => data.value.value, + collection: () => compiled.value.results, } }