From d5412f26b27c5182e40a8c06e2c902dc3bc6ebaf Mon Sep 17 00:00:00 2001 From: Kobie Botha Date: Mon, 23 Dec 2024 15:09:19 -0700 Subject: [PATCH] initial commit WIP --- .../.env.local.template | 5 + demos/supabase-infinite-scroll/.gitignore | 47 + demos/supabase-infinite-scroll/README.md | 79 ++ demos/supabase-infinite-scroll/Supabase.sql | 85 ++ demos/supabase-infinite-scroll/index.html | 49 + .../package-lock.json | 1204 +++++++++++++++++ demos/supabase-infinite-scroll/package.json | 21 + .../src/consoleList/index.js | 113 ++ .../src/dataSources/array.js | 23 + .../src/dataSources/index.js | 6 + .../src/dataSources/pagedSync.js | 57 + .../src/dataSources/preSync.js | 23 + .../src/dataSources/triggerSync.js | 81 ++ demos/supabase-infinite-scroll/src/index.js | 120 ++ .../src/listBox/index.js | 163 +++ .../src/listBox/itemBuffer.js | 53 + .../src/powersync/index.js | 150 ++ .../src/powersync/schema.js | 36 + .../src/supabase/index.js | 176 +++ demos/supabase-infinite-scroll/styles.css | 110 ++ demos/supabase-infinite-scroll/syncrules.yaml | 18 + demos/supabase-infinite-scroll/vite.config.js | 34 + 22 files changed, 2653 insertions(+) create mode 100644 demos/supabase-infinite-scroll/.env.local.template create mode 100644 demos/supabase-infinite-scroll/.gitignore create mode 100644 demos/supabase-infinite-scroll/README.md create mode 100644 demos/supabase-infinite-scroll/Supabase.sql create mode 100644 demos/supabase-infinite-scroll/index.html create mode 100644 demos/supabase-infinite-scroll/package-lock.json create mode 100644 demos/supabase-infinite-scroll/package.json create mode 100644 demos/supabase-infinite-scroll/src/consoleList/index.js create mode 100644 demos/supabase-infinite-scroll/src/dataSources/array.js create mode 100644 demos/supabase-infinite-scroll/src/dataSources/index.js create mode 100644 demos/supabase-infinite-scroll/src/dataSources/pagedSync.js create mode 100644 demos/supabase-infinite-scroll/src/dataSources/preSync.js create mode 100644 demos/supabase-infinite-scroll/src/dataSources/triggerSync.js create mode 100644 demos/supabase-infinite-scroll/src/index.js create mode 100644 demos/supabase-infinite-scroll/src/listBox/index.js create mode 100644 demos/supabase-infinite-scroll/src/listBox/itemBuffer.js create mode 100644 demos/supabase-infinite-scroll/src/powersync/index.js create mode 100644 demos/supabase-infinite-scroll/src/powersync/schema.js create mode 100644 demos/supabase-infinite-scroll/src/supabase/index.js create mode 100644 demos/supabase-infinite-scroll/styles.css create mode 100644 demos/supabase-infinite-scroll/syncrules.yaml create mode 100644 demos/supabase-infinite-scroll/vite.config.js diff --git a/demos/supabase-infinite-scroll/.env.local.template b/demos/supabase-infinite-scroll/.env.local.template new file mode 100644 index 00000000..dc4088ca --- /dev/null +++ b/demos/supabase-infinite-scroll/.env.local.template @@ -0,0 +1,5 @@ +# Copy this template: `cp .env.local.template .env.local` +# Edit .env.local and enter your Supabase and PowerSync project details. +VITE_SUPABASE_URL=https://foo.supabase.co +VITE_SUPABASE_ANON_KEY=foo +VITE_POWERSYNC_URL=https://foo.powersync.journeyapps.com diff --git a/demos/supabase-infinite-scroll/.gitignore b/demos/supabase-infinite-scroll/.gitignore new file mode 100644 index 00000000..0cc2f139 --- /dev/null +++ b/demos/supabase-infinite-scroll/.gitignore @@ -0,0 +1,47 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# ide +.idea +.fleet +.vscode + +# PWA +**/public/workbox-*.js +**/public/sw.js +**/public/swe-worker-* +**/public/worker-*.js +**/public/fallback-*.js diff --git a/demos/supabase-infinite-scroll/README.md b/demos/supabase-infinite-scroll/README.md new file mode 100644 index 00000000..72f5d27b --- /dev/null +++ b/demos/supabase-infinite-scroll/README.md @@ -0,0 +1,79 @@ +# PowerSync + Supabase Web Demo: Infinite Scrolling + +## Overview + +Demo app illustrating various infinite scrolling strategies using lazy-load scenarios described in [Use Case Examples: Infinite Scrolling](https://docs.powersync.com/usage/use-case-examples/infinite-scrolling) + +## Prerequisites + +1. You will need to sign up for a PowerSync account and create an instance connected to Supabase. A step-by-step guide on Supabase<>PowerSync integration is available [here](https://docs.powersync.com/integration-guides/supabase). +1. Apply the contents of `Supabase.sql` in this repo to your Supabase project. +1. Enable Anonymous Sign-ins in Supabase (**Project Settings -> Authentication -> User Signups**) + +## Getting Started + +First install the project dependencies. Run the following command in the repo root: + +```bash +pnpm install +pnpm build:packages +``` + +Then switch into this demo's directory: + +```bash +cd demos/supabase-infinite-scroll +``` + +Set up the Environment variables: Copy the `.env.local.template` file: + +```bash +cp .env.local.template .env.local +``` + +And then edit `.env.local` to insert your credentials for Supabase and PowerSync. + +Run the development server: + +```bash +pnpm dev +``` + +This will start the Vite development server. Open your browser and navigate to the URL provided in the terminal (usually [http://localhost:5173](http://localhost:5173)) to view the demo. Use the control buttons to toggle between scenarios and get a feel for the behavior. + +## Specific Scenarios +This section provides an index to the various scenarios described in the [docs](https://docs.powersync.com/usage/use-case-examples/infinite-scrolling). + +### Pre-sync all data and query the local database + +* Implemented in [`src/dataSources/preSync.js`](src/dataSources/preSync.js) + +### Control data sync using client parameters + +* Implemented in [`src/dataSources/pagedSync.js`](src/dataSources/pagedSync.js) +* Uses the `paged_list` table in Postgres + +### Client-side triggers a server-side function to flag data to sync + +* Implemented in [`src/dataSources/triggerSync.js`](src/dataSources/triggerSync.js) +* Uses the `syncto_list` table in Postgres + +### Sync limited data and then load more data from an API + +* Not implemented. A trivial modification of `preSync.js` would suffice. +* Would also use the `syncto_list` table in Postgres + +## Framework For Running Scenarios + +### Page Framework Files +`styles.css` +`index.html` +`index.js` + +### Virtual Listbox +`src/listBox/index.js` +`src/listBox/itemBuffer.js` + +### Known Issues + +* Sometimes when switching scenarios using the control buttons there is a remnant loading spinner. Simply scroll to dismiss it. \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/Supabase.sql b/demos/supabase-infinite-scroll/Supabase.sql new file mode 100644 index 00000000..e572bfe0 --- /dev/null +++ b/demos/supabase-infinite-scroll/Supabase.sql @@ -0,0 +1,85 @@ +-- Presync List +CREATE TABLE list ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY NOT NULL, + text text NOT NULL, + created_at timestamp with time zone DEFAULT current_timestamp +); + +DO $$ +DECLARE + i INT; + page INT := 1; + current_timestamp TIMESTAMP := NOW(); +BEGIN + FOR i IN 1..10000 LOOP + INSERT INTO list (text, created_at) + VALUES ('item ' || i, current_timestamp + (i || ' milliseconds')::INTERVAL); + IF i % 100 = 0 THEN page := page + 1; END IF; + END LOOP; +END $$; + +-- Paged List +DROP TABLE IF EXISTS paged_list; + +CREATE TABLE paged_list ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY NOT NULL, + text text NOT NULL, + page integer, + created_at timestamp with time zone DEFAULT current_timestamp +); + +DO $$ +DECLARE + i INT; + page INT := 1; + current_timestamp TIMESTAMP := NOW(); +BEGIN + FOR i IN 1..10000 LOOP + INSERT INTO paged_list (text, page, created_at) + VALUES ('item ' || i, page, current_timestamp + (i || ' milliseconds')::INTERVAL); + IF i % 100 = 0 THEN page := page + 1; END IF; + END LOOP; +END $$; + +-- SyncTo List +CREATE TABLE syncto_list ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY NOT NULL, + text text NOT NULL, + sync_to uuid[] NOT NULL, + created_at timestamp with time zone NOT NULL +); + +DO $$ +DECLARE + i INT; + page INT := 1; + current_timestamp TIMESTAMP := NOW(); +BEGIN + FOR i IN 1..10000 LOOP + INSERT INTO syncto_list (text, sync_to, created_at) + VALUES ('item ' || i, ARRAY[]::UUID[], current_timestamp + (i || ' milliseconds')::INTERVAL); + IF i % 100 = 0 THEN page := page + 1; END IF; + END LOOP; +END $$; + +CREATE OR REPLACE FUNCTION update_sync_to(start_value INT, end_value INT, user_uuid UUID) +RETURNS VOID AS $$ +BEGIN + -- Add the UUID to sync_to for the specified range of rows + WITH ranked_rows AS ( + SELECT id, ROW_NUMBER() OVER (ORDER BY created_at ASC) AS row_num + FROM syncto_list + ) + UPDATE syncto_list + SET sync_to = CASE + WHEN sync_to IS NULL THEN ARRAY[user_uuid] + ELSE array_append(sync_to, user_uuid) + END + FROM ranked_rows + WHERE syncto_list.id = ranked_rows.id + AND ranked_rows.row_num BETWEEN start_value AND end_value; +END; +$$ LANGUAGE plpgsql; + +-- Create Publication +CREATE PUBLICATION powersync FOR ALL TABLES; \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/index.html b/demos/supabase-infinite-scroll/index.html new file mode 100644 index 00000000..ea2771b7 --- /dev/null +++ b/demos/supabase-infinite-scroll/index.html @@ -0,0 +1,49 @@ + + + + + + + PowerSync Infinite Scroll Demos + + + + +
+ + +
+

