Browse Source

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.
sql-absurd-sql
Matthew Raymer 1 month ago
parent
commit
d9ce884513
  1. 414
      package-lock.json
  2. 1
      package.json
  3. 16
      src/main.capacitor.ts
  4. 20
      src/main.common.ts
  5. 15
      src/main.electron.ts
  6. 15
      src/main.pywebview.ts
  7. 14
      src/main.web.ts
  8. 139
      src/services/CapacitorPlatformService.ts
  9. 283
      src/services/ElectronPlatformService.ts
  10. 40
      src/services/PlatformService.ts
  11. 58
      src/services/PlatformServiceFactory.ts
  12. 43
      src/services/WebPlatformService.ts
  13. 10
      src/services/platforms/CapacitorPlatformService.ts
  14. 211
      src/services/platforms/ElectronPlatformService.ts
  15. 10
      src/services/platforms/PyWebViewPlatformService.ts
  16. 100
      src/services/platforms/WebPlatformService.ts
  17. 170
      src/services/sqlite/WebSQLiteService.ts
  18. 2
      src/views/HomeView.vue
  19. 58
      vite.config.common.mts

414
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",

1
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",

16
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 = `
<div style="color: red; padding: 20px; font-family: sans-serif;">
<h1>Failed to initialize app</h1>
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
<p>Please try restarting the app or contact support if the problem persists.</p>
</div>
`;
});
logger.log("[Capacitor] App mounted");

20
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");

15
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 = `
<div style="color: red; padding: 20px; font-family: sans-serif;">
<h1>Failed to initialize app</h1>
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
<p>Please try restarting the app or contact support if the problem persists.</p>
</div>
`;
});

15
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 = `
<div style="color: red; padding: 20px; font-family: sans-serif;">
<h1>Failed to initialize app</h1>
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
<p>Please try restarting the app or contact support if the problem persists.</p>
</div>
`;
});

14
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 = `
<div style="color: red; padding: 20px; font-family: sans-serif;">
<h1>Failed to initialize app</h1>
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
<p>Please try refreshing the page or contact support if the problem persists.</p>
</div>
`;
});

139
src/services/CapacitorPlatformService.ts

