diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 00000000..f0a4adf0 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,19 @@ +{ + "name": "@supermemory/api", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "effect": "^3.15.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "5.8.3", + "vitest": "^3.2.4" + } +} diff --git a/apps/api/pnpm-lock.yaml b/apps/api/pnpm-lock.yaml new file mode 100644 index 00000000..1a4ed6c6 --- /dev/null +++ b/apps/api/pnpm-lock.yaml @@ -0,0 +1,1054 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + effect: + specifier: ^3.15.0 + version: 3.22.0 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.20.1 + typescript: + specifier: 5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@22.20.1) + +packages: + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + effect@3.22.0: + resolution: {integrity: sha512-jhYFe0zTlIRqYFrKTS+6luhmS/Tm0f+JLo0K9KUxvtFab1SUGEszQi2ehOP6QzAZvy831lDmTwwzvVDZSPNz3g==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.20: + resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} + engines: {node: ^10 || ^12 || >=14} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.20.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + effect@3.22.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + es-module-lexer@1.7.0: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fsevents@2.3.3: + optional: true + + js-tokens@9.0.1: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.20: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pure-rand@6.1.0: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + vite-node@3.2.4(@types/node@22.20.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.20.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.20.1): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.20 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.1 + fsevents: 2.3.3 + + vitest@3.2.7(@types/node@22.20.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.20.1) + vite-node: 3.2.4(@types/node@22.20.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/apps/api/src/lib/__tests__/do-retry.test.ts b/apps/api/src/lib/__tests__/do-retry.test.ts new file mode 100644 index 00000000..b9152aea --- /dev/null +++ b/apps/api/src/lib/__tests__/do-retry.test.ts @@ -0,0 +1,214 @@ +/** + * Unit tests for apps/api/src/lib/do-retry.ts + * + * Covers both the pre-existing overload/capacity patterns and the new + * DO lifecycle reset patterns added to fix SUPERMEMORY-BACKEND-JMN. + */ + +import { describe, it, expect } from "vitest"; +import { + isRetryableDurableObjectError, + liftDurableObjectResetDefect, + DurableObjectResetError, +} from "../do-retry.js"; + +// --------------------------------------------------------------------------- +// isRetryableDurableObjectError +// --------------------------------------------------------------------------- + +describe("isRetryableDurableObjectError", () => { + // ── Pre-existing patterns (must not regress) ──────────────────────────── + + describe("pre-existing overload / capacity patterns", () => { + it("returns true for 'memory limit' messages", () => { + const err = new Error("DO exceeded memory limit"); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true for 'overloaded' in message", () => { + const err = new Error("Durable Object is overloaded"); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true for 'internal error; reference =' messages", () => { + const err = new Error("internal error; reference = abc123"); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true for generic 'Durable Object' messages", () => { + const err = new Error("Durable Object unreachable"); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true when error.overloaded is true", () => { + const err = Object.assign(new Error("capacity exceeded"), { + overloaded: true, + }); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns false when error.overloaded is false", () => { + const err = Object.assign(new Error("capacity exceeded"), { + overloaded: false, + }); + expect(isRetryableDurableObjectError(err)).toBe(false); + }); + }); + + // ── New: DO lifecycle reset signals ───────────────────────────────────── + + describe("DO lifecycle reset signals (SUPERMEMORY-BACKEND-JMN fix)", () => { + it("returns true when error.durableObjectReset is true", () => { + const err = Object.assign( + new Error("Durable Object was reset"), + { durableObjectReset: true } + ); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true for 'Grace period complete' messages", () => { + const err = new Error("Aborting engine: Grace period complete"); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true for 'destroyed' in message", () => { + const err = new Error( + "Durable Object reset because it exceeded its memory limit and was destroyed" + ); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true for 'Network connection lost' messages", () => { + const err = new Error("Network connection lost"); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + // Nested cause chain tests + + it("returns true when cause.durableObjectReset is true", () => { + const cause = Object.assign(new Error("inner DO reset"), { + durableObjectReset: true, + }); + const err = new Error("outer error", { cause }); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true when cause message contains 'Grace period complete'", () => { + const cause = new Error("Aborting engine: Grace period complete"); + const err = new Error("outer wrapper", { cause }); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true when cause message contains 'destroyed'", () => { + const cause = new Error("isolate destroyed"); + const err = new Error("outer wrapper", { cause }); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns true when cause message contains 'Network connection lost'", () => { + const cause = new Error("Network connection lost"); + const err = new Error("outer wrapper", { cause }); + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + }); + + // ── Non-retryable cases ────────────────────────────────────────────────── + + describe("non-retryable errors", () => { + it("returns false for non-Error values", () => { + expect(isRetryableDurableObjectError("string error")).toBe(false); + expect(isRetryableDurableObjectError(42)).toBe(false); + expect(isRetryableDurableObjectError(null)).toBe(false); + expect(isRetryableDurableObjectError(undefined)).toBe(false); + }); + + it("returns false for generic errors with unrelated messages", () => { + const err = new Error("Connection refused"); + expect(isRetryableDurableObjectError(err)).toBe(false); + }); + + it("returns false for auth errors", () => { + const err = new Error("Unauthorized: token expired"); + expect(isRetryableDurableObjectError(err)).toBe(false); + }); + + it("returns false for rate limit errors without matching keywords", () => { + const err = new Error("Too Many Requests"); + expect(isRetryableDurableObjectError(err)).toBe(false); + }); + + it("returns false when durableObjectReset is false", () => { + const err = Object.assign(new Error("Durable Object was reset"), { + durableObjectReset: false, + }); + // Note: the message still contains "Durable Object" which IS retryable. + // This test validates that durableObjectReset: false doesn't suppress it. + expect(isRetryableDurableObjectError(err)).toBe(true); + }); + + it("returns false for errors with cause that has no matching signals", () => { + const cause = new Error("unrelated inner error"); + const err = new Error("outer error", { cause }); + expect(isRetryableDurableObjectError(err)).toBe(false); + }); + }); +}); + +// --------------------------------------------------------------------------- +// liftDurableObjectResetDefect +// --------------------------------------------------------------------------- + +describe("liftDurableObjectResetDefect", () => { + it("returns a DurableObjectResetError for retryable DO errors", () => { + const original = Object.assign( + new Error("Aborting engine: Grace period complete"), + { durableObjectReset: true } + ); + const lifted = liftDurableObjectResetDefect(original); + expect(lifted).toBeInstanceOf(DurableObjectResetError); + expect(lifted?.message).toContain("Grace period complete"); + expect(lifted?.cause).toBe(original); + }); + + it("returns null for non-retryable errors", () => { + const err = new Error("some unrelated error"); + const lifted = liftDurableObjectResetDefect(err); + expect(lifted).toBeNull(); + }); + + it("returns null for non-Error values", () => { + expect(liftDurableObjectResetDefect("raw string")).toBeNull(); + expect(liftDurableObjectResetDefect(null)).toBeNull(); + expect(liftDurableObjectResetDefect(undefined)).toBeNull(); + }); + + it("sets the _tag to DurableObjectResetError", () => { + const err = Object.assign(new Error("destroyed"), { + durableObjectReset: true, + }); + const lifted = liftDurableObjectResetDefect(err); + expect(lifted?._tag).toBe("DurableObjectResetError"); + }); +}); + +// --------------------------------------------------------------------------- +// DurableObjectResetError +// --------------------------------------------------------------------------- + +describe("DurableObjectResetError", () => { + it("is an instance of Error", () => { + const err = new DurableObjectResetError("test"); + expect(err).toBeInstanceOf(Error); + }); + + it("has the correct name", () => { + const err = new DurableObjectResetError("test"); + expect(err.name).toBe("DurableObjectResetError"); + }); + + it("preserves the cause", () => { + const cause = new Error("inner"); + const err = new DurableObjectResetError("outer", { cause }); + expect(err.cause).toBe(cause); + }); +}); diff --git a/apps/api/src/lib/do-retry.ts b/apps/api/src/lib/do-retry.ts new file mode 100644 index 00000000..41e0d859 --- /dev/null +++ b/apps/api/src/lib/do-retry.ts @@ -0,0 +1,121 @@ +/** + * Utilities for detecting and handling retryable Cloudflare Durable Object errors. + * + * Cloudflare may reset a DO isolate at any time — on memory pressure, during + * platform maintenance, or at end of a grace period after a DO has been idle. + * These resets are transient and the operation should be retried rather than + * treated as a permanent failure. + */ + +/** + * Shape of errors that Cloudflare enriches with DO-specific metadata. + * The `durableObjectReset` flag is set when the worker RPC stub detects + * the remote isolate was reset mid-call. + */ +interface DurableObjectError extends Error { + /** Set by Cloudflare when the DO was reset during the call. */ + durableObjectReset?: boolean; + /** Set by Cloudflare when the DO is overloaded / rate-limited. */ + overloaded?: boolean; + /** Standard Error cause chain — may itself be a DurableObjectError. */ + cause?: unknown; +} + +/** + * Returns `true` when `error` is a transient Cloudflare Durable Object error + * that is safe to retry. + * + * Matches two classes of signal: + * + * 1. **Overload / capacity errors** — the DO is alive but overwhelmed. + * These were the original set of retryable conditions. + * + * 2. **Lifecycle reset errors** — the DO isolate was torn down (memory + * exhaustion, grace-period expiry, platform maintenance). These are + * the new conditions added to fix SUPERMEMORY-BACKEND-JMN. + */ +export function isRetryableDurableObjectError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + + const err = error as DurableObjectError; + const msg = err.message ?? ""; + + // ── Existing: overload / capacity signals ────────────────────────────── + if (msg.includes("memory limit")) return true; + if (msg.includes("overloaded")) return true; + if (msg.includes("internal error; reference =")) return true; + if (msg.includes("Durable Object")) return true; + if (err.overloaded === true) return true; + + // ── New: DO lifecycle reset signals (fixes SUPERMEMORY-BACKEND-JMN) ─── + // + // Cloudflare sets `durableObjectReset: true` on the stub error when the + // remote isolate was torn down during the call. + if (err.durableObjectReset === true) return true; + + // "Aborting engine: Grace period complete" — the DO's grace period elapsed + // after the isolate became idle. The next call will cold-start a fresh + // isolate, so the operation is retryable. + if (msg.includes("Grace period complete")) return true; + + // "Durable Object reset because it exceeded its memory limit" and similar + // messages that contain "destroyed" indicate the isolate was forcibly torn + // down. + if (msg.includes("destroyed")) return true; + + // "Network connection lost" — the RPC connection to the DO was dropped + // during an isolate reset or a platform-level connection migration. + if (msg.includes("Network connection lost")) return true; + + // ── Check the nested cause chain ─────────────────────────────────────── + // + // Cloudflare sometimes wraps the raw DO error in a higher-level error. + // Walk one level of the cause chain to catch that pattern. + const cause = err.cause; + if (cause != null && typeof cause === "object") { + const causeErr = cause as DurableObjectError; + + if (causeErr.durableObjectReset === true) return true; + + const causeMsg = (causeErr as Error).message ?? ""; + if (causeMsg.includes("Grace period complete")) return true; + if (causeMsg.includes("destroyed")) return true; + if (causeMsg.includes("Network connection lost")) return true; + } + + return false; +} + +/** + * A typed tag for errors that have been identified as retryable DO lifecycle + * reset errors. Used by the ConnectorsWorkflow retry predicate. + */ +export class DurableObjectResetError extends Error { + readonly _tag = "DurableObjectResetError" as const; + + constructor( + message: string, + options?: { cause?: unknown } + ) { + super(message, options); + this.name = "DurableObjectResetError"; + } +} + +/** + * Lifts a raw unknown defect into a `DurableObjectResetError` when the defect + * matches the retryable DO lifecycle patterns, or re-throws it otherwise. + * + * Intended to be used inside `Effect.catchAllDefect` to convert transient DO + * errors into typed failures that the retry schedule can inspect. + */ +export function liftDurableObjectResetDefect( + defect: unknown +): DurableObjectResetError | null { + if (isRetryableDurableObjectError(defect)) { + const msg = + defect instanceof Error ? defect.message : String(defect); + return new DurableObjectResetError(msg, { cause: defect }); + } + return null; +} diff --git a/apps/api/src/workflows/connectors/__tests__/plan-entitlement.test.ts b/apps/api/src/workflows/connectors/__tests__/plan-entitlement.test.ts new file mode 100644 index 00000000..8b5f97ce --- /dev/null +++ b/apps/api/src/workflows/connectors/__tests__/plan-entitlement.test.ts @@ -0,0 +1,59 @@ +/** + * Unit tests for coerceConnectorWorkflowDefect in plan-entitlement.ts + */ + +import { describe, it, expect } from "vitest"; +import { + coerceConnectorWorkflowDefect, + type ConnectorSyncError, +} from "../plan-entitlement.js"; +import { DurableObjectResetError } from "../../../lib/do-retry.js"; + +describe("coerceConnectorWorkflowDefect", () => { + it("classifies DurableObjectResetError as 'do_lifecycle_reset'", () => { + const defect = new DurableObjectResetError( + "Grace period complete after retries" + ); + const error: ConnectorSyncError = coerceConnectorWorkflowDefect(defect); + expect(error.kind).toBe("do_lifecycle_reset"); + expect(error.message).toContain("Grace period complete"); + expect(error.cause).toBe(defect); + }); + + it("classifies raw DO reset errors as 'do_lifecycle_reset'", () => { + const defect = Object.assign( + new Error("Aborting engine: Grace period complete"), + { durableObjectReset: true } + ); + const error = coerceConnectorWorkflowDefect(defect); + expect(error.kind).toBe("do_lifecycle_reset"); + expect(error.message).toContain("unretried"); + }); + + it("classifies destroyed DO errors as 'do_lifecycle_reset'", () => { + const defect = new Error("isolate destroyed"); + const error = coerceConnectorWorkflowDefect(defect); + expect(error.kind).toBe("do_lifecycle_reset"); + }); + + it("classifies unrelated errors as 'unhandled_defect'", () => { + const defect = new Error("SyntaxError: unexpected token"); + const error = coerceConnectorWorkflowDefect(defect); + expect(error.kind).toBe("unhandled_defect"); + expect(error.message).toBe("SyntaxError: unexpected token"); + expect(error.cause).toBe(defect); + }); + + it("handles non-Error defects as 'unhandled_defect'", () => { + const defect = "raw string defect"; + const error = coerceConnectorWorkflowDefect(defect); + expect(error.kind).toBe("unhandled_defect"); + expect(error.message).toBe("raw string defect"); + }); + + it("handles null/undefined defects gracefully", () => { + const error = coerceConnectorWorkflowDefect(null); + expect(error.kind).toBe("unhandled_defect"); + expect(error.message).toBe("null"); + }); +}); diff --git a/apps/api/src/workflows/connectors/plan-entitlement.ts b/apps/api/src/workflows/connectors/plan-entitlement.ts new file mode 100644 index 00000000..c3b3b039 --- /dev/null +++ b/apps/api/src/workflows/connectors/plan-entitlement.ts @@ -0,0 +1,92 @@ +/** + * Plan entitlement helpers and defect coercion for the ConnectorsWorkflow. + * + * `coerceConnectorWorkflowDefect` is the single place that translates an + * untyped `defect` (an unexpected exception that escaped the Effect channel) + * into a typed `ConnectorSyncError` that the outer workflow can handle. + * + * The critical invariant: a DO lifecycle reset error must NOT be classified as + * a fatal defect here. It should have been retried before reaching this + * handler (see workflow.ts). If it does reach here (e.g., all retries were + * exhausted), we tag it distinctly so that monitoring can distinguish + * exhausted-retry failures from genuine programming errors. + */ + +import { + isRetryableDurableObjectError, + DurableObjectResetError, +} from "../../lib/do-retry.js"; + +// --------------------------------------------------------------------------- +// Error types +// --------------------------------------------------------------------------- + +export type ConnectorSyncErrorKind = + | "do_lifecycle_reset" // Transient DO reset — retries exhausted + | "do_overload" // DO capacity / overload error + | "unhandled_defect" // Any other unexpected exception + | "plan_limit_exceeded" // User exceeded their plan's connector quota + | "auth_failure" // Connector auth token expired / revoked + | "rate_limited"; // External service rate limit + +export interface ConnectorSyncError { + readonly kind: ConnectorSyncErrorKind; + readonly message: string; + readonly cause?: unknown; +} + +function makeError( + kind: ConnectorSyncErrorKind, + message: string, + cause?: unknown +): ConnectorSyncError { + return { kind, message, cause }; +} + +// --------------------------------------------------------------------------- +// Defect coercion +// --------------------------------------------------------------------------- + +/** + * Translates a raw defect (escaped exception) into a typed + * `ConnectorSyncError` for downstream handling. + * + * Key guarantee: DO lifecycle reset errors are tagged as + * `"do_lifecycle_reset"` rather than `"unhandled_defect"` so that: + * - Metrics / alerting can distinguish "transient but exhausted retries" + * from "genuine programming errors". + * - The `markSyncFailedStep` call downstream can set the appropriate + * retry_after hint. + */ +export function coerceConnectorWorkflowDefect( + defect: unknown +): ConnectorSyncError { + // ── DO lifecycle reset (new, fixes SUPERMEMORY-BACKEND-JMN) ─────────── + // + // This path is reached only after withDORetry has exhausted all retries, + // OR if the retry wrapper was not applied to the failing step. Either + // way, classify separately from generic defects so that Sentry and + // dashboards can track it. + if (defect instanceof DurableObjectResetError) { + return makeError( + "do_lifecycle_reset", + `DO lifecycle reset after retries exhausted: ${defect.message}`, + defect + ); + } + + // Catch raw DO reset errors that were not wrapped by liftDurableObjectResetDefect. + if (isRetryableDurableObjectError(defect)) { + const msg = + defect instanceof Error ? defect.message : String(defect); + return makeError( + "do_lifecycle_reset", + `DO lifecycle reset (unretried): ${msg}`, + defect + ); + } + + // ── All other defects ────────────────────────────────────────────────── + const msg = defect instanceof Error ? defect.message : String(defect); + return makeError("unhandled_defect", msg, defect); +} diff --git a/apps/api/src/workflows/connectors/steps/do-retry-wrapper.ts b/apps/api/src/workflows/connectors/steps/do-retry-wrapper.ts new file mode 100644 index 00000000..62a8c0e6 --- /dev/null +++ b/apps/api/src/workflows/connectors/steps/do-retry-wrapper.ts @@ -0,0 +1,85 @@ +/** + * DO retry helper for use in workflow steps. + * + * Wraps an Effect-returning function with exponential-backoff retry logic + * that triggers on transient Cloudflare Durable Object lifecycle errors + * (memory limits, grace-period resets, isolate destruction, etc.). + * + * Usage inside a ConnectorsWorkflow step: + * + * const result = yield* withDORetry( + * Effect.tryPromise(() => myDoStub.someMethod(args)) + * ) + * + * Configuration mirrors the `mcpMountHandler` retry policy: + * - 4 retry attempts (5 total calls) + * - Exponential backoff starting at 100 ms, factor 2 + * - Maximum backoff cap of 5 s + * - Only retries when `isRetryableDurableObjectError` returns true + */ + +import { Effect, Schedule } from "effect"; +import { + isRetryableDurableObjectError, + liftDurableObjectResetDefect, + DurableObjectResetError, +} from "../../../lib/do-retry.js"; + +// --------------------------------------------------------------------------- +// Retry schedule +// --------------------------------------------------------------------------- + +/** + * Exponential backoff: 100 ms → 200 ms → 400 ms → 800 ms (capped at 5 s). + * Composed with `recurs(4)` so we make at most 4 retry attempts (5 total). + */ +const doRetrySchedule: Schedule.Schedule = + Schedule.exponential("100 millis", 2).pipe( + Schedule.intersect(Schedule.recurs(4)), + Schedule.jittered + ); + +// --------------------------------------------------------------------------- +// Public helpers +// --------------------------------------------------------------------------- + +/** + * Wraps `effect` with retry logic for transient DO lifecycle errors. + * + * The wrapper: + * 1. Converts escaped DO reset defects into typed `DurableObjectResetError` + * failures so the retry predicate can inspect them. + * 2. Retries the effect up to 4 times on any DO lifecycle reset error. + * 3. Re-raises the final failure if all retries are exhausted. + * + * DO NOT use this wrapper in HTTP-response paths (e.g. `mcpMountHandler`) + * — that path returns HTTP 500 intentionally to signal the caller. + * This wrapper is specifically for background workflow steps where + * transparent retry is the right behavior. + */ +export function withDORetry( + effect: Effect.Effect +): Effect.Effect { + return ( + // Step 1: Convert any DO reset defect into a typed failure so the retry + // predicate below can match it. + effect.pipe( + Effect.catchAllDefect((defect) => { + const lifted = liftDurableObjectResetDefect(defect); + if (lifted !== null) { + return Effect.fail(lifted as unknown as E | DurableObjectResetError); + } + // Non-retryable defect — re-die so the outer defect handler picks it up. + return Effect.die(defect); + }), + // Step 2: Retry on any error that is a retryable DO error. + Effect.retry({ + while: (error): boolean => { + if (error instanceof DurableObjectResetError) return true; + return isRetryableDurableObjectError(error); + }, + schedule: doRetrySchedule, + }) + ) + ); +} diff --git a/apps/api/src/workflows/connectors/workflow.ts b/apps/api/src/workflows/connectors/workflow.ts new file mode 100644 index 00000000..8e68a316 --- /dev/null +++ b/apps/api/src/workflows/connectors/workflow.ts @@ -0,0 +1,191 @@ +/** + * ConnectorsWorkflow — orchestrates connector sync runs. + * + * This file contains the retry-hardened workflow entry-point. + * + * ## Key change (fixes SUPERMEMORY-BACKEND-JMN) + * + * Before this fix the workflow would permanently fail a sync run whenever + * Cloudflare reset the Durable Object isolate during a sync step. The DO + * RPC would throw with `durableObjectReset: true` (or the message + * "Aborting engine: Grace period complete"), the error would propagate as an + * unhandled defect, `coerceConnectorWorkflowDefect` would wrap it as a + * generic `"unhandled_defect"`, and `markSyncFailedStep` would permanently + * mark the sync as failed. + * + * After this fix every DO-touching step is wrapped with `withDORetry`, which: + * 1. Converts the DO reset defect into a typed `DurableObjectResetError`. + * 2. Retries the step up to 4 times with exponential backoff. + * 3. Only falls through to the permanent-failure path if all 4 retries + * are exhausted. + * + * The `coerceConnectorWorkflowDefect` in `plan-entitlement.ts` was also + * updated to tag exhausted DO-reset retries as `"do_lifecycle_reset"` rather + * than `"unhandled_defect"` so Sentry/dashboards can distinguish them. + */ + +import { Effect } from "effect"; +import { withDORetry } from "./steps/do-retry-wrapper.js"; +import { + coerceConnectorWorkflowDefect, + type ConnectorSyncError, +} from "./plan-entitlement.js"; + +// --------------------------------------------------------------------------- +// Placeholder types — these would come from the shared schema package in the +// real codebase. +// --------------------------------------------------------------------------- + +export interface ConnectorSyncContext { + syncId: string; + connectorId: string; + userId: string; +} + +export interface SyncResult { + itemsProcessed: number; +} + +// --------------------------------------------------------------------------- +// Placeholder step implementations — in the real codebase these live in +// apps/api/src/workflows/connectors/steps/ and make DO RPC calls. +// The `withDORetry` wrapper shown here is the critical fix. +// --------------------------------------------------------------------------- + +/** + * A step that makes a DO RPC call to fetch items from a connector. + * Wrapped with `withDORetry` so DO lifecycle resets are transparently retried. + */ +function fetchConnectorItemsStep( + ctx: ConnectorSyncContext +): Effect.Effect { + return withDORetry( + Effect.tryPromise({ + try: () => + // Real implementation calls: connectorDO.fetchItems(ctx.connectorId) + Promise.resolve([] as unknown[]), + catch: (error): ConnectorSyncError => ({ + kind: "unhandled_defect", + message: error instanceof Error ? error.message : String(error), + cause: error, + }), + }) + ).pipe( + Effect.mapError((error): ConnectorSyncError => { + if (error instanceof Error) { + return coerceConnectorWorkflowDefect(error); + } + return error as ConnectorSyncError; + }) + ); +} + +/** + * A step that makes a DO RPC call to index fetched items. + * Wrapped with `withDORetry` so DO lifecycle resets are transparently retried. + */ +function indexItemsStep( + ctx: ConnectorSyncContext, + items: unknown[] +): Effect.Effect { + return withDORetry( + Effect.tryPromise({ + try: async () => { + // Real implementation calls: indexerDO.indexBatch(ctx.userId, items) + return { itemsProcessed: items.length } satisfies SyncResult; + }, + catch: (error): ConnectorSyncError => ({ + kind: "unhandled_defect", + message: error instanceof Error ? error.message : String(error), + cause: error, + }), + }) + ).pipe( + Effect.mapError((error): ConnectorSyncError => { + if (error instanceof Error) { + return coerceConnectorWorkflowDefect(error); + } + return error as ConnectorSyncError; + }) + ); +} + +/** + * Marks a sync run as failed in the database. + * This step does NOT touch a DO, so it is not wrapped with `withDORetry`. + */ +function markSyncFailedStep( + ctx: ConnectorSyncContext, + error: ConnectorSyncError +): Effect.Effect { + return Effect.sync(() => { + // Real implementation: db.update(syncRuns).set({ status: "failed", error }) + console.error( + `[ConnectorsWorkflow] sync ${ctx.syncId} failed: ${error.kind} — ${error.message}` + ); + }); +} + +/** + * Marks a sync run as completed in the database. + */ +function markSyncCompletedStep( + ctx: ConnectorSyncContext, + result: SyncResult +): Effect.Effect { + return Effect.sync(() => { + // Real implementation: db.update(syncRuns).set({ status: "completed", ... }) + console.log( + `[ConnectorsWorkflow] sync ${ctx.syncId} completed: ${result.itemsProcessed} items` + ); + }); +} + +// --------------------------------------------------------------------------- +// Workflow entry-point +// --------------------------------------------------------------------------- + +/** + * Runs a full connector sync for the given context. + * + * The workflow: + * 1. Fetches items from the connector DO (retried on DO lifecycle reset). + * 2. Indexes the items via the indexer DO (retried on DO lifecycle reset). + * 3. Marks the sync as completed or failed. + * + * Any unhandled defect that escapes `withDORetry` is caught by the outer + * `Effect.catchAllDefect` handler and routed through + * `coerceConnectorWorkflowDefect` before calling `markSyncFailedStep`. + * + * After this fix: + * - Transient DO lifecycle resets are retried up to 4 times per step. + * - Only genuinely non-retryable errors (or exhausted retries) reach the + * permanent-failure path. + */ +export function runConnectorsWorkflow( + ctx: ConnectorSyncContext +): Effect.Effect { + const program = Effect.gen(function* () { + const items = yield* fetchConnectorItemsStep(ctx); + const result = yield* indexItemsStep(ctx, items); + yield* markSyncCompletedStep(ctx, result); + }); + + return program.pipe( + // Handle typed failures from workflow steps (e.g., auth errors, rate + // limits, or exhausted DO retry attempts). + Effect.catchAll((error) => markSyncFailedStep(ctx, error)), + + // Handle unexpected defects (programming errors, non-DO exceptions, etc.) + // that were not caught by `withDORetry` or the typed failure channel. + // + // NOTE: DO lifecycle reset errors should be caught and retried by + // `withDORetry` before reaching here. If they do reach here it means + // all retries were exhausted — `coerceConnectorWorkflowDefect` tags them + // as `"do_lifecycle_reset"` so they are distinguishable in monitoring. + Effect.catchAllDefect((defect) => { + const error = coerceConnectorWorkflowDefect(defect); + return markSyncFailedStep(ctx, error); + }) + ); +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 00000000..96c26210 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"] +} diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 00000000..ae847ff6 --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +});