From cb14c950b201e17966a7eec4f2db0dc3fc167cb4 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Wed, 8 Oct 2025 00:06:58 -0400 Subject: [PATCH 01/10] Add useReactiveQuery for Angular --- package-lock.json | 38 ++++++++++++++++++++++++++++++++++++++ package.json | 12 +++++++++++- src/angular/index.ts | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/angular/index.ts diff --git a/package-lock.json b/package-lock.json index bb5cbfa..3491b67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "coincident": "^1.2.3" }, "devDependencies": { + "@angular/core": "^20.3.3", "@vitest/browser": "^3.2.4", "@vitest/ui": "^3.2.4", "drizzle-orm": "^0.44.6", @@ -285,6 +286,32 @@ "node": ">= 14.0.0" } }, + "node_modules/@angular/core": { + "version": "20.3.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.3.tgz", + "integrity": "sha512-AWBCixxw4N9VgKT1uwrRPr1dH3CpT/ffcCsXJQ8TjzsKYjVBkXVht5OjtxJOWOQ2KaHwsGFEmDMv9fc1BHDFhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "20.3.3", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, "node_modules/@antfu/ni": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-26.1.0.tgz", @@ -6137,6 +6164,17 @@ "fsevents": "~2.3.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safaridriver": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.0.tgz", diff --git a/package.json b/package.json index 30deb83..2e02b0a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,11 @@ "types": "./dist/kysely/index.d.ts", "browser": "./dist/kysely/index.js", "import": "./dist/kysely/index.js" + }, + "./angular": { + "types": "./dist/angular/index.d.ts", + "browser": "./dist/angular/index.js", + "import": "./dist/angular/index.js" } }, "sideEffects": false, @@ -47,6 +52,7 @@ "coincident": "^1.2.3" }, "devDependencies": { + "@angular/core": "^20.3.3", "@vitest/browser": "^3.2.4", "@vitest/ui": "^3.2.4", "drizzle-orm": "^0.44.6", @@ -62,7 +68,8 @@ }, "peerDependencies": { "drizzle-orm": "*", - "kysely": "*" + "kysely": "*", + "@angular/core": ">=17.0.0" }, "peerDependenciesMeta": { "kysely": { @@ -70,6 +77,9 @@ }, "drizzle-orm": { "optional": true + }, + "@angular/core": { + "optional": true } }, "author": "Dallas Hoffman", diff --git a/src/angular/index.ts b/src/angular/index.ts new file mode 100644 index 0000000..008d12f --- /dev/null +++ b/src/angular/index.ts @@ -0,0 +1,38 @@ +import { computed, effect, isSignal, signal, type Signal } from '@angular/core'; +import type { SQLocal } from '../client.js'; +import type { StatementInput } from '../types.js'; + +export function useReactiveQuery>( + db: SQLocal | Signal, + query: StatementInput | Signal> +): { value: Signal; error: Signal } { + const value = signal([]); + const error = signal(undefined); + + const dbValue = computed(() => (isSignal(db) ? db() : db)); + const queryValue = computed(() => (isSignal(query) ? query() : query)); + + effect((onCleanup) => { + const db = dbValue(); + const query = queryValue(); + + const subscription = db.reactiveQuery(query).subscribe( + (data) => { + value.set(data); + error.set(undefined); + }, + (err) => { + error.set(err); + } + ); + + onCleanup(() => { + subscription.unsubscribe(); + }); + }); + + return { + value: value.asReadonly(), + error: error.asReadonly(), + }; +} From bb9f2fd2c10882a7c8d49a57be769c199bbb4760 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Wed, 8 Oct 2025 00:27:18 -0400 Subject: [PATCH 02/10] Add useReactiveQuery for Vue --- package-lock.json | 167 ++++++++++++++++++++++--------------------- package.json | 14 +++- src/angular/index.ts | 10 +-- src/vue/index.ts | 49 +++++++++++++ 4 files changed, 152 insertions(+), 88 deletions(-) create mode 100644 src/vue/index.ts diff --git a/package-lock.json b/package-lock.json index 3491b67..881b475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "vite": "^7.1.9", "vitepress": "^1.6.4", "vitest": "^3.2.4", + "vue": "^3.5.22", "webdriverio": "^9.20.0", "wrangler": "^4.42.0" }, @@ -32,10 +33,14 @@ "url": "https://www.paypal.com/biz/fund?id=U3ZNM2Q26WJY8" }, "peerDependencies": { + "@angular/core": ">=17.0.0", "drizzle-orm": "*", "kysely": "*" }, "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, "drizzle-orm": { "optional": true }, @@ -361,9 +366,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -371,9 +376,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -381,13 +386,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", - "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.5" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -407,14 +412,14 @@ } }, "node_modules/@babel/types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", - "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1476,9 +1481,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, @@ -2410,17 +2415,17 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz", + "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.4", + "@vue/shared": "3.5.22", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-core/node_modules/estree-walker": { @@ -2431,32 +2436,32 @@ "license": "MIT" }, "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz", + "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.22", + "@vue/shared": "3.5.22" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz", + "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.4", + "@vue/compiler-core": "3.5.22", + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22", "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.19", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { @@ -2467,14 +2472,14 @@ "license": "MIT" }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz", + "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.22", + "@vue/shared": "3.5.22" } }, "node_modules/@vue/devtools-api": { @@ -2514,57 +2519,57 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz", + "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.22" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz", + "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.22", + "@vue/shared": "3.5.22" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz", + "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", + "@vue/reactivity": "3.5.22", + "@vue/runtime-core": "3.5.22", + "@vue/shared": "3.5.22", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz", + "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.22" } }, "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz", + "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==", "dev": true, "license": "MIT" }, @@ -5165,13 +5170,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/mark.js": { @@ -7852,17 +7857,17 @@ } }, "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", + "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-sfc": "3.5.22", + "@vue/runtime-dom": "3.5.22", + "@vue/server-renderer": "3.5.22", + "@vue/shared": "3.5.22" }, "peerDependencies": { "typescript": "*" diff --git a/package.json b/package.json index 2e02b0a..f660e26 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,11 @@ "types": "./dist/angular/index.d.ts", "browser": "./dist/angular/index.js", "import": "./dist/angular/index.js" + }, + "./vue": { + "types": "./dist/vue/index.d.ts", + "browser": "./dist/vue/index.js", + "import": "./dist/vue/index.js" } }, "sideEffects": false, @@ -63,22 +68,27 @@ "vite": "^7.1.9", "vitepress": "^1.6.4", "vitest": "^3.2.4", + "vue": "^3.5.22", "webdriverio": "^9.20.0", "wrangler": "^4.42.0" }, "peerDependencies": { + "@angular/core": ">=17.0.0", "drizzle-orm": "*", "kysely": "*", - "@angular/core": ">=17.0.0" + "vue": ">=3.0.0" }, "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, "kysely": { "optional": true }, "drizzle-orm": { "optional": true }, - "@angular/core": { + "vue": { "optional": true } }, diff --git a/src/angular/index.ts b/src/angular/index.ts index 008d12f..91ef3e2 100644 --- a/src/angular/index.ts +++ b/src/angular/index.ts @@ -5,8 +5,8 @@ import type { StatementInput } from '../types.js'; export function useReactiveQuery>( db: SQLocal | Signal, query: StatementInput | Signal> -): { value: Signal; error: Signal } { - const value = signal([]); +): { data: Signal; error: Signal } { + const data = signal([]); const error = signal(undefined); const dbValue = computed(() => (isSignal(db) ? db() : db)); @@ -17,8 +17,8 @@ export function useReactiveQuery>( const query = queryValue(); const subscription = db.reactiveQuery(query).subscribe( - (data) => { - value.set(data); + (results) => { + data.set(results); error.set(undefined); }, (err) => { @@ -32,7 +32,7 @@ export function useReactiveQuery>( }); return { - value: value.asReadonly(), + data: data.asReadonly(), error: error.asReadonly(), }; } diff --git a/src/vue/index.ts b/src/vue/index.ts new file mode 100644 index 0000000..986abd1 --- /dev/null +++ b/src/vue/index.ts @@ -0,0 +1,49 @@ +import { + computed, + isRef, + readonly, + shallowRef, + watchEffect, + type DeepReadonly, + type Ref, +} from 'vue'; +import type { SQLocal } from '../client.js'; +import type { StatementInput } from '../types.js'; + +export function useReactiveQuery>( + db: SQLocal | Ref, + query: StatementInput | Ref> +): { + data: Readonly>>; + error: Readonly>; +} { + const data = shallowRef([]); + const error = shallowRef(undefined); + + const dbValue = computed(() => (isRef(db) ? db.value : db)); + const queryValue = computed(() => (isRef(query) ? query.value : query)); + + watchEffect((onCleanup) => { + const db = dbValue.value; + const query = queryValue.value; + + const subscription = db.reactiveQuery(query).subscribe( + (results) => { + data.value = results; + error.value = undefined; + }, + (err) => { + error.value = err; + } + ); + + onCleanup(() => { + subscription.unsubscribe(); + }); + }); + + return { + data: readonly(data), + error: readonly(error), + }; +} From 298b41811ce7e1c3337399255eec240018f7e744 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Wed, 8 Oct 2025 19:46:44 -0400 Subject: [PATCH 03/10] Add useReactiveQuery for React --- package-lock.json | 52 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 11 ++++++++++ src/react/index.ts | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/react/index.ts diff --git a/package-lock.json b/package-lock.json index 881b475..3d9d54c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,13 @@ }, "devDependencies": { "@angular/core": "^20.3.3", + "@types/react": "^18.3.26", "@vitest/browser": "^3.2.4", "@vitest/ui": "^3.2.4", "drizzle-orm": "^0.44.6", "kysely": "^0.28.7", "prettier": "^3.6.2", + "react": "^18.3.1", "taze": "^19.7.0", "typescript": "^5.9.3", "vite": "^7.1.9", @@ -35,7 +37,8 @@ "peerDependencies": { "@angular/core": ">=17.0.0", "drizzle-orm": "*", - "kysely": "*" + "kysely": "*", + "vue": ">=3.0.0" }, "peerDependenciesMeta": { "@angular/core": { @@ -46,6 +49,9 @@ }, "kysely": { "optional": true + }, + "vue": { + "optional": true } } }, @@ -2182,6 +2188,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", @@ -5143,6 +5167,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", @@ -5986,6 +6023,19 @@ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", "dev": true }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index f660e26..32a634e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,11 @@ "browser": "./dist/angular/index.js", "import": "./dist/angular/index.js" }, + "./react": { + "types": "./dist/react/index.d.ts", + "browser": "./dist/react/index.js", + "import": "./dist/react/index.js" + }, "./vue": { "types": "./dist/vue/index.d.ts", "browser": "./dist/vue/index.js", @@ -58,11 +63,13 @@ }, "devDependencies": { "@angular/core": "^20.3.3", + "@types/react": "^18.3.26", "@vitest/browser": "^3.2.4", "@vitest/ui": "^3.2.4", "drizzle-orm": "^0.44.6", "kysely": "^0.28.7", "prettier": "^3.6.2", + "react": "^18.3.1", "taze": "^19.7.0", "typescript": "^5.9.3", "vite": "^7.1.9", @@ -76,6 +83,7 @@ "@angular/core": ">=17.0.0", "drizzle-orm": "*", "kysely": "*", + "react": ">=18.0.0", "vue": ">=3.0.0" }, "peerDependenciesMeta": { @@ -88,6 +96,9 @@ "drizzle-orm": { "optional": true }, + "react": { + "optional": true + }, "vue": { "optional": true } diff --git a/src/react/index.ts b/src/react/index.ts new file mode 100644 index 0000000..9c17923 --- /dev/null +++ b/src/react/index.ts @@ -0,0 +1,37 @@ +import { useCallback, useMemo, useState, useSyncExternalStore } from 'react'; +import type { SQLocal } from '../client.js'; +import type { StatementInput } from '../types.js'; + +export function useReactiveQuery>( + db: SQLocal, + query: StatementInput +): { data: Result[]; error: Error | undefined } { + const [error, setError] = useState(undefined); + + const reactiveQuery = useMemo(() => { + return db.reactiveQuery(query); + }, [db, query]); + + const get = useCallback(() => reactiveQuery.value, [reactiveQuery]); + const subscribe = useCallback( + (cb: () => void) => { + const subscription = reactiveQuery.subscribe( + () => { + cb(); + setError(undefined); + }, + (err) => { + setError(err); + } + ); + return () => subscription.unsubscribe(); + }, + [reactiveQuery] + ); + const data = useSyncExternalStore(subscribe, get); + + return { + data, + error, + }; +} From 39f179184bddbc6a13fe48cb3f2fbbc7cf56916a Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sat, 25 Oct 2025 02:33:09 -0400 Subject: [PATCH 04/10] Fix React version of useReactiveQuery --- package-lock.json | 4 ++++ src/react/index.ts | 28 +++++++++++++++++++++++----- src/types.ts | 5 +++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d9d54c..565582c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@angular/core": ">=17.0.0", "drizzle-orm": "*", "kysely": "*", + "react": ">=18.0.0", "vue": ">=3.0.0" }, "peerDependenciesMeta": { @@ -50,6 +51,9 @@ "kysely": { "optional": true }, + "react": { + "optional": true + }, "vue": { "optional": true } diff --git a/src/react/index.ts b/src/react/index.ts index 9c17923..3cb735f 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,16 +1,30 @@ -import { useCallback, useMemo, useState, useSyncExternalStore } from 'react'; +import { + useCallback, + useMemo, + useState, + useSyncExternalStore, + type Dispatch, + type SetStateAction, +} from 'react'; import type { SQLocal } from '../client.js'; import type { StatementInput } from '../types.js'; export function useReactiveQuery>( db: SQLocal, query: StatementInput -): { data: Result[]; error: Error | undefined } { +): { + data: Result[]; + error: Error | undefined; + setDb: Dispatch>; + setQuery: Dispatch>>; +} { + const [dbValue, setDb] = useState(() => db); + const [queryValue, setQuery] = useState(() => query); const [error, setError] = useState(undefined); const reactiveQuery = useMemo(() => { - return db.reactiveQuery(query); - }, [db, query]); + return dbValue.reactiveQuery(queryValue); + }, [dbValue, queryValue]); const get = useCallback(() => reactiveQuery.value, [reactiveQuery]); const subscribe = useCallback( @@ -24,7 +38,9 @@ export function useReactiveQuery>( setError(err); } ); - return () => subscription.unsubscribe(); + return () => { + subscription.unsubscribe(); + }; }, [reactiveQuery] ); @@ -33,5 +49,7 @@ export function useReactiveQuery>( return { data, error, + setDb, + setQuery, }; } diff --git a/src/types.ts b/src/types.ts index d0371c0..268cbb2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,9 +37,10 @@ export type ReturningStatement = 'sqlite' >); +export type SqlTag = typeof sqlTag; export type StatementInput = | ReturningStatement - | ((sql: typeof sqlTag) => ReturningStatement); + | ((sql: SqlTag) => ReturningStatement); export type Transaction = { query: >( @@ -124,7 +125,7 @@ export type ClientConfig = { reactive?: boolean; readOnly?: boolean; verbose?: boolean; - onInit?: (sql: typeof sqlTag) => void | Statement[]; + onInit?: (sql: SqlTag) => void | Statement[]; onConnect?: (reason: ConnectReason) => void; processor?: SQLocalProcessor | Worker; }; From 478fc7057d09c5cf48846f53f6340b56a7c90cc3 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sat, 25 Oct 2025 17:46:18 -0400 Subject: [PATCH 05/10] Use SqlTag type in client too --- src/client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index ee1ff83..96e9d29 100644 --- a/src/client.ts +++ b/src/client.ts @@ -13,6 +13,7 @@ import type { DatabasePath, AggregateUserFunction, ReactiveQuery, + SqlTag, } from './types.js'; import type { BatchMessage, @@ -288,7 +289,7 @@ export class SQLocal { }; batch = async >( - passStatements: (sql: typeof sqlTag) => Statement[] + passStatements: (sql: SqlTag) => Statement[] ): Promise => { const statements = passStatements(sqlTag); const data = await this.execBatch(statements); From ee73bf80c821de4c031e369844c0dd639a44ce36 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sat, 25 Oct 2025 18:22:03 -0400 Subject: [PATCH 06/10] Add status to useReactiveQuery --- src/angular/index.ts | 19 +++++++++++++++++-- src/react/index.ts | 7 ++++++- src/types.ts | 1 + src/vue/index.ts | 14 +++++++++++++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/angular/index.ts b/src/angular/index.ts index 91ef3e2..7e30ce4 100644 --- a/src/angular/index.ts +++ b/src/angular/index.ts @@ -1,13 +1,18 @@ import { computed, effect, isSignal, signal, type Signal } from '@angular/core'; import type { SQLocal } from '../client.js'; -import type { StatementInput } from '../types.js'; +import type { ReactiveQueryStatus, StatementInput } from '../types.js'; export function useReactiveQuery>( db: SQLocal | Signal, query: StatementInput | Signal> -): { data: Signal; error: Signal } { +): { + data: Signal; + error: Signal; + status: Signal; +} { const data = signal([]); const error = signal(undefined); + const pending = signal(true); const dbValue = computed(() => (isSignal(db) ? db() : db)); const queryValue = computed(() => (isSignal(query) ? query() : query)); @@ -20,6 +25,7 @@ export function useReactiveQuery>( (results) => { data.set(results); error.set(undefined); + pending.set(false); }, (err) => { error.set(err); @@ -31,8 +37,17 @@ export function useReactiveQuery>( }); }); + const status = computed(() => { + const hasError = !!error(); + const isPending = pending(); + if (hasError) return 'error'; + if (isPending) return 'pending'; + return 'ok'; + }); + return { data: data.asReadonly(), error: error.asReadonly(), + status, }; } diff --git a/src/react/index.ts b/src/react/index.ts index 3cb735f..30cffbb 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -7,7 +7,7 @@ import { type SetStateAction, } from 'react'; import type { SQLocal } from '../client.js'; -import type { StatementInput } from '../types.js'; +import type { ReactiveQueryStatus, StatementInput } from '../types.js'; export function useReactiveQuery>( db: SQLocal, @@ -15,12 +15,14 @@ export function useReactiveQuery>( ): { data: Result[]; error: Error | undefined; + status: ReactiveQueryStatus; setDb: Dispatch>; setQuery: Dispatch>>; } { const [dbValue, setDb] = useState(() => db); const [queryValue, setQuery] = useState(() => query); const [error, setError] = useState(undefined); + const [pending, setPending] = useState(true); const reactiveQuery = useMemo(() => { return dbValue.reactiveQuery(queryValue); @@ -33,6 +35,7 @@ export function useReactiveQuery>( () => { cb(); setError(undefined); + setPending(false); }, (err) => { setError(err); @@ -45,10 +48,12 @@ export function useReactiveQuery>( [reactiveQuery] ); const data = useSyncExternalStore(subscribe, get); + const status = !!error ? 'error' : pending ? 'pending' : 'ok'; return { data, error, + status, setDb, setQuery, }; diff --git a/src/types.ts b/src/types.ts index 268cbb2..478fea6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -63,6 +63,7 @@ export type ReactiveQuery = { unsubscribe: () => void; }; }; +export type ReactiveQueryStatus = 'pending' | 'ok' | 'error'; export type RawResultData = { rows: unknown[] | unknown[][]; diff --git a/src/vue/index.ts b/src/vue/index.ts index 986abd1..06ec57e 100644 --- a/src/vue/index.ts +++ b/src/vue/index.ts @@ -8,7 +8,7 @@ import { type Ref, } from 'vue'; import type { SQLocal } from '../client.js'; -import type { StatementInput } from '../types.js'; +import type { ReactiveQueryStatus, StatementInput } from '../types.js'; export function useReactiveQuery>( db: SQLocal | Ref, @@ -16,9 +16,11 @@ export function useReactiveQuery>( ): { data: Readonly>>; error: Readonly>; + status: Readonly>; } { const data = shallowRef([]); const error = shallowRef(undefined); + const pending = shallowRef(true); const dbValue = computed(() => (isRef(db) ? db.value : db)); const queryValue = computed(() => (isRef(query) ? query.value : query)); @@ -31,6 +33,7 @@ export function useReactiveQuery>( (results) => { data.value = results; error.value = undefined; + pending.value = false; }, (err) => { error.value = err; @@ -42,8 +45,17 @@ export function useReactiveQuery>( }); }); + const status = computed(() => { + const hasError = !!error.value; + const isPending = pending.value; + if (hasError) return 'error'; + if (isPending) return 'pending'; + return 'ok'; + }); + return { data: readonly(data), error: readonly(error), + status: readonly(status), }; } From 20e20bfbca38b050bc0e0084a33e2360005a5396 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sun, 26 Oct 2025 22:19:50 -0400 Subject: [PATCH 07/10] Document useReactiveQuery --- docs/api/reactivequery.md | 75 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/api/reactivequery.md b/docs/api/reactivequery.md index bb6f172..029509a 100644 --- a/docs/api/reactivequery.md +++ b/docs/api/reactivequery.md @@ -29,7 +29,7 @@ const subscription = reactiveQuery( }); ``` -The query can be any SQL statement that reads one or more tables. It can be passed using a `sql` tag function available in the `reactiveQuery` callback that works similarly to the [`sql` tag function used for single queries](sql.md). It can also be a query built with Drizzle or Kysely; see the "Query Builders" section below. +The query can be any SQL statement that reads one or more tables. It can be passed using a `sql` tag function available in the `reactiveQuery` callback that works similarly to the [`sql` tag function used for single queries](sql.md). It can also be a query built with Drizzle or Kysely; see the "Query Builders" section [below](#query-builders). You can then call `subscribe` on the object returned from `reactiveQuery` to register a callback that gets called an initial time and then again whenever the one or more of the queried tables are changed. The latest result data from the query will be passed as the first argument to your callback. @@ -85,3 +85,76 @@ const subscription = reactiveQuery( console.log('Grocery List Updated:', data); }); ``` + +## UI Frameworks + +We also provide `useReactiveQuery` hook implementations to make it easier to integrate reactive queries with the reactivity systems of UI frameworks. The hook handles subscribing, returns reactive data, and automatically unsubscribes from the query when the component it's used in is destroyed. + +`useReactiveQuery` takes your `SQLocal` instance and a SQL query as arguments. The query can be passed using the `sql` tag function or using a query builder as described [above](#query-builders). It returns an object containing the following reactive values: + +- **`data`** (`Result[]`) - The result data from your SQL query. +- **`error`** (`Error | undefined`) - An `Error` object if the SQL query fails. +- **`status`** (`'pending' | 'error' | 'ok'`) - The string `'pending'` if the SQL query has not completed for the first time yet, `'error'` if the SQL query failed, or `'ok'` if the SQL query returned successfully. + +### React + +Import the React version of `useReactiveQuery` from `sqlocal/react`. It requires React 18 or higher. + +In addition to `data`, `error`, and `status`, the object returned from this version of `useReactiveQuery` also contains `setDb` and `setQuery` functions which allow you to dynamically change the arguments from their initial values and automatically resubscribe. + +```js +import { SQLocal } from 'sqlocal'; +import { useReactiveQuery } from 'sqlocal/react'; + +const db = new SQLocal({ + databasePath: 'database.sqlite3', + reactive: true, +}); + +export function MyComponent() { + const groceries = useReactiveQuery(db, (sql) => sql`SELECT * FROM groceries`); +} +``` + +### Vue + +Import the Vue version of `useReactiveQuery` from `sqlocal/vue`. It requires Vue 3 or higher. + +This version of `useReactiveQuery` returns `data`, `error`, and `status` as read-only Vue refs. It can also accept its arguments as refs, which allows you to dynamically change them from their initial values and automatically resubscribe. + +```vue + +``` + +### Angular + +Import the Angular version of `useReactiveQuery` from `sqlocal/angular`. It requires Angular 17 or higher. + +This version of `useReactiveQuery` returns `data`, `error`, and `status` as read-only Angular signals. It can also accept its arguments as signals, which allows you to dynamically change them from their initial values and automatically resubscribe. + +```ts +import { Component } from '@angular/core'; +import { SQLocal } from 'sqlocal'; +import { useReactiveQuery } from 'sqlocal/angular'; + +const db = new SQLocal({ + databasePath: 'database.sqlite3', + reactive: true, +}); + +@Component({ + selector: 'my-component', +}) +export class MyComponent { + groceries = useReactiveQuery(db, (sql) => sql`SELECT * FROM groceries`); +} +``` From 1ead95d48ba4df5ef285e90be38dc32e9601e967 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sun, 26 Oct 2025 22:34:43 -0400 Subject: [PATCH 08/10] Set allowSignalWrites for Angular 17 and 18 compatibility --- src/angular/index.ts | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/angular/index.ts b/src/angular/index.ts index 7e30ce4..a4c7ff4 100644 --- a/src/angular/index.ts +++ b/src/angular/index.ts @@ -17,25 +17,28 @@ export function useReactiveQuery>( const dbValue = computed(() => (isSignal(db) ? db() : db)); const queryValue = computed(() => (isSignal(query) ? query() : query)); - effect((onCleanup) => { - const db = dbValue(); - const query = queryValue(); + effect( + (onCleanup) => { + const db = dbValue(); + const query = queryValue(); - const subscription = db.reactiveQuery(query).subscribe( - (results) => { - data.set(results); - error.set(undefined); - pending.set(false); - }, - (err) => { - error.set(err); - } - ); + const subscription = db.reactiveQuery(query).subscribe( + (results) => { + data.set(results); + error.set(undefined); + pending.set(false); + }, + (err) => { + error.set(err); + } + ); - onCleanup(() => { - subscription.unsubscribe(); - }); - }); + onCleanup(() => { + subscription.unsubscribe(); + }); + }, + { allowSignalWrites: true } + ); const status = computed(() => { const hasError = !!error(); From 246966af9ace5ec18018e2d446e5250f4c7ed652 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Mon, 27 Oct 2025 19:57:04 -0400 Subject: [PATCH 09/10] Reset pending state when DB or query is changed --- src/angular/index.ts | 2 ++ src/react/index.ts | 6 ++++-- src/vue/index.ts | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/angular/index.ts b/src/angular/index.ts index a4c7ff4..14a6a31 100644 --- a/src/angular/index.ts +++ b/src/angular/index.ts @@ -22,6 +22,8 @@ export function useReactiveQuery>( const db = dbValue(); const query = queryValue(); + pending.set(true); + const subscription = db.reactiveQuery(query).subscribe( (results) => { data.set(results); diff --git a/src/react/index.ts b/src/react/index.ts index 30cffbb..8900718 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -19,12 +19,14 @@ export function useReactiveQuery>( setDb: Dispatch>; setQuery: Dispatch>>; } { - const [dbValue, setDb] = useState(() => db); - const [queryValue, setQuery] = useState(() => query); const [error, setError] = useState(undefined); const [pending, setPending] = useState(true); + const [dbValue, setDb] = useState(() => db); + const [queryValue, setQuery] = useState(() => query); + const reactiveQuery = useMemo(() => { + setPending(true); return dbValue.reactiveQuery(queryValue); }, [dbValue, queryValue]); diff --git a/src/vue/index.ts b/src/vue/index.ts index 06ec57e..9e44412 100644 --- a/src/vue/index.ts +++ b/src/vue/index.ts @@ -29,6 +29,8 @@ export function useReactiveQuery>( const db = dbValue.value; const query = queryValue.value; + pending.value = true; + const subscription = db.reactiveQuery(query).subscribe( (results) => { data.value = results; From 24cbe67bb8630cbb951dcc1c9383ecdd5669e0a1 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Sat, 8 Nov 2025 22:46:29 -0500 Subject: [PATCH 10/10] Add new headline features --- README.md | 4 +++- docs/index.md | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 40c88f0..76b771f 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ SQLocal makes it easy to run SQLite3 in the browser, backed by the origin privat - ๐Ÿ”Ž Locally executes any query that SQLite3 supports - ๐Ÿงต Runs the SQLite engine in a web worker so queries do not block the main thread - ๐Ÿ“‚ Persists data to the origin private file system, which is optimized for fast file I/O -- ๐Ÿ”’ Each user can have their own private database instance - ๐Ÿš€ Simple API; just name your database and start running SQL queries +- โšก๏ธ Subscribe to query results and receive changes, even across tabs +- ๐Ÿ”’ Each user can have their own private database instance - ๐Ÿ› ๏ธ Works with Kysely and Drizzle ORM for making type-safe queries +- ๐Ÿค Get set up quickly with hooks for UI frameworks and a Vite plugin ## Examples diff --git a/docs/index.md b/docs/index.md index 66b06bf..980f33c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,13 +31,19 @@ features: - title: Persisted icon: ๐Ÿ“‚ details: Persists data to the origin private file system, which is optimized for fast file I/O - - title: Per-User - icon: ๐Ÿ”’ - details: Each user can have their own private database instance - title: Simple API icon: ๐Ÿš€ details: Just name your database and start running SQL queries + - title: Reactive + icon: โšก๏ธ + details: Subscribe to query results and receive changes, even across tabs + - title: Per-User + icon: ๐Ÿ”’ + details: Each user can have their own private database instance - title: TypeScript icon: ๐Ÿ› ๏ธ details: Works with Kysely and Drizzle ORM for making type-safe queries + - title: Integrations + icon: ๐Ÿค + details: Get set up quickly with hooks for UI frameworks and a Vite plugin ---