@ -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<SQLiteOperations> {
if (!this.sqliteService) {
this.sqliteService = new CapacitorSQLiteService();
}
return this.sqliteService;
}
async readFile(path: string): Promise<string> {
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<void> {
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<void> {
try {
await Filesystem.deleteFile({
path,
directory: Directory.Data,
});
} catch (error) {
logger.error("Failed to delete file:", error);
throw error;
}
}
async listFiles(directory: string): Promise<string[]> {
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<void> {
// Implement deep link handling for Capacitor platform
logger.info("Handling deep link:", url);
}
}

283
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<string> => {
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<Buffer> => {
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<void> => {
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<void> => {
return new Promise((resolve, reject) => {
fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => {
if (err) reject(err);
else resolve();
});
});
};
const readdirAsync = (dirPath: string): Promise<string[]> => {
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<fs.Stats> => {
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<void> {
@ -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<T>(sql, params);
const rows = await this.db.all<T[]>(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<SQLiteOperations> {
throw new Error("Electron platform service is not available in this environment");
}
async getSQLite(): Promise<SQLiteOperations> {
if (!this.sqliteService) {
this.sqliteService = new ElectronSQLiteService();
}
return this.sqliteService;
}
async readFile(path: string): Promise<string> {
throw new Error("Electron platform service is not available in this environment");
}
async readFile(path: string): Promise<string> {
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<void> {
throw new Error("Electron platform service is not available in this environment");
}
async writeFile(path: string, content: string): Promise<void> {
try {
await fs.writeFile(path, content, "utf-8");
} catch (error) {
logger.error("Failed to write file:", error);
throw error;
}
}
async deleteFile(path: string): Promise<void> {
throw new Error("Electron platform service is not available in this environment");
}
async deleteFile(path: string): Promise<void> {
try {
await fs.unlink(path);
} catch (error) {
logger.error("Failed to delete file:", error);
throw error;
}
}
async listFiles(directory: string): Promise<string[]> {
throw new Error("Electron platform service is not available in this environment");
}
async listFiles(directory: string): Promise<string[]> {
try {
const files = await fs.readdir(directory);
return files;
} catch (error) {
logger.error("Failed to list files:", error);
throw error;
}
}
async takePicture(): Promise<ImageResult> {
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<ImageResult> {
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<void> {
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<Account[]> {
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<Account | undefined> {
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<void> {
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<Contact[]> {
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<Contact[]> {
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<Settings>): Promise<void> {
throw new Error("Electron platform service is not available in this environment");
}
async handleDeepLink(url: string): Promise<void> {
// Implement deep link handling for Electron platform
logger.info("Handling deep link:", url);
async getActiveAccountSettings(): Promise<Settings> {
throw new Error("Electron platform service is not available in this environment");
}
async updateAccountSettings(accountDid: string, settingsChanges: Partial<Settings>): Promise<void> {
throw new Error("Electron platform service is not available in this environment");
}
async getHeaders(did?: string): Promise<Record<string, string>> {
throw new Error("Electron platform service is not available in this environment");
}
async getPlanFromCache(
handleId: string | undefined,
axios: Axios,
apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> {
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 {}

40
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<SQLiteOperations>;
getSQLite(): Promise<SQLiteOperations>;
/**
* 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<Record<string, string>>;
// Account Management
/**
@ -303,4 +313,32 @@ export interface PlatformService {
accountDid: string,
settingsChanges: Partial<Settings>,
): Promise<void>;
// Contact Management
/**
* Gets all contacts from the database
* @returns Promise resolving to array of contacts
*/
getContacts(): Promise<Contact[]>;
/**
* Gets all contacts from the database (alias for getContacts)
* @returns Promise resolving to array of contacts
*/
getAllContacts(): Promise<Contact[]>;
/**
* 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<PlanSummaryRecord | undefined>;
}

58
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<PlatformService>} Promise resolving to the singleton instance of PlatformService
*/
public static getInstance(): PlatformService {
public static async getInstance(): Promise<PlatformService> {
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;
}
}
}

43
src/services/WebPlatformService.ts

@ -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<SQLiteOperations> {
if (!this.sqliteService) {
this.sqliteService = new AbsurdSQLService();
}
return this.sqliteService;
}
// ... existing file system and camera methods ...
async handleDeepLink(url: string): Promise<void> {
// Implement deep link handling for web platform
logger.info("Handling deep link:", url);
}
}

10
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<void> {
throw new Error("Not implemented");
}
// Contact Management
async getContacts(): Promise<Contact[]> {
return await db.contacts.toArray();
}
async getAllContacts(): Promise<Contact[]> {
return await this.getContacts();
}
}

211
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<string> {
throw new Error("Not implemented");
}
// Create Promise-based versions of fs functions
const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise<string> => {
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<void> {
throw new Error("Not implemented");
}
const readFileBufferAsync = (filePath: string): Promise<Buffer> => {
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<void> {
throw new Error("Not implemented");
}
const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise<void> => {
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<string[]> {
throw new Error("Not implemented");
}
const unlinkAsync = (filePath: string): Promise<void> => {
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<ImageResult> {
logger.error("takePicture not implemented in Electron platform");
throw new Error("Not implemented");
}
const readdirAsync = (dirPath: string): Promise<string[]> => {
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<ImageResult> {
logger.error("pickImage not implemented in Electron platform");
throw new Error("Not implemented");
}
const statAsync = (filePath: string): Promise<fs.Stats> => {
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<void> {
logger.error("handleDeepLink not implemented in Electron platform");
throw new Error("Not implemented");
}
// Account Management
async getAccounts(): Promise<Account[]> {
return await db.accounts.toArray();
}
async getAccount(did: string): Promise<Account | undefined> {
return await db.accounts.where("did").equals(did).first();
}
interface SQLiteDatabase extends Database {
changes: number;
}
async addAccount(account: Account): Promise<void> {
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<Settings>,
): Promise<void> {
throw new Error("Not implemented");
}
// ... rest of the ElectronSQLiteService implementation ...
}
async getActiveAccountSettings(): Promise<Settings> {
throw new Error("Not implemented");
}
export class ElectronPlatformService implements PlatformService {
private sqliteService: ElectronSQLiteService | null = null;
async updateAccountSettings(
accountDid: string,
settingsChanges: Partial<Settings>,
): Promise<void> {
throw new Error("Not implemented");
}
}
// ... rest of the ElectronPlatformService implementation ...
}

10
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<void> {
throw new Error("Not implemented");
}
// Contact Management
async getContacts(): Promise<Contact[]> {
return await db.contacts.toArray();
}
async getAllContacts(): Promise<Contact[]> {
return await this.getContacts();
}
}

100
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<Account[]> {
return await db.accounts.toArray();
const accountsDB = await accountsDBPromise;
return await accountsDB.accounts.toArray();
}
async getAccount(did: string): Promise<Account | undefined> {
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<void> {
await db.accounts.add(account);
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add(account);
}
// Contact Management
async getContacts(): Promise<Contact[]> {
return await db.contacts.toArray();
}
async getAllContacts(): Promise<Contact[]> {
return await this.getContacts();
}
async getHeaders(did?: string): Promise<Record<string, string>> {
const headers: Record<string, string> = {
"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<string> {
// 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<string> {
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<SQLiteOperations> {
if (!this.sqliteService) {
this.sqliteService = new WebSQLiteService();
}
return this.sqliteService;
}
async getPlanFromCache(
handleId: string | undefined,
axios: Axios,
apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> {
return getPlanFromCacheImpl(handleId, axios, apiServer, requesterDid);
}
}

170
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<void> {
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<T>(
sql: string,
params: unknown[] = [],
operation: "query" | "execute" = "query",
): Promise<SQLiteResult<T>> {
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<void> {
if (this.db) {
this.db.close();
this.db = null;
}
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
this.initialized = false;
}
async getDatabaseSize(): Promise<number> {
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<T>(sql: string): Promise<PreparedStatement<T>> {
if (!this.db) {
throw new Error("Database not initialized");
}
const stmt = this.db.prepare(sql);
const key = sql;
const preparedStmt: PreparedStatement<T> = {
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<SQLiteStats> {
await this.updateStats();
return this.stats;
}
private async updateStats(): Promise<void> {
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;
}
}

2
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 {

58
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'
}

Loading…
Cancel
Save