diff --git a/nuxt.config.ts b/nuxt.config.ts index 3f0991e..c0a8fee 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -14,6 +14,8 @@ export default defineNuxtConfig({ githubAppId: '', githubAppPrivateKey: '', githubWebhookSecret: '', + cronSecret: '', + workerBudgetMs: '', }, typescript: { nodeTsConfig: { @@ -31,6 +33,18 @@ export default defineNuxtConfig({ experimental: { typedPages: true, }, + nitro: { + vercel: { + config: { + crons: [ + { + path: '/api/jobs/run', + schedule: '* * * * *' + }, + ] + }, + } + }, compatibilityDate: '2024-04-03', // Ensure that any HTML validation errors are treated as build errors htmlValidator: { diff --git a/package.json b/package.json index c860cef..fe29f81 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "vue-router": "^5.0.6" }, "devDependencies": { + "@electric-sql/pglite": "^0.4.5", "@nuxt/test-utils": "4.0.3", "@octokit/webhooks-types": "^7.6.1", "@playwright/test": "1.59.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cb7183..52a132a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,13 +17,13 @@ importers: version: 1.1.0 '@nuxt/fonts': specifier: ^0.14.0 - version: 0.14.0(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) + version: 0.14.0(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) '@nuxt/image': specifier: ^2.0.0 - version: 2.0.0(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(srvx@0.11.15) + version: 2.0.0(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(srvx@0.11.15) '@nuxt/scripts': specifier: ^1.0.6 - version: 1.0.6(@unhead/vue@2.1.13(vue@3.5.33(typescript@6.0.3)))(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3)) + version: 1.0.6(@unhead/vue@2.1.13(vue@3.5.33(typescript@6.0.3)))(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3)) '@nuxtjs/html-validator': specifier: ^2.1.0 version: 2.1.0(@voidzero-dev/vite-plus-test@0.1.20)(magicast@0.5.2) @@ -32,13 +32,13 @@ importers: version: 6.0.0 drizzle-orm: specifier: ^0.45.2 - version: 0.45.2(@neondatabase/serverless@1.1.0) + version: 0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0) nuxt: specifier: ^4.4.4 - version: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) + version: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@electric-sql/pglite@0.4.5)(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) nuxt-og-image: specifier: ^6.4.11 - version: 6.4.11(ea4a24142c8db751653c3d93ffc3a1c4) + version: 6.4.11(73738ea48627a6be1d31778aaae04f8e) rolldown: specifier: ^1.0.0-rc.18 version: 1.0.0-rc.18 @@ -52,6 +52,9 @@ importers: specifier: ^5.0.6 version: 5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@6.0.3)) devDependencies: + '@electric-sql/pglite': + specifier: ^0.4.5 + version: 0.4.5 '@nuxt/test-utils': specifier: 4.0.3 version: 4.0.3(@playwright/test@1.59.1)(@voidzero-dev/vite-plus-test@0.1.20)(@vue/test-utils@2.4.10(@vue/compiler-dom@3.5.33)(@vue/server-renderer@3.5.33(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3)))(crossws@0.4.5(srvx@0.11.15))(happy-dom@20.9.0)(magicast@0.5.2)(playwright-core@1.59.1)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) @@ -275,6 +278,9 @@ packages: '@dxup/unimport@0.1.2': resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==} + '@electric-sql/pglite@0.4.5': + resolution: {integrity: sha512-aGG2zGEyZzGWKy8P+9ZoNUV0jxt1+hgbeTf+bVAYyxVZZLXg3/9aFlfLxb08AYZVAfAkQlQIysmWjhc5hwDG8g==} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -5870,6 +5876,8 @@ snapshots: '@dxup/unimport@0.1.2': {} + '@electric-sql/pglite@0.4.5': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -6543,13 +6551,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/fonts@0.14.0(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))': + '@nuxt/fonts@0.14.0(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))': dependencies: '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) '@nuxt/kit': 4.4.4(magicast@0.5.2) consola: 3.4.2 defu: 6.1.7 - fontless: 0.2.1(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) + fontless: 0.2.1(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) h3: 1.15.11 magic-regexp: 0.10.0 ofetch: 1.5.1 @@ -6559,7 +6567,7 @@ snapshots: ufo: 1.6.4 unifont: 0.7.4 unplugin: 3.0.0 - unstorage: 1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6583,7 +6591,7 @@ snapshots: - uploadthing - vite - '@nuxt/image@2.0.0(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(srvx@0.11.15)': + '@nuxt/image@2.0.0(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(srvx@0.11.15)': dependencies: '@nuxt/kit': 4.4.4(magicast@0.5.2) consola: 3.4.2 @@ -6596,7 +6604,7 @@ snapshots: std-env: 3.10.0 ufo: 1.6.4 optionalDependencies: - ipx: 3.1.1(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(srvx@0.11.15) + ipx: 3.1.1(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(srvx@0.11.15) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6671,7 +6679,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.4.4(@babel/core@7.29.0)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(ioredis@5.10.1)(magicast@0.5.2)(nuxt@4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4))(oxc-parser@0.128.0)(rolldown@1.0.0-rc.18)(srvx@0.11.15)(typescript@6.0.3)': + '@nuxt/nitro-server@4.4.4(c703237518f96a01201357fd255c3b67)': dependencies: '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@nuxt/devalue': 2.0.2 @@ -6689,8 +6697,8 @@ snapshots: impound: 1.1.5 klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.13.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(oxc-parser@0.128.0)(rolldown@1.0.0-rc.18)(srvx@0.11.15) - nuxt: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) + nitropack: 2.13.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0))(oxc-parser@0.128.0)(rolldown@1.0.0-rc.18)(srvx@0.11.15) + nuxt: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@electric-sql/pglite@0.4.5)(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) nypm: 0.6.6 ohash: 2.0.11 pathe: 2.0.3 @@ -6698,7 +6706,7 @@ snapshots: std-env: 4.1.0 ufo: 1.6.4 unctx: 2.5.0 - unstorage: 1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) vue: 3.5.33(typescript@6.0.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 @@ -6749,7 +6757,7 @@ snapshots: pkg-types: 2.3.1 std-env: 4.1.0 - '@nuxt/scripts@1.0.6(@unhead/vue@2.1.13(vue@3.5.33(typescript@6.0.3)))(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3))': + '@nuxt/scripts@1.0.6(@unhead/vue@2.1.13(vue@3.5.33(typescript@6.0.3)))(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(magicast@0.5.2)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3))': dependencies: '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) '@nuxt/kit': 4.4.4(magicast@0.5.2) @@ -6771,7 +6779,7 @@ snapshots: ufo: 1.6.4 ultrahtml: 1.6.0 unplugin: 3.0.0 - unstorage: 1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) valibot: 1.3.1(typescript@6.0.3) transitivePeerDependencies: - '@azure/app-configuration' @@ -6850,7 +6858,7 @@ snapshots: - typescript - vite - '@nuxt/vite-builder@4.4.4(28264294504fc5bb34607a4fbd2f89d6)': + '@nuxt/vite-builder@4.4.4(60540df5947dbcc3d30c3e97abb55c90)': dependencies: '@nuxt/kit': 4.4.4(magicast@0.5.2) '@rollup/plugin-replace': 6.0.3(rollup@4.60.2) @@ -6868,7 +6876,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) + nuxt: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@electric-sql/pglite@0.4.5)(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) nypm: 0.6.6 pathe: 2.0.3 pkg-types: 2.3.1 @@ -8403,9 +8411,10 @@ snapshots: culori@4.0.2: {} - db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)): + db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)): optionalDependencies: - drizzle-orm: 0.45.2(@neondatabase/serverless@1.1.0) + '@electric-sql/pglite': 0.4.5 + drizzle-orm: 0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0) debug@4.4.3: dependencies: @@ -8469,8 +8478,9 @@ snapshots: esbuild: 0.25.12 tsx: 4.21.0 - drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0): + drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0): optionalDependencies: + '@electric-sql/pglite': 0.4.5 '@neondatabase/serverless': 1.1.0 duplexer@0.1.2: {} @@ -8822,7 +8832,7 @@ snapshots: dependencies: tiny-inflate: 1.0.3 - fontless@0.2.1(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)): + fontless@0.2.1(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)): dependencies: consola: 3.4.2 css-tree: 3.2.1 @@ -8836,7 +8846,7 @@ snapshots: pathe: 2.0.3 ufo: 1.6.4 unifont: 0.7.4 - unstorage: 1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) optionalDependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) transitivePeerDependencies: @@ -9054,7 +9064,7 @@ snapshots: transitivePeerDependencies: - supports-color - ipx@3.1.1(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(srvx@0.11.15): + ipx@3.1.1(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(srvx@0.11.15): dependencies: '@fastify/accept-negotiator': 2.0.1 citty: 0.1.6 @@ -9070,7 +9080,7 @@ snapshots: sharp: 0.34.5 svgo: 4.0.1 ufo: 1.6.4 - unstorage: 1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) xss: 1.0.15 transitivePeerDependencies: - '@azure/app-configuration' @@ -9432,7 +9442,7 @@ snapshots: natural-compare@1.4.0: {} - nitropack@2.13.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(oxc-parser@0.128.0)(rolldown@1.0.0-rc.18)(srvx@0.11.15): + nitropack@2.13.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0))(oxc-parser@0.128.0)(rolldown@1.0.0-rc.18)(srvx@0.11.15): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.60.2) @@ -9453,7 +9463,7 @@ snapshots: cookie-es: 2.0.1 croner: 10.0.1 crossws: 0.3.5 - db0: 0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)) + db0: 0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)) defu: 6.1.7 destr: 2.0.5 dot-prop: 10.1.0 @@ -9499,7 +9509,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 6.2.0(oxc-parser@0.128.0) unplugin-utils: 0.3.1 - unstorage: 1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) untyped: 2.0.0 unwasm: 0.5.3 youch: 4.1.1 @@ -9576,7 +9586,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt-og-image@6.4.11(ea4a24142c8db751653c3d93ffc3a1c4): + nuxt-og-image@6.4.11(73738ea48627a6be1d31778aaae04f8e): dependencies: '@clack/prompts': 1.3.0 '@nuxt/kit': 4.4.4(magicast@0.5.2) @@ -9592,8 +9602,8 @@ snapshots: magic-string: 0.30.21 magicast: 0.5.2 mocked-exports: 0.1.1 - nuxt-site-config: 4.0.8(@nuxt/schema@4.4.4)(magicast@0.5.2)(nuxt@4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3)) - nuxtseo-shared: 5.1.3(524f84012e9d72beb906744b155b32ec) + nuxt-site-config: 4.0.8(63185d4d07eff04c3532e23607595514) + nuxtseo-shared: 5.1.3(722a2202af61d2736787650a24b9e3d3) nypm: 0.6.6 ofetch: 1.5.1 ohash: 2.0.11 @@ -9609,11 +9619,11 @@ snapshots: ufo: 1.6.4 ultrahtml: 1.6.0 unplugin: 3.0.0 - unstorage: 1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1) optionalDependencies: '@resvg/resvg-js': 2.6.2 '@resvg/resvg-wasm': 2.6.2 - fontless: 0.2.1(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) + fontless: 0.2.1(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) playwright-core: 1.59.1 sharp: 0.34.5 unifont: 0.7.4 @@ -9635,12 +9645,12 @@ snapshots: - magicast - vue - nuxt-site-config@4.0.8(@nuxt/schema@4.4.4)(magicast@0.5.2)(nuxt@4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3)): + nuxt-site-config@4.0.8(63185d4d07eff04c3532e23607595514): dependencies: '@nuxt/kit': 4.4.4(magicast@0.5.2) h3: 1.15.11 nuxt-site-config-kit: 4.0.8(magicast@0.5.2)(vue@3.5.33(typescript@6.0.3)) - nuxtseo-shared: 5.1.3(524f84012e9d72beb906744b155b32ec) + nuxtseo-shared: 5.1.3(722a2202af61d2736787650a24b9e3d3) pathe: 2.0.3 pkg-types: 2.3.1 site-config-stack: 4.0.8(vue@3.5.33(typescript@6.0.3)) @@ -9653,16 +9663,16 @@ snapshots: - vue - zod - nuxt@4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4): + nuxt@4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@electric-sql/pglite@0.4.5)(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4): dependencies: '@dxup/nuxt': 0.4.1(magicast@0.5.2)(typescript@6.0.3) '@nuxt/cli': 3.35.1(@nuxt/schema@4.4.4)(cac@6.7.14)(magicast@0.5.2) '@nuxt/devtools': 3.2.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3)) '@nuxt/kit': 4.4.4(magicast@0.5.2) - '@nuxt/nitro-server': 4.4.4(@babel/core@7.29.0)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(ioredis@5.10.1)(magicast@0.5.2)(nuxt@4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4))(oxc-parser@0.128.0)(rolldown@1.0.0-rc.18)(srvx@0.11.15)(typescript@6.0.3) + '@nuxt/nitro-server': 4.4.4(c703237518f96a01201357fd255c3b67) '@nuxt/schema': 4.4.4 '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.4.4(magicast@0.5.2)) - '@nuxt/vite-builder': 4.4.4(28264294504fc5bb34607a4fbd2f89d6) + '@nuxt/vite-builder': 4.4.4(60540df5947dbcc3d30c3e97abb55c90) '@unhead/vue': 2.1.13(vue@3.5.33(typescript@6.0.3)) '@vue/shared': 3.5.33 chokidar: 5.0.0 @@ -9788,7 +9798,7 @@ snapshots: - xml2js - yaml - nuxtseo-shared@5.1.3(524f84012e9d72beb906744b155b32ec): + nuxtseo-shared@5.1.3(722a2202af61d2736787650a24b9e3d3): dependencies: '@clack/prompts': 1.3.0 '@nuxt/devtools-kit': 4.0.0-alpha.3(magicast@0.5.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) @@ -9797,7 +9807,7 @@ snapshots: birpc: 4.0.0 consola: 3.4.2 defu: 6.1.7 - nuxt: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) + nuxt: 4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@electric-sql/pglite@0.4.5)(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4) ofetch: 1.5.1 pathe: 2.0.3 pkg-types: 2.3.1 @@ -9807,7 +9817,7 @@ snapshots: ufo: 1.6.4 vue: 3.5.33(typescript@6.0.3) optionalDependencies: - nuxt-site-config: 4.0.8(@nuxt/schema@4.4.4)(magicast@0.5.2)(nuxt@4.4.4(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0))(esbuild@0.28.0)(eslint@10.3.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(oxlint@1.61.0(oxlint-tsgolint@0.22.0))(rolldown@1.0.0-rc.18)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.18)(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(tsx@4.21.0)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.4))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3)) + nuxt-site-config: 4.0.8(63185d4d07eff04c3532e23607595514) transitivePeerDependencies: - magicast - vite @@ -10775,7 +10785,7 @@ snapshots: escape-string-regexp: 5.0.0 ufo: 1.6.4 - unstorage@1.17.5(db0@0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1): + unstorage@1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)))(ioredis@5.10.1): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -10786,7 +10796,7 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.4 optionalDependencies: - db0: 0.3.4(drizzle-orm@0.45.2(@neondatabase/serverless@1.1.0)) + db0: 0.3.4(@electric-sql/pglite@0.4.5)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.5)(@neondatabase/serverless@1.1.0)) ioredis: 5.10.1 untun@0.1.3: diff --git a/server/api/github/webhook.post.ts b/server/api/github/webhook.post.ts index e0e7845..65031ef 100644 --- a/server/api/github/webhook.post.ts +++ b/server/api/github/webhook.post.ts @@ -1,7 +1,15 @@ -import type { InstallationEvent } from '@octokit/webhooks-types' +import type { + CreateEvent, + DeleteEvent, + InstallationEvent, + InstallationRepositoriesEvent, + PushEvent, + RepositoryEvent, +} from '@octokit/webhooks-types' import { verify } from '@octokit/webhooks-methods' import { sql } from 'drizzle-orm' import { installation, webhookEvent } from '~~/server/db/schema' +import { enqueue } from '~~/server/utils/queue' const RECOGNISED_EVENTS = new Set([ 'push', @@ -95,10 +103,70 @@ export default defineEventHandler(async event => { } } - // TODO(commit 7): enqueue a job for recognised event types. + // Enqueue work for events we care about. Envelope shape is the minimum the + // handler needs to re-fetch fresh data via the GitHub API; we never persist + // the raw webhook body. See PLAN.md "Deferred / follow-ups". if (RECOGNISED_EVENTS.has(eventName)) { - // Will become: await enqueue({ kind: eventName, payload: }) + await enqueueForEvent(event, eventName, deliveryId) } return { ok: true, deliveryId } }) + +async function enqueueForEvent(event: Parameters[0], eventName: string, deliveryId: string) { + if (eventName === 'push') { + const body = await readBody(event) + if (!body.installation) return + await enqueue('github.push', { + deliveryId, + installationId: body.installation.id, + githubRepoId: body.repository.id, + ref: body.ref, + before: body.before, + after: body.after, + }) + } + else if (eventName === 'create') { + const body = await readBody(event) + if (!body.installation) return + await enqueue('github.create', { + deliveryId, + installationId: body.installation.id, + githubRepoId: body.repository.id, + refType: body.ref_type, + ref: body.ref, + }) + } + else if (eventName === 'delete') { + const body = await readBody(event) + if (!body.installation) return + await enqueue('github.delete', { + deliveryId, + installationId: body.installation.id, + githubRepoId: body.repository.id, + refType: body.ref_type, + ref: body.ref, + }) + } + else if (eventName === 'repository') { + const body = await readBody(event) + if (!body.installation) return + await enqueue('github.repository', { + deliveryId, + installationId: body.installation.id, + githubRepoId: body.repository.id, + action: body.action, + }) + } + else if (eventName === 'installation_repositories') { + const body = await readBody(event) + await enqueue('github.installation_repositories', { + deliveryId, + installationId: body.installation.id, + action: body.action, + addedRepoIds: 'repositories_added' in body ? body.repositories_added.map(r => r.id) : [], + removedRepoIds: 'repositories_removed' in body ? body.repositories_removed.map(r => r.id) : [], + }) + } + // 'installation' is handled inline above for bookkeeping; no job enqueued. +} diff --git a/server/api/jobs/run.post.ts b/server/api/jobs/run.post.ts new file mode 100644 index 0000000..ddfbffd --- /dev/null +++ b/server/api/jobs/run.post.ts @@ -0,0 +1,54 @@ +import crypto from 'node:crypto' +import { dispatch } from '~~/server/utils/job-handlers' +import { claim, complete, fail } from '~~/server/utils/queue' + +const LEASE_MS = 5 * 60_000 // 5 min — generous for a sync job +const DEFAULT_BUDGET_MS = 25_000 // leave headroom under Vercel's 10s default; pro tiers can override + +export default defineEventHandler(async event => { + const config = useRuntimeConfig() + const cronSecret = config.cronSecret + if (!cronSecret) { + throw createError({ statusCode: 500, statusMessage: 'cron secret not configured' }) + } + + const auth = getRequestHeader(event, 'authorization') + if (auth !== `Bearer ${cronSecret}`) { + throw createError({ statusCode: 401, statusMessage: 'unauthorized' }) + } + + const workerId = `${process.env.VERCEL_DEPLOYMENT_ID ?? 'local'}:${crypto.randomUUID()}` + const budgetMs = Number(config.workerBudgetMs) || DEFAULT_BUDGET_MS + const deadline = Date.now() + budgetMs + + let processed = 0 + let drained = false + + // Sequential by design: each iteration claims one job, runs it, records the + // outcome. We don't parallelise because each Vercel invocation is a single + // small worker; concurrency comes from cron firing multiple invocations. + // eslint-disable-next-line no-await-in-loop + while (Date.now() < deadline) { + // eslint-disable-next-line no-await-in-loop + const job = await claim(workerId, LEASE_MS) + if (!job) { + drained = true + break + } + + try { + // eslint-disable-next-line no-await-in-loop + await dispatch(job) + // eslint-disable-next-line no-await-in-loop + await complete(job.id) + } + catch (err) { + // eslint-disable-next-line no-await-in-loop + await fail(job.id, job.attempts, err) + } + + processed++ + } + + return { ok: true, processed, drained, workerId } +}) diff --git a/server/utils/db.ts b/server/utils/db.ts index d685d2a..c8a8187 100644 --- a/server/utils/db.ts +++ b/server/utils/db.ts @@ -1,17 +1,32 @@ import { neon } from '@neondatabase/serverless' -import { drizzle } from 'drizzle-orm/neon-http' +import { drizzle as drizzleNeon } from 'drizzle-orm/neon-http' import * as schema from '../db/schema' -let _db: ReturnType> | undefined +export type Db = ReturnType> -export function useDb() { +let _db: Db | undefined + +/** + * Override the DB returned by `useDb()`. Tests inject a PGlite-backed Drizzle + * instance with the same schema. Production code never calls this. + */ +export function setDb(db: Db) { + _db = db +} + +/** Clear the cached DB so the next `useDb()` reconstructs from runtime config. */ +export function clearDb() { + _db = undefined +} + +export function useDb(): Db { if (_db) return _db const { databaseUrl } = useRuntimeConfig() if (!databaseUrl) { throw new Error('NUXT_DATABASE_URL is not set') } const client = neon(databaseUrl) - _db = drizzle(client, { schema }) + _db = drizzleNeon(client, { schema }) return _db } diff --git a/server/utils/job-handlers.ts b/server/utils/job-handlers.ts new file mode 100644 index 0000000..eef28f3 --- /dev/null +++ b/server/utils/job-handlers.ts @@ -0,0 +1,30 @@ +import type { JobEnvelope } from './queue' + +/** + * Map of job kind → handler. Handlers are filled in by later commits: + * - 'github.push' → commit 11 (sync push events) + * - 'github.create' / 'github.delete' → commit 12 (branch/tag ref ops) + * - 'github.repository' → commit 13/14 (description, lifecycle) + * - 'tangled.create-repo' → commit 10 (initial enrolment) + * - 'atproto.publish-pubkey' → commit 9 (publish ssh public key) + * + * For now the dispatcher knows the recognised kinds but routes them all to a + * noop. An unknown kind throws so it surfaces as a job failure rather than + * silent acknowledgement. + */ +const KNOWN_KINDS = new Set([ + 'github.push', + 'github.create', + 'github.delete', + 'github.repository', + 'github.installation_repositories', + 'tangled.create-repo', + 'atproto.publish-pubkey', +]) + +export async function dispatch(envelope: JobEnvelope): Promise { + if (!KNOWN_KINDS.has(envelope.kind)) { + throw new Error(`unknown job kind: ${envelope.kind}`) + } + // No-op until handlers land in later commits. +} diff --git a/server/utils/queue.ts b/server/utils/queue.ts new file mode 100644 index 0000000..69a90e6 --- /dev/null +++ b/server/utils/queue.ts @@ -0,0 +1,113 @@ +import { sql } from 'drizzle-orm' +import { job } from '../db/schema' +import { useDb } from './db' + +export interface JobEnvelope { + id: number + kind: string + payload: unknown + attempts: number +} + +export interface EnqueueOptions { + /** Run no earlier than this time. Default: now. */ + runAfter?: Date +} + +/** + * Push a job onto the queue. `payload` must be a JSON-serialisable object — keep + * it small (an envelope of identifiers, not a webhook body); see PLAN.md. + */ +export async function enqueue(kind: string, payload: object, opts: EnqueueOptions = {}) { + const db = useDb() + const [row] = await db.insert(job).values({ + kind, + payload, + runAfter: opts.runAfter ?? new Date(), + }).returning({ id: job.id }) + return row +} + +/** + * Atomically claim the oldest runnable job. Uses + * `UPDATE ... WHERE id = (SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1)` + * so multiple workers don't race for the same row. + * + * Returns null when the queue is empty (or all rows are locked / not yet due). + */ +export async function claim(workerId: string, leaseMs: number): Promise { + const db = useDb() + const leaseUntil = new Date(Date.now() + leaseMs) + + // Two conditions for a job to be claimable: + // 1. status='queued' AND run_after <= now() + // 2. status='running' AND locked_until < now() (lease expired) + const result = await db.execute(sql` + UPDATE ${job} + SET + status = 'running', + locked_by = ${workerId}, + locked_until = ${leaseUntil.toISOString()}, + attempts = ${job.attempts} + 1, + updated_at = now() + WHERE ${job.id} = ( + SELECT ${job.id} FROM ${job} + WHERE + (${job.status} = 'queued' AND ${job.runAfter} <= now()) + OR + (${job.status} = 'running' AND ${job.lockedUntil} < now()) + ORDER BY ${job.runAfter} ASC + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) + RETURNING id, kind, payload, attempts + `) + + // drizzle's neon-http execute returns rows on `.rows`; pglite's returns directly. + // Normalise. + const rows = (Array.isArray(result) ? result : (result as { rows?: unknown[] }).rows) ?? [] + if (rows.length === 0) return null + return rows[0] as JobEnvelope +} + +/** Mark a job as completed. */ +export async function complete(id: number) { + const db = useDb() + await db.update(job) + .set({ status: 'done', lockedBy: null, lockedUntil: null, lastError: null, updatedAt: new Date() }) + .where(sql`${job.id} = ${id}`) +} + +/** + * Record a failure. Re-queues with exponential backoff until `maxAttempts`, + * after which the job is marked `failed` and stays put for inspection. + */ +export async function fail(id: number, attempts: number, err: unknown, maxAttempts = 5) { + const db = useDb() + const message = err instanceof Error ? err.message : String(err) + + if (attempts >= maxAttempts) { + await db.update(job) + .set({ status: 'failed', lastError: message, lockedBy: null, lockedUntil: null, updatedAt: new Date() }) + .where(sql`${job.id} = ${id}`) + return + } + + // Exponential backoff with ±20% jitter: base*2^attempts, capped. + const base = 30_000 // 30s + const cap = 60 * 60_000 // 1h + const backoff = Math.min(base * (2 ** (attempts - 1)), cap) + const jitter = backoff * (0.8 + Math.random() * 0.4) + const runAfter = new Date(Date.now() + jitter) + + await db.update(job) + .set({ + status: 'queued', + lockedBy: null, + lockedUntil: null, + runAfter, + lastError: message, + updatedAt: new Date(), + }) + .where(sql`${job.id} = ${id}`) +} diff --git a/test/unit/queue.spec.ts b/test/unit/queue.spec.ts new file mode 100644 index 0000000..6772a92 --- /dev/null +++ b/test/unit/queue.spec.ts @@ -0,0 +1,98 @@ +import { sql } from 'drizzle-orm' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { job } from '../../server/db/schema' +import { clearDb, setDb, useDb } from '../../server/utils/db' +import { claim, complete, enqueue, fail } from '../../server/utils/queue' +import { createTestDb } from '../utils/db' + +describe('queue', () => { + beforeEach(async () => { + setDb(await createTestDb()) + }) + + afterEach(() => { + clearDb() + }) + + it('enqueues and claims a job', async () => { + const queued = await enqueue('github.push', { foo: 'bar' }) + expect(queued.id).toBeGreaterThan(0) + + const claimed = await claim('worker-1', 60_000) + expect(claimed?.kind).toBe('github.push') + expect(claimed?.payload).toEqual({ foo: 'bar' }) + expect(claimed?.attempts).toBe(1) + }) + + it('returns null when the queue is empty', async () => { + const claimed = await claim('worker-1', 60_000) + expect(claimed).toBeNull() + }) + + it('does not claim the same job twice', async () => { + await enqueue('github.push', { foo: 'bar' }) + + const a = await claim('worker-1', 60_000) + const b = await claim('worker-2', 60_000) + + expect(a).not.toBeNull() + expect(b).toBeNull() + }) + + it('respects run_after', async () => { + const future = new Date(Date.now() + 60_000) + await enqueue('github.push', {}, { runAfter: future }) + + const claimed = await claim('worker-1', 60_000) + expect(claimed).toBeNull() + }) + + it('reclaims a job whose lease has expired', async () => { + await enqueue('github.push', {}) + + const first = await claim('worker-1', 60_000) + expect(first).not.toBeNull() + + // Force the lease into the past. + const db = useDb() + await db.execute(sql`UPDATE ${job} SET locked_until = now() - interval '1 minute'`) + + const second = await claim('worker-2', 60_000) + expect(second?.id).toBe(first!.id) + expect(second?.attempts).toBe(2) // attempts increments on each claim + }) + + it('marks a job as done on complete', async () => { + const queued = await enqueue('github.push', {}) + const claimed = await claim('worker-1', 60_000) + await complete(claimed!.id) + + const db = useDb() + const rows = await db.select().from(job).where(sql`${job.id} = ${queued.id}`) + expect(rows[0]?.status).toBe('done') + expect(rows[0]?.lockedBy).toBeNull() + }) + + it('re-queues with backoff on fail before maxAttempts', async () => { + await enqueue('github.push', {}) + const claimed = await claim('worker-1', 60_000) + await fail(claimed!.id, claimed!.attempts, new Error('boom')) + + const db = useDb() + const rows = await db.select().from(job).where(sql`${job.id} = ${claimed!.id}`) + expect(rows[0]?.status).toBe('queued') + expect(rows[0]?.lastError).toBe('boom') + expect(new Date(rows[0]?.runAfter ?? 0).getTime()).toBeGreaterThan(Date.now()) + }) + + it('marks failed once attempts >= maxAttempts', async () => { + await enqueue('github.push', {}) + const claimed = await claim('worker-1', 60_000) + await fail(claimed!.id, 5, new Error('terminal'), 5) + + const db = useDb() + const rows = await db.select().from(job).where(sql`${job.id} = ${claimed!.id}`) + expect(rows[0]?.status).toBe('failed') + expect(rows[0]?.lastError).toBe('terminal') + }) +}) diff --git a/test/utils/db.ts b/test/utils/db.ts new file mode 100644 index 0000000..0b9ee12 --- /dev/null +++ b/test/utils/db.ts @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { PGlite } from '@electric-sql/pglite' +import { drizzle } from 'drizzle-orm/pglite' +import * as schema from '../../server/db/schema' +import type { Db } from '../../server/utils/db' + +const migrationPath = fileURLToPath( + new URL('../../server/db/migrations/0000_initial.sql', import.meta.url), +) + +/** + * Create a fresh in-memory Postgres (PGlite) for a single test, apply our + * schema, and return a Drizzle instance. Each call returns an isolated DB. + */ +export async function createTestDb(): Promise { + const pg = new PGlite() + const sql = readFileSync(migrationPath, 'utf8') + + // The drizzle-generated migration file uses `--> statement-breakpoint` between + // statements; PGlite's exec accepts the whole thing if we strip those markers. + // Sequential by design — DDL ordering matters. + for (const statement of sql.split('--> statement-breakpoint')) { + const trimmed = statement.trim() + // eslint-disable-next-line no-await-in-loop + if (trimmed) await pg.exec(trimmed) + } + + return drizzle(pg, { schema }) as unknown as Db +}