Infinite Scroll Demos

+
+
+
+
+
+ +
+

select method

+ + + + + +
+
+ + +
+
+ console output +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/package-lock.json b/demos/supabase-infinite-scroll/package-lock.json new file mode 100644 index 00000000..f044e78f --- /dev/null +++ b/demos/supabase-infinite-scroll/package-lock.json @@ -0,0 +1,1204 @@ +{ + "name": "vite-project", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite-project", + "version": "0.0.0", + "dependencies": { + "@journeyapps/wa-sqlite": "^0.3.0", + "@powersync/web": "^1.8.1", + "@supabase/supabase-js": "^2.45.4", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0" + }, + "devDependencies": { + "vite": "^5.4.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@journeyapps/wa-sqlite": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@journeyapps/wa-sqlite/-/wa-sqlite-0.3.0.tgz", + "integrity": "sha512-LQMjcMh92myqzq9kpKFJJ+t1zY7owHTq8TvVYG83luCKzaZepNk86jNB/56fb/vCEy1PQBRc/cI7BTt10SfItA==" + }, + "node_modules/@powersync/common": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@powersync/common/-/common-1.19.0.tgz", + "integrity": "sha512-Cb8tzuAJ0ff075vfiuTMBkOqOpdh7xMZriDFhojHERaUnPlEtaHJBoB8TRU8Y0i7dz3v3TBIBYuhJpEzObbejQ==", + "dependencies": { + "js-logger": "^1.6.1" + } + }, + "node_modules/@powersync/web": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@powersync/web/-/web-1.8.2.tgz", + "integrity": "sha512-M4zilRPO5ZSLL5znGUT24Q1ywa655vW5bd26fPCHb+KSMB1QCGCobezOwGVL8bzQK7+08mb+Pbu/MwBjVG/e4w==", + "dependencies": { + "@powersync/common": "1.19.0", + "async-mutex": "^0.4.0", + "bson": "^6.6.0", + "comlink": "^4.4.1", + "js-logger": "^1.6.1" + }, + "peerDependencies": { + "@journeyapps/wa-sqlite": "^0.3.0", + "@powersync/common": "^1.19.0" + } + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@supabase/auth-js": { + "version": "2.65.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.0.tgz", + "integrity": "sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz", + "integrity": "sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.1.tgz", + "integrity": "sha512-EOSEZFm5pPuCPGCmLF1VOCS78DfkSz600PBuvBND/IZmMciJ1pmsS3ss6TkB6UkuvTybYiBh7gKOYyxoEO3USA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.2.tgz", + "integrity": "sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.0.tgz", + "integrity": "sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.45.4", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.4.tgz", + "integrity": "sha512-E5p8/zOLaQ3a462MZnmnz03CrduA5ySH9hZyL03Y+QZLIOO4/Gs8Rdy4ZCKDHsN7x0xdanVEWWFN3pJFQr9/hg==", + "dependencies": { + "@supabase/auth-js": "2.65.0", + "@supabase/functions-js": "2.4.1", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.16.1", + "@supabase/realtime-js": "2.10.2", + "@supabase/storage-js": "2.7.0" + } + }, + "node_modules/@swc/core": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.35.tgz", + "integrity": "sha512-3cUteCTbr2r5jqfgx0r091sfq5Mgh6F1SQh8XAOnSvtKzwv2bC31mvBHVAieD1uPa2kHJhLav20DQgXOhpEitw==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.13" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.7.35", + "@swc/core-darwin-x64": "1.7.35", + "@swc/core-linux-arm-gnueabihf": "1.7.35", + "@swc/core-linux-arm64-gnu": "1.7.35", + "@swc/core-linux-arm64-musl": "1.7.35", + "@swc/core-linux-x64-gnu": "1.7.35", + "@swc/core-linux-x64-musl": "1.7.35", + "@swc/core-win32-arm64-msvc": "1.7.35", + "@swc/core-win32-ia32-msvc": "1.7.35", + "@swc/core-win32-x64-msvc": "1.7.35" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.35.tgz", + "integrity": "sha512-BQSSozVxjxS+SVQz6e3GC/+OBWGIK3jfe52pWdANmycdjF3ch7lrCKTHTU7eHwyoJ96mofszPf5AsiVJF34Fwg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.35.tgz", + "integrity": "sha512-44TYdKN/EWtkU88foXR7IGki9JzhEJzaFOoPevfi9Xe7hjAD/x2+AJOWWqQNzDPMz9+QewLdUVLyR6s5okRgtg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.35.tgz", + "integrity": "sha512-ccfA5h3zxwioD+/z/AmYtkwtKz9m4rWTV7RoHq6Jfsb0cXHrd6tbcvgqRWXra1kASlE+cDWsMtEZygs9dJRtUQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.35.tgz", + "integrity": "sha512-hx65Qz+G4iG/IVtxJKewC5SJdki8PAPFGl6gC/57Jb0+jA4BIoGLD/J3Q3rCPeoHfdqpkCYpahtyUq8CKx41Jg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.35.tgz", + "integrity": "sha512-kL6tQL9No7UEoEvDRuPxzPTpxrvbwYteNRbdChSSP74j13/55G2/2hLmult5yFFaWuyoyU/2lvzjRL/i8OLZxg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.35.tgz", + "integrity": "sha512-Ke4rcLQSwCQ2LHdJX1FtnqmYNQ3IX6BddKlUtS7mcK13IHkQzZWp0Dcu6MgNA3twzb/dBpKX5GLy07XdGgfmyw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.35.tgz", + "integrity": "sha512-T30tlLnz0kYyDFyO5RQF5EQ4ENjW9+b56hEGgFUYmfhFhGA4E4V67iEx7KIG4u0whdPG7oy3qjyyIeTb7nElEw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.35.tgz", + "integrity": "sha512-CfM/k8mvtuMyX+okRhemfLt784PLS0KF7Q9djA8/Dtavk0L5Ghnq+XsGltO3d8B8+XZ7YOITsB14CrjehzeHsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.35.tgz", + "integrity": "sha512-ATB3uuH8j/RmS64EXQZJSbo2WXfRNpTnQszHME/sGaexsuxeijrp3DTYSFAA3R2Bu6HbIIX6jempe1Au8I3j+A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.35.tgz", + "integrity": "sha512-iDGfQO1571NqWUXtLYDhwIELA/wadH42ioGn+J9R336nWx40YICzy9UQyslWRhqzhQ5kT+QXAW/MoCWc058N6Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/types": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz", + "integrity": "sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz", + "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==" + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/comlink": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz", + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-logger": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/js-logger/-/js-logger-1.6.1.tgz", + "integrity": "sha512-yTgMCPXVjhmg28CuUH8CKjU+cIKL/G+zTu4Fn4lQxs8mRFH/03QTNvEFngcxfg/gRDiQAOoyCKmMTOm9ayOzXA==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz", + "integrity": "sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.7.0", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.3.0.tgz", + "integrity": "sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/demos/supabase-infinite-scroll/package.json b/demos/supabase-infinite-scroll/package.json new file mode 100644 index 00000000..79c45210 --- /dev/null +++ b/demos/supabase-infinite-scroll/package.json @@ -0,0 +1,21 @@ +{ + "name": "vite-project", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^5.4.1", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0" + }, + "dependencies": { + "@journeyapps/wa-sqlite": "^0.4.1", + "@powersync/web": "workspace:*", + "@supabase/supabase-js": "^2.45.4" + } +} diff --git a/demos/supabase-infinite-scroll/src/consoleList/index.js b/demos/supabase-infinite-scroll/src/consoleList/index.js new file mode 100644 index 00000000..224b8ce7 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/consoleList/index.js @@ -0,0 +1,113 @@ +export const useConsoleList = async (messages, context) => { + const logDiv = document.createElement("div"); + logDiv.style.marginBottom = "4px"; // Add bottom margin to each log item + + let textContent = []; + let objectSpans = []; + + Array.from(messages).forEach((message, index) => { + if (typeof message === "object" && message !== null) { + textContent.push("[object Object]"); + const objectSpan = document.createElement("span"); + objectSpan.style.display = "none"; + objectSpan.textContent = JSON.stringify(message); + objectSpan.classList.add("hidden-object"); + objectSpan.dataset.index = index; + objectSpans.push(objectSpan); + } else { + textContent.push(String(message)); + } + }); + + logDiv.textContent = textContent.join(" "); + objectSpans.forEach(span => { + logDiv.appendChild(span); + logDiv.addEventListener("click", itemClickHandler); + }); + + const consoleList = document.getElementById("console-list"); + + if (consoleList) { + consoleList.appendChild(logDiv); + + consoleList.scrollTop = consoleList.scrollHeight; + } else { + console.error("Element with id 'console-list' not found"); + } +}; + +function createJsonModal(jsonString) { + const modal = document.createElement('div'); + modal.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: white; + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + width: fit-content; + max-width: 90vw; + height: 80%; + display: flex; + flex-direction: column; + z-index: 1000; + `; + + const header = document.createElement('div'); + header.style.cssText = ` + padding: 10px; + border-bottom: 1px solid #eee; + display: flex; + justify-content: flex-end; + `; + + const closeButton = document.createElement('button'); + closeButton.textContent = '×'; + closeButton.style.cssText = ` + background: none; + border: none; + font-size: 20px; + cursor: pointer; + padding: 0; + line-height: 20px; + width: 20px; + height: 20px; + `; + closeButton.onclick = () => document.body.removeChild(modal); + + const contentContainer = document.createElement('div'); + contentContainer.style.cssText = ` + flex-grow: 1; + overflow-y: auto; + padding: 20px; + `; + + const pre = document.createElement('pre'); + pre.style.cssText = ` + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; + `; + pre.textContent = JSON.stringify(JSON.parse(jsonString), null, 2); + + header.appendChild(closeButton); + contentContainer.appendChild(pre); + modal.appendChild(header); + modal.appendChild(contentContainer); + document.body.appendChild(modal); +} + +function itemClickHandler(event) { + if (event.target.textContent.includes("[object Object]")) { + const text = event.target.getElementsByTagName("SPAN")[0].textContent; + createJsonModal(text); + } +} + +export function initializeConsoleList(messages, context) { + consoleListAppender(messages, context); +} + + + diff --git a/demos/supabase-infinite-scroll/src/dataSources/array.js b/demos/supabase-infinite-scroll/src/dataSources/array.js new file mode 100644 index 00000000..d330ebb5 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/dataSources/array.js @@ -0,0 +1,23 @@ +import Logger from "js-logger"; +Logger.useDefaults(); + +const logger = Logger.get("src/dataSources/pagedSync"); + +export default function useArray() { + const items = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`); + + async function getItems(start, count) { + logger.info(`array dataSournce get items from ${start} to ${start + count - 1}`); + logger.info(`items.length: ${items.length}`); + const rval = items.slice(start, start + count).map((item, index) => ({ + text: item, + id: start + index, + })); + return rval; + } + return { + getItems, + getItemCount: () => Math.max(items.length, 100), + name: "array" + }; +} \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/dataSources/index.js b/demos/supabase-infinite-scroll/src/dataSources/index.js new file mode 100644 index 00000000..c6220cc7 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/dataSources/index.js @@ -0,0 +1,6 @@ +import useArray from "./array.js"; +import usePreSync from "./preSync.js"; +import usePagedSync from "./pagedSync.js"; +import useTriggerSync from "./triggerSync.js"; + +export { useArray, usePreSync, usePagedSync, useTriggerSync }; diff --git a/demos/supabase-infinite-scroll/src/dataSources/pagedSync.js b/demos/supabase-infinite-scroll/src/dataSources/pagedSync.js new file mode 100644 index 00000000..9c4e4030 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/dataSources/pagedSync.js @@ -0,0 +1,57 @@ +// Control data sync using client parameters +import { execute, reconnect } from "../powersync"; + +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +import Logger from "js-logger"; +Logger.useDefaults(); + +const logger = Logger.get("src/dataSources/pagedSync"); + +export default function usePagedSync() { + let pageNumber = 0; + let pageSize = 0; + let buffer = []; + let itemCount = 0; + + async function getItems(start, count) { + while (start + count > itemCount) { + logger.info("pagedSync getItems", start, count, itemCount, pageNumber); + + if (await reconnect(++pageNumber)) { + //TODO: getAll instead of execute to simplify extracting results (see other implementations too) + const rval = await execute( + "SELECT * FROM paged_list WHERE page = ? ORDER BY created_at", + [pageNumber] + ); + let newRows = rval.rows._array; + + // Maintain buffer size by removing old items if necessary + if (buffer.length + newRows.length > 199) { + const keepCount = 199 - newRows.length; + buffer = buffer.slice(-keepCount); + } + + buffer = [...buffer, ...newRows]; + if (!pageSize) { + pageSize = newRows.length; + } + itemCount += newRows.length; + } else { + logger.warn("pagedSync failed to reconnect"); + return []; + } + } + + // Calculate the correct slice indices based on the requested start position + const bufferStart = Math.max(0, start - (itemCount - buffer.length)); + return buffer.slice(bufferStart, bufferStart + count); + } + return { + name: "pagedSync", + getItems, + getItemCount: () => 10000, + }; +} \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/dataSources/preSync.js b/demos/supabase-infinite-scroll/src/dataSources/preSync.js new file mode 100644 index 00000000..1b3d4c3e --- /dev/null +++ b/demos/supabase-infinite-scroll/src/dataSources/preSync.js @@ -0,0 +1,23 @@ +import { execute } from "../powersync"; +import Logger from "js-logger"; + +const logger = Logger.get("src/dataSources/preSync"); + +export default function usePreSync() { + let count = 0; + + return { + name: "preSync", + getItems: async (start, count) => { + logger.info(`preSync getItems ${start} to ${start + count - 1}`); + const rval = await execute( + "SELECT * FROM list ORDER BY created_at LIMIT ? OFFSET ?", + [count, start] + ); + logger.debug(`Retrieved ${rval.rows._array.length} items`); + count = count + rval.rows._array.length; + return rval.rows._array; + }, + getItemCount: () => Math.max(count, 10000) + }; +} \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/dataSources/triggerSync.js b/demos/supabase-infinite-scroll/src/dataSources/triggerSync.js new file mode 100644 index 00000000..17f546e0 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/dataSources/triggerSync.js @@ -0,0 +1,81 @@ +import { Supabase, PowerSync, execute } from "../powersync"; +import Logger from "js-logger"; + +const logger = Logger.get("src/dataSources/triggerSync"); + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); +export default function useTriggerSync() { + let itemCount = 0; + + async function sync(start, count) { + try { + return new Promise(async resolve => { + logger.info(`Syncing items from ${start} to ${start + count - 1}`); + let lastSyncedTime = + PowerSync.currentStatus?.lastSyncedAt?.getTime() || 0; + + const id = Supabase.currentSession.user.id; + + + const dispose = PowerSync.registerListener({ + statusChanged: async status => { + const currentSyncTime = status.lastSyncedAt.getTime(); + if ( + (!lastSyncedTime && currentSyncTime) || + currentSyncTime - lastSyncedTime > 0 + ) { + dispose(); + logger.debug("Sync completed successfully"); + resolve(true); + } + }, + }); + + const { data, error } = await Supabase.client.rpc( + "update_sync_to", + { + start_value: start + 1, + end_value: start + count, + user_uuid: id, + } + ); + if (error) { + logger.error("Error calling update_sync_to RPC:", error); + } + }); + } catch (error) { + console.log(error); + debugger; + } + } + + async function getItems(start, count) { + logger.info(`triggerSync getItems ${start} to ${start + count - 1}`); + + logger.info("getting items"); + let newRows; + + while ( + (newRows = await PowerSync.getAll( + `SELECT * FROM syncto_list ORDER BY created_at LIMIT ? OFFSET ?`, + [count, start] + )).length === 0 + ) { + await sync(start, count); + await sleep(1000); + } + if (newRows.length === 0) { + logger.info("No items found, stopping render"); + debugger; + } + + itemCount = start + count + 1; + return newRows; + } + + return { + name: "triggerSync", + getItems, + getItemCount: () => 10000, + }; +} \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/index.js b/demos/supabase-infinite-scroll/src/index.js new file mode 100644 index 00000000..c377e253 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/index.js @@ -0,0 +1,120 @@ +import { loginAnon, openDatabase } from "@/powersync"; +import { + useArray, + usePreSync, + usePagedSync, + useTriggerSync, +} from "./dataSources"; +import { useListBox } from "./listBox"; +import Logger from "js-logger"; +import { useConsoleList } from "./consoleList"; + + +Logger.useDefaults({ defaultLevel: Logger.DEBUG }); +Logger.setHandler((messages, context) => { + Logger.createDefaultHandler()(messages, context); + useConsoleList(messages, context); +}); + +const logger = Logger.get("src/index"); + +let config = { + itemsPerPage: 15, + prefetchCount: 30, + bufferSize: 30, + itemHeight: getItemHeight(), // Adjust this value based on your item's actual height +}; + +function getItemHeight() { + logger.debug("Calculating item height"); + const item = document.createElement("li"); + let itemHeight = 0; + + item.style.visibility = "hidden"; + item.textContent = "Dummy Item"; + item.classList.add("list-item"); + document.querySelector("#listItems").appendChild(item); + itemHeight = item.clientHeight + 4; // margin + item.remove(); + logger.debug(`Calculated item height: ${itemHeight}px`); + return itemHeight; +} + +async function usingArray() { + logger.info("Switching to Array Data Source"); + document.getElementById("which").textContent = "Array"; + + config.dataSource = useArray(); + useListBox(config); +} + +async function usingPreSync() { + logger.info("Switching to Pre-Sync Data Source"); + document.getElementById("which").textContent = "Pre-Sync"; + config.dataSource = usePreSync(); + + useListBox(config); +} + +async function usingPagedSync() { + logger.info("Switching to Paged Sync Data Source"); + document.getElementById("which").textContent = "Paged Sync"; + config.dataSource = await usePagedSync(); + config.prefetchCount = 0; + useListBox(config); +} + +async function usingTriggerSync() { + logger.info("Switching to Trigger Sync Data Source"); + config.dataSource = useTriggerSync(config.itemsPerPage); + document.getElementById("which").textContent = "Trigger Sync"; + config.prefetchCount = 100; + useListBox(config); +} + +async function setupListeners() { + document + .getElementById("arrayProvider") + .addEventListener("click", usingArray); + document + .getElementById("preSyncProvider") + .addEventListener("click", usingPreSync); + document + .getElementById("pagedSyncProvider") + .addEventListener("click", usingPagedSync); + document + .getElementById("triggerSyncProvider") + .addEventListener("click", usingTriggerSync); + + try { + try { + logger.info("Opening database"); + + const opened = await openDatabase(); + if (!opened) { + alert("Failed to open database."); + debugger; + } else { + logger.info("Database opened successfully"); + document.getElementById("preSyncProvider").disabled = false; + document.getElementById("pagedSyncProvider").disabled = false; + document.getElementById("triggerSyncProvider").disabled = false; + logger.info("Enabled sync provider buttons"); + } + } catch (error) { + logger.error("Error during initialization:", error); + } + } catch (error) { + logger.error("Error during initialization:", error); + } +} + +if (!document.getElementById("arrayProvider")) { + document.addEventListener("DOMContentLoaded", async () => { + logger.info("DOM content loaded, initializing application"); + + setupListeners(); + }); +} else { + setupListeners(); +} \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/listBox/index.js b/demos/supabase-infinite-scroll/src/listBox/index.js new file mode 100644 index 00000000..96a83ed5 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/listBox/index.js @@ -0,0 +1,163 @@ +import useItemBuffer from "./itemBuffer"; +import Logger from "js-logger"; + +const logger = Logger.get("src/listBox"); + +export async function useListBox(config) { + logger.info("Initializing ListBox with config:", JSON.stringify(config)); + + const itemBuffer = useItemBuffer(config); + const listContainer = document.getElementById("listBox"); + const list = listContainer.querySelector("#listItems"); + const spinner = document.getElementById("spinner"); // Spinner element + const { itemHeight, itemsPerPage } = config; + let eol = false; + let totalItems = 0; + let items = []; + let eofIndex = Number.MAX_SAFE_INTEGER; + + let isRendering = false; + let pendingRender = null; + + listContainer.style.height = `${itemHeight * itemsPerPage}px`; + + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + const debouncedHandleScroll = debounce(handleScroll, 100); + + if (listContainer._scrollHandler) { + listContainer.removeEventListener("scroll", listContainer._scrollHandler); + } + + listContainer._scrollHandler = debouncedHandleScroll; + listContainer.addEventListener("scroll", listContainer._scrollHandler); + + function showSpinner() { + spinner.classList.remove("hidden"); + } + + function hideSpinner() { + spinner.classList.add("hidden"); + } + + hideSpinner(); + + // Initialize by getting items and rendering them + await getItems(0, config.itemsPerPage) + .then(itemz => { + logger.info("Initial items", itemz); + items = itemz; + return getItemCount(); + }) + .then(async count => { + totalItems = count; + logger.info(`Set list height to ${totalItems * itemHeight}px`); + await renderItems(0); + }); + + listContainer.scrollTop = 0; + + async function getItems(startIndex, count) { + logger.debug( + `Getting items from ${startIndex} to ${startIndex + count - 1}` + ); + return await itemBuffer.getItems(startIndex, count); + } + + async function getItemCount() { + const count = await itemBuffer.getItemCount(); + logger.debug(`Total item count: ${count}`); + return count; + } + + async function handleScroll() { + let scrollTop = Math.round(listContainer.scrollTop / itemHeight); + + while (true) { + await renderItems(scrollTop); + if (!pendingRender) { + break; + } + scrollTop = pendingRender; + } + if (scrollTop >= eofIndex - itemsPerPage) { + list.style.marginTop = `${(eofIndex - itemsPerPage) * itemHeight}px`; + + listContainer.scrollTop = Math.round( + (eofIndex - itemsPerPage) * itemHeight + ); + } + } + + async function renderItems(startIndex) { + try { + return new Promise(async resolve => { + if (startIndex + itemsPerPage > eofIndex) { + logger.debug("Reached end of list. Skipping render."); + resolve(); + } + items = []; + showSpinner(); + items = await getItems(startIndex, itemsPerPage); + if (items.length === 0) { + const n = await getItemCount(); + eofIndex = startIndex = n; + list.style.marginTop = `${(startIndex + 1) * itemHeight}px`; + listContainer.scrollTop = (n - itemsPerPage) * itemHeight; + resolve(); + } + logger.info("Rendering items", items); + list.innerHTML = ""; + // debugger; + // list.style.transform = `translateY(${startIndex * itemHeight}px)`; + list.style.marginTop = `${startIndex * itemHeight}px`; + items.forEach(item => { + const div = document.createElement("div"); + div.className = "list-item"; + div.textContent = item.text; + list.appendChild(div); + }); + hideSpinner(); + resolve(); + }); + } catch (error) { + logger.error("Error during rendering:", error); + } finally { + isRendering = false; + } + } +} + +document.addEventListener("DOMContentLoaded", () => { + // Function to log the position of the listBox and move the spinner + function updateSpinnerPosition() { + const listBox = document.getElementById("listBox"); + const spinner = document.getElementById("spinner"); + + if (listBox && spinner) { + const rect = listBox.getBoundingClientRect(); + + // Move spinner to the upper left corner of the listBox + spinner.style.top = `${rect.top + 50}px`; + spinner.style.left = `${rect.left + 100}px`; + } else { + console.error("listBox or spinner element not found"); + } + } + + // Add event listener for window resize + window.addEventListener("resize", updateSpinnerPosition); + + // Initial update to position spinner on page load + updateSpinnerPosition(); +}); \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/listBox/itemBuffer.js b/demos/supabase-infinite-scroll/src/listBox/itemBuffer.js new file mode 100644 index 00000000..76cff7ee --- /dev/null +++ b/demos/supabase-infinite-scroll/src/listBox/itemBuffer.js @@ -0,0 +1,53 @@ +import Logger from "js-logger"; + +const logger = Logger.get("src/listBox/itemBuffer"); + +export default function useItemBuffer(config) { + let items = []; + let startIndex = 0; + let inhere = false; + + async function fetchItems(start, count) { + const result = await config.dataSource.getItems( + start, + count + config.prefetchCount + ); + return result; + } + + async function ensureItems(count) { + try { + inhere = true; + + if (count > items.length) { + const newItems = await fetchItems(items.length, count); + + items.push(...newItems); + } + inhere = false; + } catch (error) { + logger.error("Error fetching items:", error); + } + } + + async function getItems(start, count) { + + if (start + count >= items.length) { + await ensureItems(start + count); + } + + const result = [...items.slice(start, start + count)]; + return result; + } + + async function getItemCount() { + return config.dataSource.getItemCount(); + } + return { + items, + getBuffer: () => [...items], + ensureItems, + getItems, + getItemCount, + }; +} \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/powersync/index.js b/demos/supabase-infinite-scroll/src/powersync/index.js new file mode 100644 index 00000000..26991fac --- /dev/null +++ b/demos/supabase-infinite-scroll/src/powersync/index.js @@ -0,0 +1,150 @@ +import { PowerSyncDatabase } from "@powersync/web"; +import { SupabaseConnector } from "@/supabase"; +import { schema } from "./schema"; + +import Logger from "js-logger"; +Logger.useDefaults(); +Logger.setLevel(Logger.DEBUG); + +const logger = Logger.get("src/powersync"); +const internalLogger = Logger.get("internal"); +export let PowerSync; + +internalLogger.setLevel(Logger.DEBUG); + +export let Supabase; + +async function initSupabase() { + Supabase = new SupabaseConnector({ + powersyncUrl: import.meta.env.VITE_POWERSYNC_URL, + supabaseUrl: import.meta.env.VITE_SUPABASE_URL, + supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY, + }); + await Supabase.init(); +} + +const create = async () => { + logger.info("creating Supabase"); + await initSupabase(); + logger.info("Creating PowerSyncDatabase"); + PowerSync = new PowerSyncDatabase({ + schema, + database: { + dbFilename: import.meta.env.VITE_SQL_DB_FILENAME, + }, + // logger, + useWebWorker: false, + }); + window.$powerSync = PowerSync; + + logger.info("PowerSyncDatabase Created", PowerSync); +}; + +export const reconnect = async current_page => { + let dispose; + + logger.info("reconnecting to supabase ...", current_page); + + await PowerSync.disconnect(); + + return new Promise(async resolve => { + try { + let lastSyncedTime = + PowerSync.currentStatus?.lastSyncedAt?.getTime() || 0; + + const dispose = PowerSync.registerListener({ + statusChanged: async status => { + console.log("status", status); + const currentSyncTime = status.lastSyncedAt?.getTime() || 0; + logger.info("currentSyncTime", currentSyncTime); + logger.info("lastSyncedTime", lastSyncedTime); + logger.info("diff", currentSyncTime - lastSyncedTime); + if ( + (!lastSyncedTime && currentSyncTime) || + currentSyncTime - lastSyncedTime > 0 + ) { + dispose(); + logger.debug("Sync completed successfully"); + resolve(true); + } + }, + }); + + await PowerSync.connect(Supabase, { params: { current_page } }); + } catch (error) { + logger.error("Error reconnecting to supabase", error); + resolve(false); + dispose(); + } + }); +}; + +export const connect = async () => { + try { + logger.info("setting Supabase as PowerSync backend connector"); + await PowerSync.connect(Supabase); + logger.info("connected to supabase, waitForReady"); + await PowerSync.waitForReady(); + logger.info("wait for first sync"); + return PowerSync.waitForFirstSync().then(() => { + logger.info("First sync done"); + logger.info("connected to supabase"); + logger.info("connected to powersync"); + }); + } catch (error) { + logger.error("Error connecting to supabase", error); + } +}; + +export const loginAnon = async () => { + await Supabase.loginAnon(); +}; + +export const openDatabase = async config => { + try { + await create(); + await connect(); + return true; + } catch (error) { + logger.error("Error opening database", error); + return false; + } +}; + +export function watchList(onResult) { + PowerSync.watch(`SELECT * FROM list ORDER BY created_at`, [], { + onResult: result => { + onResult(result.rows); + }, + }); +} + +export const insertItem = async text => { + return PowerSync.execute( + "INSERT INTO list(id, text) VALUES(uuid(), ?) RETURNING *", + [text] + ); +}; + +export const updateItem = async (id, text) => { + return PowerSync.execute("UPDATE list SET text = ? WHERE id = ?", [ + text, + id, + ]); +}; + +export const deleteItem = async id => { + return PowerSync.execute("DELETE FROM list WHERE id = ?", [id]); +}; + +export const allItems = async () => { + return await PowerSync.getAll("SELECT * FROM list ORDER BY created_at"); +}; + +export const deleteAllItems = async () => { + return PowerSync.execute("DELETE FROM list"); +}; + +export const execute = async (sql, params) => { + return PowerSync.execute(sql, params); +}; \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/src/powersync/schema.js b/demos/supabase-infinite-scroll/src/powersync/schema.js new file mode 100644 index 00000000..bc2d49db --- /dev/null +++ b/demos/supabase-infinite-scroll/src/powersync/schema.js @@ -0,0 +1,36 @@ +import { column, Schema, Table } from "@powersync/web"; +// OR: import { column, Schema, Table } from '@powersync/react-native'; + +const list = new Table( + { + // id column (text) is automatically included + text: column.text, + created_at: column.text, + }, + { indexes: {} } +); + +const paged_list = new Table( + { + // id column (text) is automatically included + text: column.text, + page: column.integer, + created_at: column.text, + }, + { indexes: {} } +); + +const syncto_list = new Table( + { + // id column (text) is automatically included + text: column.text, + created_at: column.text, + }, + { indexes: {} } +); + +export const schema = new Schema({ + list, + paged_list, + syncto_list, +}); diff --git a/demos/supabase-infinite-scroll/src/supabase/index.js b/demos/supabase-infinite-scroll/src/supabase/index.js new file mode 100644 index 00000000..0c5a2bf7 --- /dev/null +++ b/demos/supabase-infinite-scroll/src/supabase/index.js @@ -0,0 +1,176 @@ +import { BaseObserver } from "@powersync/web"; +import { createClient } from "@supabase/supabase-js"; + +import Logger from "js-logger"; +Logger.useDefaults(); + +const logger = Logger.get("src/supabase"); + +const FATAL_RESPONSE_CODES = [ + // Postgres errors + /^22\d{3}$/, // Data exception + /^23\d{3}$/, // Integrity constraint violation + /^42\d{3}$/, // Syntax error or access rule violation + // Supabase errors + /^PGRST\d{3}$/, // PostgREST errors +]; + +export class SupabaseConnector extends BaseObserver { + constructor(config) { + super(); + logger.info("SupabaseConnector constructor"); + this.config = config; + this.client = createClient( + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_ANON_KEY, + { + auth: { + persistSession: true, + }, + } + ); + this.currentSession = null; + this.ready = false; + } + + async init() { + if (this.ready) { + return; + } + // Ensures that we don't accidentally check/create multiple anon sessions during initialization + // const release = await SupabaseConnector.SHARED_MUTEX.acquire(); + + let sessionResponse = await this.client.auth.getSession(); + if (sessionResponse.error) { + logger.error(sessionResponse.error); + throw sessionResponse.error; + } else if (!sessionResponse.data.session) { + logger.info("No session found, logging in anonymously"); + const anonUser = await this.client.auth.signInAnonymously(); + if (anonUser.error) { + throw anonUser.error; + } + sessionResponse = await this.client.auth.getSession(); + } + + this.updateSession(sessionResponse.data.session); + + this.ready = true; + this.iterateListeners(cb => cb.initialized?.()); + + // release(); + } + + async fetchCredentials() { + logger.info("fetching credentials"); + const { + data: { session }, + error, + } = await this.client.auth.getSession(); + + if (!session || error) { + throw new Error(`Could not fetch Supabase credentials: ${error}`); + } + + logger.info("session expires at", session.expires_at); + + this.updateSession(session); + const credentials = { + endpoint: import.meta.env.VITE_POWERSYNC_URL, + token: session.access_token ?? "", + expiresAt: session.expires_at + ? new Date(session.expires_at * 1000) + : undefined, + }; + logger.info("credentials", credentials); + return credentials; + } + + async uploadData(database) { + logger.info("uploading data"); + const transaction = await database.getNextCrudTransaction(); + + if (!transaction) { + return; + } + + let lastOp = null; + try { + // Note: If transactional consistency is important, use database functions + // or edge functions to process the entire transaction in a single call. + for (const op of transaction.crud) { + lastOp = op; + const table = this.client.from(op.table); + let result; + switch (op.op) { + case "PUT": + const record = { ...op.opData, id: op.id }; + result = await table.upsert(record); + break; + case "PATCH": + result = await table.update(op.opData).eq("id", op.id); + break; + case "DELETE": + result = await table.delete().eq("id", op.id); + break; + } + + if (result.error) { + logger.error(result.error); + throw new Error( + `Could not update Supabase. Received error: ${result.error.message}` + ); + } + } + + await transaction.complete(); + } catch (ex) { + logger.debug(ex); + if ( + typeof ex.code == "string" && + FATAL_RESPONSE_CODES.some(regex => regex.test(ex.code)) + ) { + /** + * Instead of blocking the queue with these errors, + * discard the (rest of the) transaction. + * + * Note that these errors typically indicate a bug in the application. + * If protecting against data loss is important, save the failing records + * elsewhere instead of discarding, and/or notify the user. + */ + logger.info(`Data upload error - discarding ${lastOp}`, ex); + await transaction.complete(); + } else { + // Error may be retryable - e.g. network error or temporary server error. + // Throwing an error here causes this call to be retried after a delay. + throw ex; + } + } + } + + async loginAnon() { + const { + data: { session }, + error, + } = await this.client.auth.signInAnonymously(); + + if (error) { + throw error; + } + + this.updateSession(session); + } + + async logout() { + await this.client.auth.signOut(); + } + + updateSession(session) { + logger.info("updateSession", session); + this.currentSession = session; + if (!session) { + return; + } + this.iterateListeners(cb => cb.sessionStarted?.(session)); + } +} diff --git a/demos/supabase-infinite-scroll/styles.css b/demos/supabase-infinite-scroll/styles.css new file mode 100644 index 00000000..5f979656 --- /dev/null +++ b/demos/supabase-infinite-scroll/styles.css @@ -0,0 +1,110 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + margin: 0; + padding: 20px; + box-sizing: border-box; +} + +#app { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +h1, +h3 { + text-align: center; + width: 300px; + margin-bottom: 0px; +} + +h3 { + margin: 0px; +} +.content-wrapper { + display: flex; + align-items: flex-start; + gap: 20px; +} + +#console-wrapper { + width: 250px; + height: 386px; + border: 1px solid #ccc; + overflow-y: scroll; + position: relative; +} + +#listBox, #console-list { + width: 250px; + height: 390px; + border: 1px solid #ccc; + overflow-y: scroll; + position: relative; +} + +#listItems { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 20000px; +} + +.list-item { + padding: 1px; + cursor: pointer; + height: 20px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-bottom: 4px; +} + +.button-container { + display: flex; + flex-direction: column; + gap: 10px; +} + +button { + padding: 10px; + cursor: pointer; +} + + +#listItems { + position: relative; +} + +#spinner { + position: absolute; /* Make sure spinner does not take up space in the document flow */ + top: 180px; /* Position the spinner 1/5th of the way down from the top of #listBox */ + left: 100px; /* Center horizontally */ + transform: translate(-50%, 0); /* Adjust the spinner to be perfectly centered */ + width: 20px; /* Smaller spinner size */ + height: 20px; /* Smaller spinner size */ + border: 4px solid #f0f0f0; /* Light grey border */ + border-top: 4px solid #888; /* Dark grey spinner indicator */ + border-radius: 50%; + animation: spin 1s linear infinite; + z-index: 1000; /* Spinner on top of other content */ + pointer-events: none; /* Ensure spinner doesn't interfere with scrolling */ +} + +.hidden { + display: none; /* Hide spinner when not rendering */ +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/demos/supabase-infinite-scroll/syncrules.yaml b/demos/supabase-infinite-scroll/syncrules.yaml new file mode 100644 index 00000000..d478c4be --- /dev/null +++ b/demos/supabase-infinite-scroll/syncrules.yaml @@ -0,0 +1,18 @@ +bucket_definitions: + global: + data: + # Sync all rows (scenario "Pre-sync all data and query the local database") + - SELECT * FROM list + by_page: + # Sync by page number (scenario "Control data sync using client parameters") + accept_potentially_dangerous_queries: true + parameters: SELECT (request.parameters() ->> 'current_page') as page_number + data: + - SELECT * FROM paged_list WHERE paged_list.page = bucket.page_number + + by_uuid: + # Sync rows having user_id in sync_to array (secnario "Client-side triggers a server-side function to flag data to sync") + parameters: SELECT request.user_id() as user_id + data: + - SELECT id, text, created_at from syncto_list WHERE bucket.user_id IN sync_to + \ No newline at end of file diff --git a/demos/supabase-infinite-scroll/vite.config.js b/demos/supabase-infinite-scroll/vite.config.js new file mode 100644 index 00000000..777e9f00 --- /dev/null +++ b/demos/supabase-infinite-scroll/vite.config.js @@ -0,0 +1,34 @@ +import path from "path"; +import wasm from "vite-plugin-wasm"; +import topLevelAwait from "vite-plugin-top-level-await"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + base: "/iscroll/", + root: ".", + build: { + outDir: "./dist", + rollupOptions: { + input: "index.html", + }, + emptyOutDir: true, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + envDir: ".", // Use this dir for env vars, not 'src'. + optimizeDeps: { + // Don't optimize these packages as they contain web workers and WASM files. + // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673 + exclude: ["@journeyapps/wa-sqlite", "@powersync/web"], + include: ["@powersync/web > js-logger",], + }, + plugins: [wasm(), topLevelAwait()], + worker: { + format: "es", + plugins: () => [wasm(), topLevelAwait()], + }, +});