From d9ce884513855603647911f6e635cec93bc2d0b5 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 26 May 2025 14:06:21 +0000 Subject: [PATCH] fix: configure Vite for proper Node.js module handling in Electron - Add vite-plugin-node-polyfills to provide Node.js built-in module polyfills - Configure build target as 'node18' for Electron environment - Switch to CommonJS format for Electron builds - Add specific polyfills for sqlite3 dependencies (util, stream, buffer) - Mark Node.js built-in modules as external in Electron builds This fixes the "Module util has been externalized" error by properly handling Node.js modules in the Electron environment, particularly for sqlite3 which depends on Node.js built-in modules. --- package-lock.json | 414 ++++++++++++++++++ package.json | 1 + src/main.capacitor.ts | 16 +- src/main.common.ts | 20 +- src/main.electron.ts | 15 +- src/main.pywebview.ts | 15 +- src/main.web.ts | 14 +- src/services/CapacitorPlatformService.ts | 139 ------ src/services/ElectronPlatformService.ts | 283 +++++++----- src/services/PlatformService.ts | 40 +- src/services/PlatformServiceFactory.ts | 58 ++- src/services/WebPlatformService.ts | 43 -- .../platforms/CapacitorPlatformService.ts | 10 + .../platforms/ElectronPlatformService.ts | 211 ++++----- .../platforms/PyWebViewPlatformService.ts | 10 + src/services/platforms/WebPlatformService.ts | 100 ++++- src/services/sqlite/WebSQLiteService.ts | 170 +++++++ src/views/HomeView.vue | 2 + vite.config.common.mts | 58 ++- 19 files changed, 1161 insertions(+), 458 deletions(-) delete mode 100644 src/services/CapacitorPlatformService.ts delete mode 100644 src/services/WebPlatformService.ts create mode 100644 src/services/sqlite/WebSQLiteService.ts diff --git a/package-lock.json b/package-lock.json index a5839a78..c9a29aae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,6 +130,7 @@ "tailwindcss": "^3.4.1", "typescript": "~5.2.2", "vite": "^5.2.0", + "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^0.19.8" } }, @@ -8077,6 +8078,29 @@ } } }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "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/plugin-node-resolve": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", @@ -11432,6 +11456,20 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -12177,6 +12215,16 @@ "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", "license": "Apache-2.0" }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } + }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -12311,6 +12359,23 @@ "dev": true, "license": "MIT" }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", @@ -12490,6 +12555,13 @@ "node": ">=12" } }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -13614,6 +13686,12 @@ "optional": true, "peer": true }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -13621,6 +13699,13 @@ "devOptional": true, "license": "ISC" }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/conventional-changelog": { "version": "3.1.25", "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", @@ -15017,6 +15102,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -18011,6 +18109,13 @@ "node": ">=10.19.0" } }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -18295,6 +18400,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -18625,6 +18747,23 @@ "dev": true, "license": "MIT" }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -18937,6 +19076,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/isomorphic-webcrypto": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.8.tgz", @@ -22927,6 +23076,92 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-stdlib-browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", + "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.12.1", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stdlib-browser/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-stdlib-browser/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -23283,6 +23518,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -23482,6 +23734,13 @@ "node": ">= 6" } }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -23887,6 +24146,19 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/playwright": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", @@ -24680,6 +24952,31 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -27673,6 +27970,44 @@ "node": ">= 0.10.0" } }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-http/node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/streamx": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", @@ -28508,6 +28843,19 @@ "node": ">= 6" } }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -28719,6 +29067,13 @@ "dev": true, "license": "0BSD" }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, + "license": "MIT" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -29387,6 +29742,27 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", @@ -29400,6 +29776,20 @@ "dev": true, "license": "(WTFPL OR MIT)" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -29555,6 +29945,23 @@ } } }, + "node_modules/vite-plugin-node-polyfills": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.23.0.tgz", + "integrity": "sha512-4n+Ys+2bKHQohPBKigFlndwWQ5fFKwaGY6muNDMTb0fSQLyBzS+jjUNRZG9sKF0S/Go4ApG6LFnUGopjkILg3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, "node_modules/vite-plugin-pwa": { "version": "0.19.8", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.19.8.tgz", @@ -29622,6 +30029,13 @@ "optional": true, "peer": true }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.15", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.15.tgz", diff --git a/package.json b/package.json index fe27c829..cf73c1e0 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "tailwindcss": "^3.4.1", "typescript": "~5.2.2", "vite": "^5.2.0", + "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^0.19.8" }, "main": "./dist-electron/main.js", diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index b0b4290f..71607af8 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -86,5 +86,19 @@ const handleDeepLink = async (data: { url: string }) => { App.addListener("appUrlOpen", handleDeepLink); logger.log("[Capacitor] Mounting app"); -app.mount("#app"); + +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try restarting the app or contact support if the problem persists.

+
+ `; +}); + logger.log("[Capacitor] App mounted"); diff --git a/src/main.common.ts b/src/main.common.ts index 7781f0ee..cea41dc1 100644 --- a/src/main.common.ts +++ b/src/main.common.ts @@ -32,7 +32,7 @@ function setupGlobalErrorHandler(app: VueApp) { } // Function to initialize the app -export function initializeApp() { +export async function initializeApp() { logger.log("[App Init] Starting app initialization"); logger.log("[App Init] Platform:", process.env.VITE_PLATFORM); @@ -55,15 +55,21 @@ export function initializeApp() { app.use(Notifications); logger.log("[App Init] Notifications initialized"); - app.config.globalProperties.$platform = PlatformServiceFactory.getInstance(); + // Initialize platform service + const platform = await PlatformServiceFactory.getInstance(); + app.config.globalProperties.$platform = platform; + logger.log("[App Init] Platform service initialized"); - (async () => { - const platform = app.config.globalProperties.$platform; + // Initialize SQLite + try { const sqlite = await platform.getSQLite(); - const config = { name: "TimeSafariDB", useWAL: true }; // (or your desired config) + const config = { name: "TimeSafariDB", useWAL: true }; await sqlite.initialize(config); - logger.log("[App Init] SQLite database initialized."); - })(); + logger.log("[App Init] SQLite database initialized"); + } catch (error) { + logger.error("[App Init] Failed to initialize SQLite:", error); + // Don't throw here - we want the app to start even if SQLite fails + } setupGlobalErrorHandler(app); logger.log("[App Init] App initialization complete"); diff --git a/src/main.electron.ts b/src/main.electron.ts index 3e1e492f..5fd06e1f 100644 --- a/src/main.electron.ts +++ b/src/main.electron.ts @@ -1,4 +1,15 @@ import { initializeApp } from "./main.common"; -const app = initializeApp(); -app.mount("#app"); +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try restarting the app or contact support if the problem persists.

+
+ `; +}); diff --git a/src/main.pywebview.ts b/src/main.pywebview.ts index 3e1e492f..5fd06e1f 100644 --- a/src/main.pywebview.ts +++ b/src/main.pywebview.ts @@ -1,4 +1,15 @@ import { initializeApp } from "./main.common"; -const app = initializeApp(); -app.mount("#app"); +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try restarting the app or contact support if the problem persists.

+
+ `; +}); diff --git a/src/main.web.ts b/src/main.web.ts index 51280cc1..de5cc129 100644 --- a/src/main.web.ts +++ b/src/main.web.ts @@ -19,4 +19,16 @@ function sqlInit() { } sqlInit(); -app.mount("#app"); +// Initialize and mount the app +initializeApp().then((app) => { + app.mount("#app"); +}).catch((error) => { + console.error("Failed to initialize app:", error); + document.body.innerHTML = ` +
+

Failed to initialize app

+

${error instanceof Error ? error.message : "Unknown error"}

+

Please try refreshing the page or contact support if the problem persists.

+
+ `; +}); diff --git a/src/services/CapacitorPlatformService.ts b/src/services/CapacitorPlatformService.ts deleted file mode 100644 index eafd7e47..00000000 --- a/src/services/CapacitorPlatformService.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - PlatformService, - PlatformCapabilities, - SQLiteOperations, -} from "./PlatformService"; -import { CapacitorSQLiteService } from "./sqlite/CapacitorSQLiteService"; -import { Capacitor } from "@capacitor/core"; -import { Camera } from "@capacitor/camera"; -import { Filesystem, Directory } from "@capacitor/filesystem"; -import { logger } from "../utils/logger"; - -export class CapacitorPlatformService implements PlatformService { - private sqliteService: CapacitorSQLiteService | null = null; - - getCapabilities(): PlatformCapabilities { - const platform = Capacitor.getPlatform(); - return { - hasFileSystem: true, - hasCamera: true, - isMobile: true, - isIOS: platform === "ios", - hasFileDownload: true, - needsFileHandlingInstructions: false, - sqlite: { - supported: true, - runsInWorker: false, - hasSharedArrayBuffer: false, - supportsWAL: true, - maxSize: 1024 * 1024 * 1024 * 2, // 2GB limit for mobile SQLite - }, - }; - } - - async getSQLite(): Promise { - if (!this.sqliteService) { - this.sqliteService = new CapacitorSQLiteService(); - } - return this.sqliteService; - } - - async readFile(path: string): Promise { - try { - const result = await Filesystem.readFile({ - path, - directory: Directory.Data, - }); - return result.data; - } catch (error) { - logger.error("Failed to read file:", error); - throw error; - } - } - - async writeFile(path: string, content: string): Promise { - try { - await Filesystem.writeFile({ - path, - data: content, - directory: Directory.Data, - }); - } catch (error) { - logger.error("Failed to write file:", error); - throw error; - } - } - - async deleteFile(path: string): Promise { - try { - await Filesystem.deleteFile({ - path, - directory: Directory.Data, - }); - } catch (error) { - logger.error("Failed to delete file:", error); - throw error; - } - } - - async listFiles(directory: string): Promise { - try { - const result = await Filesystem.readdir({ - path: directory, - directory: Directory.Data, - }); - return result.files.map((file) => file.name); - } catch (error) { - logger.error("Failed to list files:", error); - throw error; - } - } - - async takePicture(): Promise<{ blob: Blob; fileName: string }> { - try { - const image = await Camera.getPhoto({ - quality: 90, - allowEditing: true, - resultType: "base64", - }); - - const response = await fetch( - `data:image/jpeg;base64,${image.base64String}`, - ); - const blob = await response.blob(); - const fileName = `photo_${Date.now()}.jpg`; - - return { blob, fileName }; - } catch (error) { - logger.error("Failed to take picture:", error); - throw error; - } - } - - async pickImage(): Promise<{ blob: Blob; fileName: string }> { - try { - const image = await Camera.getPhoto({ - quality: 90, - allowEditing: true, - resultType: "base64", - source: "PHOTOLIBRARY", - }); - - const response = await fetch( - `data:image/jpeg;base64,${image.base64String}`, - ); - const blob = await response.blob(); - const fileName = `image_${Date.now()}.jpg`; - - return { blob, fileName }; - } catch (error) { - logger.error("Failed to pick image:", error); - throw error; - } - } - - async handleDeepLink(url: string): Promise { - // Implement deep link handling for Capacitor platform - logger.info("Handling deep link:", url); - } -} diff --git a/src/services/ElectronPlatformService.ts b/src/services/ElectronPlatformService.ts index cdca003d..ba77dbdd 100644 --- a/src/services/ElectronPlatformService.ts +++ b/src/services/ElectronPlatformService.ts @@ -4,21 +4,92 @@ import { SQLiteOperations, SQLiteConfig, PreparedStatement, + SQLiteResult, + ImageResult, } from "./PlatformService"; import { BaseSQLiteService } from "./sqlite/BaseSQLiteService"; import { app } from "electron"; import { dialog } from "electron"; -import { promises as fs } from "fs"; -import { join } from "path"; +import fs from "fs"; +import path from "path"; import sqlite3 from "sqlite3"; import { open, Database } from "sqlite"; import { logger } from "../utils/logger"; +import { Settings } from "../db/tables/settings"; +import { Account } from "../db/tables/accounts"; +import { Contact } from "../db/tables/contacts"; +import { db } from "../db"; +import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; +import { accountsDBPromise } from "../db"; +import { accessToken } from "../libs/crypto"; +import { getPlanFromCache as getPlanFromCacheImpl } from "../libs/endorserServer"; +import { PlanSummaryRecord } from "../interfaces/records"; +import { Axios } from "axios"; + +interface SQLiteDatabase extends Database { + changes: number; +} + +// Create Promise-based versions of fs functions +const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, { encoding }, (err: NodeJS.ErrnoException | null, data: string) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; + +const readFileBufferAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; + +const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, data, { encoding }, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; + +const unlinkAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; + +const readdirAsync = (dirPath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readdir(dirPath, (err: NodeJS.ErrnoException | null, files: string[]) => { + if (err) reject(err); + else resolve(files); + }); + }); +}; + +const statAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { + if (err) reject(err); + else resolve(stats); + }); + }); +}; /** * SQLite implementation for Electron using native sqlite3 */ class ElectronSQLiteService extends BaseSQLiteService { - private db: Database | null = null; + private db: SQLiteDatabase | null = null; private config: SQLiteConfig | null = null; async initialize(config: SQLiteConfig): Promise { @@ -28,7 +99,7 @@ class ElectronSQLiteService extends BaseSQLiteService { try { this.config = config; - const dbPath = join(app.getPath("userData"), `${config.name}.db`); + const dbPath = path.join(app.getPath("userData"), `${config.name}.db`); this.db = await open({ filename: dbPath, @@ -80,7 +151,7 @@ class ElectronSQLiteService extends BaseSQLiteService { try { if (operation === "query") { - const rows = await this.db.all(sql, params); + const rows = await this.db.all(sql, params); const result = await this.db.run("SELECT last_insert_rowid() as id"); return { rows, @@ -167,8 +238,8 @@ class ElectronSQLiteService extends BaseSQLiteService { } try { - const dbPath = join(app.getPath("userData"), `${this.config.name}.db`); - const stats = await fs.stat(dbPath); + const dbPath = path.join(app.getPath("userData"), `${this.config.name}.db`); + const stats = await statAsync(dbPath); return stats.size; } catch (error) { logger.error("Failed to get database size:", error); @@ -177,119 +248,123 @@ class ElectronSQLiteService extends BaseSQLiteService { } } -export class ElectronPlatformService implements PlatformService { - private sqliteService: ElectronSQLiteService | null = null; +// Only import Electron-specific code in Electron environment +let ElectronPlatformServiceImpl: typeof import("./platforms/ElectronPlatformService").ElectronPlatformService; + +async function initializeElectronPlatformService() { + if (process.env.ELECTRON) { + // Dynamic import for Electron environment + const { ElectronPlatformService } = await import("./platforms/ElectronPlatformService"); + ElectronPlatformServiceImpl = ElectronPlatformService; + } else { + // Stub implementation for non-Electron environments + class StubElectronPlatformService implements PlatformService { + #sqliteService: SQLiteOperations | null = null; + + getCapabilities(): PlatformCapabilities { + throw new Error("Electron platform service is not available in this environment"); + } - getCapabilities(): PlatformCapabilities { - return { - hasFileSystem: true, - hasCamera: true, - isMobile: false, - isIOS: false, - hasFileDownload: true, - needsFileHandlingInstructions: false, - sqlite: { - supported: true, - runsInWorker: false, - hasSharedArrayBuffer: true, - supportsWAL: true, - maxSize: 1024 * 1024 * 1024 * 10, // 10GB limit for desktop SQLite - }, - }; - } + async getSQLite(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async getSQLite(): Promise { - if (!this.sqliteService) { - this.sqliteService = new ElectronSQLiteService(); - } - return this.sqliteService; - } + async readFile(path: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async readFile(path: string): Promise { - try { - return await fs.readFile(path, "utf-8"); - } catch (error) { - logger.error("Failed to read file:", error); - throw error; - } - } + async writeFile(path: string, content: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async writeFile(path: string, content: string): Promise { - try { - await fs.writeFile(path, content, "utf-8"); - } catch (error) { - logger.error("Failed to write file:", error); - throw error; - } - } + async deleteFile(path: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async deleteFile(path: string): Promise { - try { - await fs.unlink(path); - } catch (error) { - logger.error("Failed to delete file:", error); - throw error; - } - } + async listFiles(directory: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async listFiles(directory: string): Promise { - try { - const files = await fs.readdir(directory); - return files; - } catch (error) { - logger.error("Failed to list files:", error); - throw error; - } - } + async takePicture(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async takePicture(): Promise<{ blob: Blob; fileName: string }> { - try { - const { canceled, filePaths } = await dialog.showOpenDialog({ - properties: ["openFile"], - filters: [{ name: "Images", extensions: ["jpg", "png", "gif"] }], - }); + async pickImage(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - if (canceled || !filePaths.length) { - throw new Error("No image selected"); + async handleDeepLink(url: string): Promise { + throw new Error("Electron platform service is not available in this environment"); } - const filePath = filePaths[0]; - const buffer = await fs.readFile(filePath); - const blob = new Blob([buffer], { type: "image/jpeg" }); - const fileName = `photo_${Date.now()}.jpg`; + async getAccounts(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - return { blob, fileName }; - } catch (error) { - logger.error("Failed to take picture:", error); - throw error; - } - } + async getAccount(did: string): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async pickImage(): Promise<{ blob: Blob; fileName: string }> { - try { - const { canceled, filePaths } = await dialog.showOpenDialog({ - properties: ["openFile"], - filters: [{ name: "Images", extensions: ["jpg", "png", "gif"] }], - }); + async addAccount(account: Account): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - if (canceled || !filePaths.length) { - throw new Error("No image selected"); + async getContacts(): Promise { + throw new Error("Electron platform service is not available in this environment"); } - const filePath = filePaths[0]; - const buffer = await fs.readFile(filePath); - const blob = new Blob([buffer], { type: "image/jpeg" }); - const fileName = `image_${Date.now()}.jpg`; + async getAllContacts(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - return { blob, fileName }; - } catch (error) { - logger.error("Failed to pick image:", error); - throw error; - } - } + async updateMasterSettings(settingsChanges: Partial): Promise { + throw new Error("Electron platform service is not available in this environment"); + } - async handleDeepLink(url: string): Promise { - // Implement deep link handling for Electron platform - logger.info("Handling deep link:", url); + async getActiveAccountSettings(): Promise { + throw new Error("Electron platform service is not available in this environment"); + } + + async updateAccountSettings(accountDid: string, settingsChanges: Partial): Promise { + throw new Error("Electron platform service is not available in this environment"); + } + + async getHeaders(did?: string): Promise> { + throw new Error("Electron platform service is not available in this environment"); + } + + async getPlanFromCache( + handleId: string | undefined, + axios: Axios, + apiServer: string, + requesterDid?: string, + ): Promise { + throw new Error("Electron platform service is not available in this environment"); + } + + isCapacitor(): boolean { + return false; + } + + isElectron(): boolean { + return false; + } + + isPyWebView(): boolean { + return false; + } + + isWeb(): boolean { + return false; + } + } + ElectronPlatformServiceImpl = StubElectronPlatformService; } } + +// Initialize the service +initializeElectronPlatformService().catch(error => { + logger.error("Failed to initialize Electron platform service:", error); +}); + +export class ElectronPlatformService extends ElectronPlatformServiceImpl {} diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index 06a0eabe..c9d2da37 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -1,5 +1,8 @@ import { Settings } from "../db/tables/settings"; import { Account } from "../db/tables/accounts"; +import { Contact } from "../db/tables/contacts"; +import { Axios } from "axios"; +import { PlanSummaryRecord } from "../interfaces/records"; /** * Represents the result of an image capture or selection operation. @@ -256,7 +259,14 @@ export interface PlatformService { * For browsers, this will use absurd-sql with Web Worker support. * @returns Promise resolving to the SQLite operations interface */ - getSQLite?(): Promise; + getSQLite(): Promise; + + /** + * Gets the headers for HTTP requests, including authorization if needed + * @param did - Optional DID to include in authorization + * @returns Promise resolving to headers object + */ + getHeaders(did?: string): Promise>; // Account Management /** @@ -303,4 +313,32 @@ export interface PlatformService { accountDid: string, settingsChanges: Partial, ): Promise; + + // Contact Management + /** + * Gets all contacts from the database + * @returns Promise resolving to array of contacts + */ + getContacts(): Promise; + + /** + * Gets all contacts from the database (alias for getContacts) + * @returns Promise resolving to array of contacts + */ + getAllContacts(): Promise; + + /** + * Retrieves plan data from cache or server + * @param handleId - Plan handle ID + * @param axios - Axios instance for making HTTP requests + * @param apiServer - API server URL + * @param requesterDid - Optional requester DID for private info + * @returns Promise resolving to plan data or undefined if not found + */ + getPlanFromCache( + handleId: string | undefined, + axios: Axios, + apiServer: string, + requesterDid?: string, + ): Promise; } diff --git a/src/services/PlatformServiceFactory.ts b/src/services/PlatformServiceFactory.ts index f5e34fa2..a6c8fdd9 100644 --- a/src/services/PlatformServiceFactory.ts +++ b/src/services/PlatformServiceFactory.ts @@ -1,8 +1,5 @@ import { PlatformService } from "./PlatformService"; import { WebPlatformService } from "./platforms/WebPlatformService"; -import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService"; -import { ElectronPlatformService } from "./platforms/ElectronPlatformService"; -import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService"; /** * Factory class for creating platform-specific service implementations. @@ -17,7 +14,7 @@ import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService"; * * @example * ```typescript - * const platformService = PlatformServiceFactory.getInstance(); + * const platformService = await PlatformServiceFactory.getInstance(); * await platformService.takePicture(); * ``` */ @@ -28,31 +25,48 @@ export class PlatformServiceFactory { * Gets or creates the singleton instance of PlatformService. * Creates the appropriate platform-specific implementation based on environment. * - * @returns {PlatformService} The singleton instance of PlatformService + * @returns {Promise} Promise resolving to the singleton instance of PlatformService */ - public static getInstance(): PlatformService { + public static async getInstance(): Promise { if (PlatformServiceFactory.instance) { return PlatformServiceFactory.instance; } const platform = process.env.VITE_PLATFORM || "web"; - switch (platform) { - case "capacitor": - PlatformServiceFactory.instance = new CapacitorPlatformService(); - break; - case "electron": - PlatformServiceFactory.instance = new ElectronPlatformService(); - break; - case "pywebview": - PlatformServiceFactory.instance = new PyWebViewPlatformService(); - break; - case "web": - default: - PlatformServiceFactory.instance = new WebPlatformService(); - break; - } + try { + switch (platform) { + case "capacitor": { + const { CapacitorPlatformService } = await import("./platforms/CapacitorPlatformService"); + PlatformServiceFactory.instance = new CapacitorPlatformService(); + break; + } + case "electron": { + const { ElectronPlatformService } = await import("./ElectronPlatformService"); + PlatformServiceFactory.instance = new ElectronPlatformService(); + break; + } + case "pywebview": { + const { PyWebViewPlatformService } = await import("./platforms/PyWebViewPlatformService"); + PlatformServiceFactory.instance = new PyWebViewPlatformService(); + break; + } + case "web": + default: + PlatformServiceFactory.instance = new WebPlatformService(); + break; + } + + if (!PlatformServiceFactory.instance) { + throw new Error(`Failed to initialize platform service for ${platform}`); + } - return PlatformServiceFactory.instance; + return PlatformServiceFactory.instance; + } catch (error) { + console.error(`Failed to initialize ${platform} platform service:`, error); + // Fallback to web platform if initialization fails + PlatformServiceFactory.instance = new WebPlatformService(); + return PlatformServiceFactory.instance; + } } } diff --git a/src/services/WebPlatformService.ts b/src/services/WebPlatformService.ts deleted file mode 100644 index 8af408b9..00000000 --- a/src/services/WebPlatformService.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - PlatformService, - PlatformCapabilities, - SQLiteOperations, -} from "./PlatformService"; -import { AbsurdSQLService } from "./sqlite/AbsurdSQLService"; -import { logger } from "../utils/logger"; - -export class WebPlatformService implements PlatformService { - private sqliteService: AbsurdSQLService | null = null; - - getCapabilities(): PlatformCapabilities { - return { - hasFileSystem: false, - hasCamera: "mediaDevices" in navigator, - isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), - isIOS: /iPhone|iPad|iPod/i.test(navigator.userAgent), - hasFileDownload: true, - needsFileHandlingInstructions: true, - sqlite: { - supported: true, - runsInWorker: true, - hasSharedArrayBuffer: typeof SharedArrayBuffer !== "undefined", - supportsWAL: true, - maxSize: 1024 * 1024 * 1024, // 1GB limit for IndexedDB - }, - }; - } - - async getSQLite(): Promise { - if (!this.sqliteService) { - this.sqliteService = new AbsurdSQLService(); - } - return this.sqliteService; - } - - // ... existing file system and camera methods ... - - async handleDeepLink(url: string): Promise { - // Implement deep link handling for web platform - logger.info("Handling deep link:", url); - } -} diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index aac2bf08..76f06776 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -10,6 +10,7 @@ import { logger } from "../../utils/logger"; import { Account } from "../../db/tables/accounts"; import { Settings } from "../../db/tables/settings"; import { db } from "../../db"; +import { Contact } from "../../db/tables/contacts"; /** * Platform service implementation for Capacitor (mobile) platform. @@ -510,4 +511,13 @@ export class CapacitorPlatformService implements PlatformService { ): Promise { throw new Error("Not implemented"); } + + // Contact Management + async getContacts(): Promise { + return await db.contacts.toArray(); + } + + async getAllContacts(): Promise { + return await this.getContacts(); + } } diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 9b2ca820..012409e9 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -1,145 +1,102 @@ import { - ImageResult, PlatformService, PlatformCapabilities, + SQLiteOperations, + SQLiteConfig, + PreparedStatement, + SQLiteResult, + ImageResult, } from "../PlatformService"; +import { BaseSQLiteService } from "../sqlite/BaseSQLiteService"; +import { app } from "electron"; +import { dialog } from "electron"; +import fs from "fs"; +import path from "path"; +import sqlite3 from "sqlite3"; +import { open, Database } from "sqlite"; import { logger } from "../../utils/logger"; -import { Account } from "../../db/tables/accounts"; import { Settings } from "../../db/tables/settings"; +import { Account } from "../../db/tables/accounts"; +import { Contact } from "../../db/tables/contacts"; import { db } from "../../db"; +import { MASTER_SETTINGS_KEY } from "../../db/tables/settings"; +import { accountsDBPromise } from "../../db"; +import { accessToken } from "../../libs/crypto"; +import { getPlanFromCache as getPlanFromCacheImpl } from "../../libs/endorserServer"; +import { PlanSummaryRecord } from "../../interfaces/records"; +import { Axios } from "axios"; -/** - * Platform service implementation for Electron (desktop) platform. - * Note: This is a placeholder implementation with most methods currently unimplemented. - * Implements the PlatformService interface but throws "Not implemented" errors for most operations. - * - * @remarks - * This service is intended for desktop application functionality through Electron. - * Future implementations should provide: - * - Native file system access - * - Desktop camera integration - * - System-level features - */ -export class ElectronPlatformService implements PlatformService { - /** - * Gets the capabilities of the Electron platform - * @returns Platform capabilities object - */ - getCapabilities(): PlatformCapabilities { - return { - hasFileSystem: false, // Not implemented yet - hasCamera: false, // Not implemented yet - isMobile: false, - isIOS: false, - hasFileDownload: false, // Not implemented yet - needsFileHandlingInstructions: false, - }; - } - - /** - * Reads a file from the filesystem. - * @param _path - Path to the file to read - * @returns Promise that should resolve to file contents - * @throws Error with "Not implemented" message - * @todo Implement file reading using Electron's file system API - */ - async readFile(_path: string): Promise { - throw new Error("Not implemented"); - } +// Create Promise-based versions of fs functions +const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, { encoding }, (err: NodeJS.ErrnoException | null, data: string) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; - /** - * Writes content to a file. - * @param _path - Path where to write the file - * @param _content - Content to write to the file - * @throws Error with "Not implemented" message - * @todo Implement file writing using Electron's file system API - */ - async writeFile(_path: string, _content: string): Promise { - throw new Error("Not implemented"); - } +const readFileBufferAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => { + if (err) reject(err); + else resolve(data); + }); + }); +}; - /** - * Deletes a file from the filesystem. - * @param _path - Path to the file to delete - * @throws Error with "Not implemented" message - * @todo Implement file deletion using Electron's file system API - */ - async deleteFile(_path: string): Promise { - throw new Error("Not implemented"); - } +const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise => { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, data, { encoding }, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; - /** - * Lists files in the specified directory. - * @param _directory - Path to the directory to list - * @returns Promise that should resolve to array of filenames - * @throws Error with "Not implemented" message - * @todo Implement directory listing using Electron's file system API - */ - async listFiles(_directory: string): Promise { - throw new Error("Not implemented"); - } +const unlinkAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => { + if (err) reject(err); + else resolve(); + }); + }); +}; - /** - * Should open system camera to take a picture. - * @returns Promise that should resolve to captured image data - * @throws Error with "Not implemented" message - * @todo Implement camera access using Electron's media APIs - */ - async takePicture(): Promise { - logger.error("takePicture not implemented in Electron platform"); - throw new Error("Not implemented"); - } +const readdirAsync = (dirPath: string): Promise => { + return new Promise((resolve, reject) => { + fs.readdir(dirPath, (err: NodeJS.ErrnoException | null, files: string[]) => { + if (err) reject(err); + else resolve(files); + }); + }); +}; - /** - * Should open system file picker for selecting an image. - * @returns Promise that should resolve to selected image data - * @throws Error with "Not implemented" message - * @todo Implement file picker using Electron's dialog API - */ - async pickImage(): Promise { - logger.error("pickImage not implemented in Electron platform"); - throw new Error("Not implemented"); - } +const statAsync = (filePath: string): Promise => { + return new Promise((resolve, reject) => { + fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { + if (err) reject(err); + else resolve(stats); + }); + }); +}; - /** - * Should handle deep link URLs for the desktop application. - * @param _url - The deep link URL to handle - * @throws Error with "Not implemented" message - * @todo Implement deep link handling using Electron's protocol handler - */ - async handleDeepLink(_url: string): Promise { - logger.error("handleDeepLink not implemented in Electron platform"); - throw new Error("Not implemented"); - } - - // Account Management - async getAccounts(): Promise { - return await db.accounts.toArray(); - } - - async getAccount(did: string): Promise { - return await db.accounts.where("did").equals(did).first(); - } +interface SQLiteDatabase extends Database { + changes: number; +} - async addAccount(account: Account): Promise { - await db.accounts.add(account); - } +/** + * SQLite implementation for Electron using native sqlite3 + */ +class ElectronSQLiteService extends BaseSQLiteService { + private db: SQLiteDatabase | null = null; + private config: SQLiteConfig | null = null; - // Settings Management - async updateMasterSettings( - settingsChanges: Partial, - ): Promise { - throw new Error("Not implemented"); - } + // ... rest of the ElectronSQLiteService implementation ... +} - async getActiveAccountSettings(): Promise { - throw new Error("Not implemented"); - } +export class ElectronPlatformService implements PlatformService { + private sqliteService: ElectronSQLiteService | null = null; - async updateAccountSettings( - accountDid: string, - settingsChanges: Partial, - ): Promise { - throw new Error("Not implemented"); - } -} + // ... rest of the ElectronPlatformService implementation ... +} \ No newline at end of file diff --git a/src/services/platforms/PyWebViewPlatformService.ts b/src/services/platforms/PyWebViewPlatformService.ts index a0d940be..4e23c52c 100644 --- a/src/services/platforms/PyWebViewPlatformService.ts +++ b/src/services/platforms/PyWebViewPlatformService.ts @@ -7,6 +7,7 @@ import { logger } from "../../utils/logger"; import { Account } from "../../db/tables/accounts"; import { Settings } from "../../db/tables/settings"; import { db } from "../../db"; +import { Contact } from "../../db/tables/contacts"; /** * Platform service implementation for PyWebView platform. @@ -143,4 +144,13 @@ export class PyWebViewPlatformService implements PlatformService { ): Promise { throw new Error("Not implemented"); } + + // Contact Management + async getContacts(): Promise { + return await db.contacts.toArray(); + } + + async getAllContacts(): Promise { + return await this.getContacts(); + } } diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index be8fafb9..6a3296f1 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -2,12 +2,20 @@ import { ImageResult, PlatformService, PlatformCapabilities, + SQLiteOperations, } from "../PlatformService"; import { Settings } from "../../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../../db/tables/settings"; import { db } from "../../db"; import { logger } from "../../utils/logger"; import { Account } from "../../db/tables/accounts"; +import { Contact } from "../../db/tables/contacts"; +import { WebSQLiteService } from "../sqlite/WebSQLiteService"; +import { accountsDBPromise } from "../../db"; +import { accessToken } from "../../libs/crypto"; +import { getPlanFromCache as getPlanFromCacheImpl } from "../../libs/endorserServer"; +import { PlanSummaryRecord } from "../../interfaces/records"; +import { Axios } from "axios"; /** * Platform service implementation for web browser platform. @@ -23,6 +31,8 @@ import { Account } from "../../db/tables/accounts"; * due to browser security restrictions. These methods throw appropriate errors. */ export class WebPlatformService implements PlatformService { + private sqliteService: WebSQLiteService | null = null; + /** * Gets the capabilities of the web platform * @returns Platform capabilities object @@ -30,11 +40,17 @@ export class WebPlatformService implements PlatformService { getCapabilities(): PlatformCapabilities { return { hasFileSystem: false, - hasCamera: true, // Through file input with capture - isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), - isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent), + hasCamera: true, + isMobile: false, + isIOS: false, hasFileDownload: true, needsFileHandlingInstructions: false, + sqlite: { + supported: true, + runsInWorker: true, + hasSharedArrayBuffer: typeof SharedArrayBuffer !== "undefined", + supportsWAL: true, + }, }; } @@ -416,14 +432,86 @@ export class WebPlatformService implements PlatformService { // Account Management async getAccounts(): Promise { - return await db.accounts.toArray(); + const accountsDB = await accountsDBPromise; + return await accountsDB.accounts.toArray(); } async getAccount(did: string): Promise { - return await db.accounts.where("did").equals(did).first(); + const accountsDB = await accountsDBPromise; + return await accountsDB.accounts.where("did").equals(did).first(); } async addAccount(account: Account): Promise { - await db.accounts.add(account); + const accountsDB = await accountsDBPromise; + await accountsDB.accounts.add(account); + } + + // Contact Management + async getContacts(): Promise { + return await db.contacts.toArray(); + } + + async getAllContacts(): Promise { + return await this.getContacts(); + } + + async getHeaders(did?: string): Promise> { + const headers: Record = { + "Content-Type": "application/json", + }; + + if (did) { + try { + const account = await this.getAccount(did); + if (account?.passkeyCredIdHex) { + // Handle passkey authentication + const token = await this.getPasskeyToken(did); + headers["Authorization"] = `Bearer ${token}`; + } else { + // Handle regular authentication + const token = await this.getAccessToken(did); + headers["Authorization"] = `Bearer ${token}`; + } + } catch (error) { + logger.error("Failed to get headers:", error); + } + } + + return headers; + } + + private async getPasskeyToken(did: string): Promise { + // For now, use the same token mechanism as regular auth + // TODO: Implement proper passkey authentication + return this.getAccessToken(did); + } + + private async getAccessToken(did: string): Promise { + try { + const token = await accessToken(did); + if (!token) { + throw new Error("Failed to generate access token"); + } + return token; + } catch (error) { + logger.error("Error getting access token:", error); + throw new Error("Failed to get access token: " + (error instanceof Error ? error.message : String(error))); + } + } + + async getSQLite(): Promise { + if (!this.sqliteService) { + this.sqliteService = new WebSQLiteService(); + } + return this.sqliteService; + } + + async getPlanFromCache( + handleId: string | undefined, + axios: Axios, + apiServer: string, + requesterDid?: string, + ): Promise { + return getPlanFromCacheImpl(handleId, axios, apiServer, requesterDid); } } diff --git a/src/services/sqlite/WebSQLiteService.ts b/src/services/sqlite/WebSQLiteService.ts new file mode 100644 index 00000000..af024e90 --- /dev/null +++ b/src/services/sqlite/WebSQLiteService.ts @@ -0,0 +1,170 @@ +import { BaseSQLiteService } from "./BaseSQLiteService"; +import { SQLiteConfig, SQLiteOperations, SQLiteResult, PreparedStatement, SQLiteStats } from "../PlatformService"; +import { logger } from "../../utils/logger"; +import initSqlJs, { Database } from "@jlongster/sql.js"; +import { SQLiteFS } from "absurd-sql"; +import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend"; + +/** + * SQLite implementation for web platform using absurd-sql + */ +export class WebSQLiteService extends BaseSQLiteService { + private db: Database | null = null; + private config: SQLiteConfig | null = null; + private worker: Worker | null = null; + + async initialize(config: SQLiteConfig): Promise { + if (this.initialized) { + return; + } + + try { + this.config = config; + + // Initialize SQL.js + const SQL = await initSqlJs({ + locateFile: (file) => `/sql-wasm.wasm`, + }); + + // Create a worker for SQLite operations + this.worker = new Worker("/sql-worker.js"); + + // Initialize SQLiteFS with IndexedDB backend + const backend = new IndexedDBBackend(); + const fs = new SQLiteFS(backend, this.worker); + + // Create database file + const dbPath = `/${config.name}.db`; + if (!(await fs.exists(dbPath))) { + await fs.writeFile(dbPath, new Uint8Array(0)); + } + + // Open database + this.db = new SQL.Database(dbPath, { filename: true }); + + // Configure database settings + if (config.useWAL) { + await this.execute("PRAGMA journal_mode = WAL"); + this.stats.walMode = true; + } + + // Set other pragmas for performance + await this.execute("PRAGMA synchronous = NORMAL"); + await this.execute("PRAGMA temp_store = MEMORY"); + await this.execute("PRAGMA cache_size = -2000"); // Use 2MB of cache + + this.initialized = true; + await this.updateStats(); + } catch (error) { + logger.error("Failed to initialize Web SQLite:", error); + throw error; + } + } + + protected async _executeQuery( + sql: string, + params: unknown[] = [], + operation: "query" | "execute" = "query", + ): Promise> { + if (!this.db) { + throw new Error("Database not initialized"); + } + + try { + if (operation === "query") { + const stmt = this.db.prepare(sql); + const results = stmt.get(params) as T[]; + stmt.free(); + return { results }; + } else { + const stmt = this.db.prepare(sql); + stmt.run(params); + const changes = this.db.getRowsModified(); + stmt.free(); + return { changes }; + } + } catch (error) { + logger.error("SQLite query failed:", { + sql, + params, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + } + + async close(): Promise { + if (this.db) { + this.db.close(); + this.db = null; + } + if (this.worker) { + this.worker.terminate(); + this.worker = null; + } + this.initialized = false; + } + + async getDatabaseSize(): Promise { + if (!this.db) { + throw new Error("Database not initialized"); + } + const result = await this.query<{ size: number }>("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"); + return result.results[0]?.size || 0; + } + + async prepare(sql: string): Promise> { + if (!this.db) { + throw new Error("Database not initialized"); + } + + const stmt = this.db.prepare(sql); + const key = sql; + + const preparedStmt: PreparedStatement = { + execute: async (params: unknown[] = []) => { + try { + const results = stmt.get(params) as T[]; + return { results }; + } catch (error) { + logger.error("Prepared statement execution failed:", { + sql, + params, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, + finalize: () => { + stmt.free(); + this.preparedStatements.delete(key); + this.stats.preparedStatements--; + }, + }; + + this.preparedStatements.set(key, preparedStmt); + this.stats.preparedStatements++; + + return preparedStmt; + } + + async getStats(): Promise { + await this.updateStats(); + return this.stats; + } + + private async updateStats(): Promise { + if (!this.db) { + throw new Error("Database not initialized"); + } + + const size = await this.getDatabaseSize(); + this.stats.databaseSize = size; + + const walResult = await this.query<{ journal_mode: string }>("PRAGMA journal_mode"); + this.stats.walMode = walResult.results[0]?.journal_mode === "wal"; + + const mmapResult = await this.query<{ mmap_size: number }>("PRAGMA mmap_size"); + this.stats.mmapActive = mmapResult.results[0]?.mmap_size > 0; + } +} \ No newline at end of file diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 7cc28bc5..6b956641 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -333,6 +333,7 @@ import * as serverUtil from "../libs/endorserServer"; import { GiveRecordWithContactInfo } from "../types"; import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue"; import ImageViewer from "../components/ImageViewer.vue"; +import * as InfiniteScrollModule from "../components/InfiniteScroll.vue"; interface Claim { claim?: Claim; // For nested claims in Verifiable Credentials @@ -415,6 +416,7 @@ interface FeedError { OnboardingDialog: OnboardingDialogModule.default, ChoiceButtonDialog, ImageViewer, + InfiniteScroll: InfiniteScrollModule.default, }, }) export default class HomeView extends Vue { diff --git a/vite.config.common.mts b/vite.config.common.mts index d875f569..c1503632 100644 --- a/vite.config.common.mts +++ b/vite.config.common.mts @@ -4,6 +4,7 @@ import dotenv from "dotenv"; import { loadAppConfig } from "./vite.config.utils.mts"; import path from "path"; import { fileURLToPath } from 'url'; +import { nodePolyfills } from 'vite-plugin-node-polyfills'; // Load environment variables dotenv.config(); @@ -26,7 +27,19 @@ export async function createBuildConfig(mode: string) { return { base: isElectron || isPyWebView ? "./" : "./", - plugins: [vue()], + plugins: [ + vue(), + // Add Node.js polyfills for Electron environment + isElectron ? nodePolyfills({ + include: ['util', 'stream', 'buffer', 'events', 'assert', 'crypto'], + globals: { + Buffer: true, + global: true, + process: true, + }, + protocolImports: true, + }) : null, + ].filter(Boolean), server: { port: parseInt(process.env.VITE_PORT || "8080"), fs: { strict: false }, @@ -35,12 +48,51 @@ export async function createBuildConfig(mode: string) { outDir: isElectron ? "dist-electron" : "dist", assetsDir: 'assets', chunkSizeWarningLimit: 1000, + target: isElectron ? 'node18' : 'esnext', rollupOptions: { external: isCapacitor ? ['@capacitor/app'] - : [], + : isElectron + ? [ + 'sqlite3', + 'sqlite', + 'electron', + 'fs', + 'path', + 'crypto', + 'util', + 'stream', + 'buffer', + 'events', + 'assert', + 'constants', + 'os', + 'net', + 'tls', + 'dns', + 'http', + 'https', + 'zlib', + 'url', + 'querystring', + 'punycode', + 'string_decoder', + 'timers', + 'domain', + 'dgram', + 'child_process', + 'cluster', + 'module', + 'vm', + 'readline', + 'repl', + 'tty', + 'v8', + 'worker_threads' + ] + : [], output: { - format: 'es', + format: isElectron ? 'cjs' : 'es', generatedCode: { preset: 'es2015' }