diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a1aed2..8156acd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.14.8 - 2026-09-09 + +- Hold the actor instance and message claim locks through every fenced commit + on PostgreSQL and MySQL. Lease recovery and stale-process cleanup revalidate + those rows after waiting, so they cannot requeue a message that completed in + the meantime. + ## 0.14.7 - 2026-09-05 - Read SQL message results and completion status in one statement so concurrent diff --git a/docs/parity.md b/docs/parity.md index 433ff0a..c36a4ca 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -48,7 +48,7 @@ is needed for the JavaScript stale-result race fix. | ------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Actor registry, durable identity, JSON state, and adjacent state migrations | Native | Ordinary classes, static actor types, inferred state, explicit migrations, and isolated runtime context across every actor-instance callback. | | Fluent committed calls and background delivery | Native | `await reference.operation()` and `reference.send.operation()`. | -| Ordered mailbox, sequence allocation, idempotency, retries, dead letters, leases, renewal, and fenced commits | Native | Relational ready/claimed membership tables, distinct generated request IDs and caller idempotency keys, durable history, and adapter-appropriate sequence locking. | +| Ordered mailbox, sequence allocation, idempotency, retries, dead letters, leases, renewal, and fenced commits | Native | Relational ready/claimed membership tables, distinct generated request IDs and caller idempotency keys, durable history, adapter-appropriate sequence locking, and PostgreSQL/MySQL row locks held from fence validation through commit. | | Domain rejection and strict poison ordering | Native | Rejections accept JavaScript identifier-style codes and roll back without retry; invalid codes fail terminally, while retryable failures block later operations until completion or dead-lettering. | | Bounded activation passes and hot-actor fairness | Native | Configurable turn-count and elapsed-time budgets bound each pass, then move only that actor's already-due memberships behind actors already waiting. | | Bounded claim candidate scan | Native | A configurable ordered scan continues to another ready actor when a worker loses the first candidate's lease race. | diff --git a/package.json b/package.json index 819b8c7..63c737a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-objects", - "version": "0.14.7", + "version": "0.14.8", "description": "Race-free realtime state per application identity, backed by your SQL database", "type": "module", "license": "MIT", @@ -136,14 +136,14 @@ "@types/node": "^24.0.0", "@types/pg": "^8.21.0", "@types/ws": "^8.18.1", - "@vitest/coverage-v8": "^4.1.10", + "@vitest/coverage-v8": "^4.1.11", "mysql2": "^3.23.3", "pg": "^8.23.0", "prettier": "^3.9.6", "redis": "^6.2.1", "signal-polyfill": "^0.2.2", "typescript": "^5.9.0", - "vitest": "^4.1.10", + "vitest": "^4.1.11", "wrangler": "^4.129.0", "ws": "^8.21.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d63f715..11e0d4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,13 +4,16 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + sharp@<0.35.4: ^0.35.4 + importers: .: devDependencies: '@cloudflare/vitest-plugin': specifier: ^1.1.4 - version: 1.1.4(@cloudflare/workers-types@5.20260904.1)(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10) + version: 1.1.4(@cloudflare/workers-types@5.20260904.1)(@types/node@24.13.3)(@vitest/runner@4.1.11)(@vitest/snapshot@4.1.11)(vitest@4.1.11) '@cloudflare/workers-types': specifier: ^5.20260904.1 version: 5.20260904.1 @@ -30,8 +33,8 @@ importers: specifier: ^8.18.1 version: 8.18.1 '@vitest/coverage-v8': - specifier: ^4.1.10 - version: 4.1.10(vitest@4.1.10) + specifier: ^4.1.11 + version: 4.1.11(vitest@4.1.11) mysql2: specifier: ^3.23.3 version: 3.23.3(@types/node@24.13.3) @@ -51,11 +54,11 @@ importers: specifier: ^5.9.0 version: 5.9.3 vitest: - specifier: ^4.1.10 - version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) + specifier: ^4.1.11 + version: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) wrangler: specifier: ^4.129.0 - version: 4.129.0(@cloudflare/workers-types@5.20260904.1) + version: 4.129.0(@cloudflare/workers-types@5.20260904.1)(@types/node@24.13.3) ws: specifier: ^8.21.3 version: 8.21.3 @@ -303,160 +306,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.2': - resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.2': - resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.2': - resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.1': - resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.1': - resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.1': - resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.1': - resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.1': - resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.1': - resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.1': - resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.1': - resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.1': - resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.1': - resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.2': - resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.2': - resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.2': - resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.2': - resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.2': - resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.2': - resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.2': - resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.2': - resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.2': - resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.2': - resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.2': - resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.2': - resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.2': - resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -465,8 +468,8 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -652,20 +655,20 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@vitest/coverage-v8@4.1.10': - resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + '@vitest/coverage-v8@4.1.11': + resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} peerDependencies: - '@vitest/browser': 4.1.10 - vitest: 4.1.10 + '@vitest/browser': 4.1.11 + vitest: 4.1.11 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -675,27 +678,27 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-v8-to-istanbul@1.0.5: - resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + ast-v8-to-istanbul@1.0.6: + resolution: {integrity: sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==} aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} @@ -729,8 +732,8 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} @@ -956,8 +959,8 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} playwright-core@1.62.1: @@ -1012,9 +1015,14 @@ packages: engines: {node: '>=10'} hasBin: true - sharp@0.35.2: - resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1051,8 +1059,8 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.3.0: - resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} tinyglobby@0.2.17: @@ -1124,20 +1132,20 @@ packages: yaml: optional: true - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1247,18 +1255,19 @@ snapshots: optionalDependencies: workerd: 1.20260903.1 - '@cloudflare/vitest-plugin@1.1.4(@cloudflare/workers-types@5.20260904.1)(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10)': + '@cloudflare/vitest-plugin@1.1.4(@cloudflare/workers-types@5.20260904.1)(@types/node@24.13.3)(@vitest/runner@4.1.11)(@vitest/snapshot@4.1.11)(vitest@4.1.11)': dependencies: - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 cjs-module-lexer: 1.2.3 esbuild: 0.28.1 - miniflare: 5.20260903.0-alpha - vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) - wrangler: 4.129.0(@cloudflare/workers-types@5.20260904.1) + miniflare: 5.20260903.0-alpha(@types/node@24.13.3) + vitest: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) + wrangler: 4.129.0(@cloudflare/workers-types@5.20260904.1)(@types/node@24.13.3) zod: 4.4.3 transitivePeerDependencies: - '@cloudflare/workers-types' + - '@types/node' - bufferutil - utf-8-validate @@ -1368,123 +1377,123 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.35.2': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.2': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.2': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.2 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.1': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.1': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.1': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.1': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.1': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.1': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.1': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.1': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.1': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.2': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.2': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.2': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.2': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.2': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.2': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.2': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.2': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.2': + '@img/sharp-wasm32@0.35.4': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.2': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.2 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.2': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.2': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.2': + '@img/sharp-win32-x64@0.35.4': optional: true '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@oxc-project/types@0.143.0': {} @@ -1599,11 +1608,11 @@ snapshots: dependencies: '@types/node': 24.13.3 - '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.10 - ast-v8-to-istanbul: 1.0.5 + '@vitest/utils': 4.1.11 + ast-v8-to-istanbul: 1.0.6 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 @@ -1611,52 +1620,52 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) + vitest: 4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) - '@vitest/expect@4.1.10': + '@vitest/expect@4.1.11': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1))': + '@vitest/mocker@4.1.11(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1))': dependencies: - '@vitest/spy': 4.1.10 + '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1) - '@vitest/pretty-format@4.1.10': + '@vitest/pretty-format@4.1.11': dependencies: tinyrainbow: 3.1.1 - '@vitest/runner@4.1.10': + '@vitest/runner@4.1.11': dependencies: - '@vitest/utils': 4.1.10 + '@vitest/utils': 4.1.11 pathe: 2.0.3 - '@vitest/snapshot@4.1.10': + '@vitest/snapshot@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.10': {} + '@vitest/spy@4.1.11': {} - '@vitest/utils@4.1.10': + '@vitest/utils@4.1.11': dependencies: - '@vitest/pretty-format': 4.1.10 + '@vitest/pretty-format': 4.1.11 convert-source-map: 2.0.0 tinyrainbow: 3.1.1 assertion-error@2.0.1: {} - ast-v8-to-istanbul@1.0.5: + ast-v8-to-istanbul@1.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -1680,7 +1689,7 @@ snapshots: error-stack-parser-es@1.0.5: {} - es-module-lexer@2.3.1: {} + es-module-lexer@2.3.2: {} esbuild@0.28.1: optionalDependencies: @@ -1717,9 +1726,9 @@ snapshots: expect-type@1.4.0: {} - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 fsevents@2.3.2: optional: true @@ -1813,7 +1822,7 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 magicast@0.5.4: dependencies: @@ -1825,15 +1834,16 @@ snapshots: dependencies: semver: 7.8.5 - miniflare@5.20260903.0-alpha: + miniflare@5.20260903.0-alpha(@types/node@24.13.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - sharp: 0.35.2 + sharp: 0.35.4(@types/node@24.13.3) undici: 7.29.0 workerd: 1.20260903.1 ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: + - '@types/node' - bufferutil - utf-8-validate @@ -1897,7 +1907,7 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.5: {} + picomatch@4.0.7: {} playwright-core@1.62.1: {} @@ -1960,37 +1970,38 @@ snapshots: semver@7.8.5: {} - sharp@0.35.2: + sharp@0.35.4(@types/node@24.13.3): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.2 - '@img/sharp-darwin-x64': 0.35.2 - '@img/sharp-freebsd-wasm32': 0.35.2 - '@img/sharp-libvips-darwin-arm64': 1.3.1 - '@img/sharp-libvips-darwin-x64': 1.3.1 - '@img/sharp-libvips-linux-arm': 1.3.1 - '@img/sharp-libvips-linux-arm64': 1.3.1 - '@img/sharp-libvips-linux-ppc64': 1.3.1 - '@img/sharp-libvips-linux-riscv64': 1.3.1 - '@img/sharp-libvips-linux-s390x': 1.3.1 - '@img/sharp-libvips-linux-x64': 1.3.1 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 - '@img/sharp-libvips-linuxmusl-x64': 1.3.1 - '@img/sharp-linux-arm': 0.35.2 - '@img/sharp-linux-arm64': 0.35.2 - '@img/sharp-linux-ppc64': 0.35.2 - '@img/sharp-linux-riscv64': 0.35.2 - '@img/sharp-linux-s390x': 0.35.2 - '@img/sharp-linux-x64': 0.35.2 - '@img/sharp-linuxmusl-arm64': 0.35.2 - '@img/sharp-linuxmusl-x64': 0.35.2 - '@img/sharp-webcontainers-wasm32': 0.35.2 - '@img/sharp-win32-arm64': 0.35.2 - '@img/sharp-win32-ia32': 0.35.2 - '@img/sharp-win32-x64': 0.35.2 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 24.13.3 siginfo@2.0.0: {} @@ -2014,12 +2025,12 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.3.0: {} + tinyexec@1.3.1: {} tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tinyrainbow@3.1.1: {} @@ -2039,7 +2050,7 @@ snapshots: vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 + picomatch: 4.0.7 postcss: 8.5.26 rolldown: 1.2.3 tinyglobby: 0.2.17 @@ -2048,31 +2059,31 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 - vitest@4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)): + vitest@4.1.11(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)): dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 expect-type: 1.4.0 magic-string: 0.30.21 obug: 2.1.4 pathe: 2.0.3 - picomatch: 4.0.5 + picomatch: 4.0.7 std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.3.0 + tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 - '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) transitivePeerDependencies: - msw @@ -2089,13 +2100,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260903.1 '@cloudflare/workerd-windows-64': 1.20260903.1 - wrangler@4.129.0(@cloudflare/workers-types@5.20260904.1): + wrangler@4.129.0(@cloudflare/workers-types@5.20260904.1)(@types/node@24.13.3): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260903.1) blake3-wasm: 2.1.5 esbuild: 0.28.1 - miniflare: 5.20260903.0-alpha + miniflare: 5.20260903.0-alpha(@types/node@24.13.3) path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 workerd: 1.20260903.1 @@ -2103,6 +2114,7 @@ snapshots: '@cloudflare/workers-types': 5.20260904.1 fsevents: 2.3.3 transitivePeerDependencies: + - '@types/node' - bufferutil - utf-8-validate diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 02365ef..0f8fd9d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,9 @@ allowBuilds: esbuild: true workerd: true +minimumReleaseAgeExclude: + - vitest@4.1.11 + - "@vitest/mocker@4.1.11" + - sharp@0.35.4 +overrides: + sharp@<0.35.4: ^0.35.4 diff --git a/src/repository.ts b/src/repository.ts index 80cdf52..c66a87b 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -43,6 +43,21 @@ export interface SyncDiagnosticsRecord { nowMilliseconds: number } +interface ActivationFenceRow { + activation_owner_id: string | null + activation_token: string | null + activation_generation: number | bigint + activation_expires_at_ms: number | bigint | null +} + +interface MessageClaimFenceRow { + instance_id: string + sequence: number | bigint + process_id: string + activation_token: string + activation_generation: number | bigint +} + export class Repository { constructor(private readonly settings: RuntimeSettings) {} @@ -165,21 +180,59 @@ export class Repository { const claims = await connection.all<{ message_id: string instance_id: string - sequence: number | bigint }>( - `SELECT message_id, instance_id, sequence + `SELECT message_id, instance_id FROM ${this.table("claimed_messages")} WHERE process_id = ?`, [processId], ) for (const claim of claims) { - await connection.run(`DELETE FROM ${this.table("claimed_messages")} WHERE message_id = ?`, [ - claim.message_id, - ]) + const instance = await this.lockActivationFence(connection, claim.instance_id) + if (!instance) continue + const currentClaim = await this.lockMessageClaim(connection, claim.message_id) + if ( + !currentClaim || + currentClaim.instance_id !== claim.instance_id || + currentClaim.process_id !== processId + ) { + continue + } + const removed = await connection.run( + `DELETE FROM ${this.table("claimed_messages")} + WHERE message_id = ? AND instance_id = ? AND process_id = ? AND activation_token = ? + AND activation_generation = ?`, + [ + claim.message_id, + claim.instance_id, + processId, + currentClaim.activation_token, + currentClaim.activation_generation, + ], + ) + if (removed.changes !== 1) continue await connection.run( `INSERT INTO ${this.table("ready_messages")} (message_id, instance_id, sequence, available_at_ms) VALUES (?, ?, ?, ?) ON CONFLICT(message_id) DO NOTHING`, - [claim.message_id, claim.instance_id, claim.sequence, now], + [claim.message_id, claim.instance_id, currentClaim.sequence, now], + ) + const ownsActivation = + instance.activation_owner_id === processId && + instance.activation_token === currentClaim.activation_token && + BigInt(instance.activation_generation) === BigInt(currentClaim.activation_generation) + if (!ownsActivation) continue + await connection.run( + `UPDATE ${this.table("instances")} + SET activation_owner_id = NULL, activation_token = NULL, + activation_expires_at_ms = NULL, updated_at_ms = ? + WHERE id = ? AND activation_owner_id = ? AND activation_token = ? + AND activation_generation = ?`, + [ + now, + claim.instance_id, + processId, + currentClaim.activation_token, + currentClaim.activation_generation, + ], ) } await connection.run( @@ -1921,6 +1974,35 @@ export class Repository { return " FOR UPDATE SKIP LOCKED" } + private rowLockClause(): string { + if (this.settings.database.family === "sqlite") return "" + return " FOR UPDATE" + } + + private lockActivationFence( + connection: DatabaseConnection, + instanceId: string, + ): Promise { + return connection.get( + `SELECT activation_owner_id, activation_token, activation_generation, + activation_expires_at_ms + FROM ${this.table("instances")} WHERE id = ?${this.rowLockClause()}`, + [instanceId], + ) + } + + private lockMessageClaim( + connection: DatabaseConnection, + messageId: string, + ): Promise { + return connection.get( + `SELECT instance_id, sequence, process_id, activation_token, activation_generation + FROM ${this.table("claimed_messages")} + WHERE message_id = ?${this.rowLockClause()}`, + [messageId], + ) + } + private async loadActorIdentity( connection: DatabaseConnection, instanceId: string, @@ -1961,45 +2043,84 @@ export class Repository { [now], ) for (const claim of expired) { - await connection.run(`DELETE FROM ${this.table("claimed_messages")} WHERE message_id = ?`, [ - claim.message_id, - ]) + const instance = await this.lockActivationFence(connection, claim.instance_id) + if ( + !instance || + instance.activation_expires_at_ms === null || + Number(instance.activation_expires_at_ms) > now + ) { + continue + } + const currentClaim = await this.lockMessageClaim(connection, claim.message_id) + if ( + !currentClaim || + currentClaim.instance_id !== claim.instance_id || + currentClaim.process_id !== instance.activation_owner_id || + currentClaim.activation_token !== instance.activation_token || + BigInt(currentClaim.activation_generation) !== BigInt(instance.activation_generation) + ) { + continue + } + const removed = await connection.run( + `DELETE FROM ${this.table("claimed_messages")} + WHERE message_id = ? AND instance_id = ? AND process_id = ? AND activation_token = ? + AND activation_generation = ?`, + [ + claim.message_id, + claim.instance_id, + currentClaim.process_id, + currentClaim.activation_token, + currentClaim.activation_generation, + ], + ) + if (removed.changes !== 1) continue await connection.run( `INSERT INTO ${this.table("ready_messages")}(message_id, instance_id, sequence, available_at_ms) VALUES (?, ?, ?, ?) ON CONFLICT(message_id) DO NOTHING`, - [claim.message_id, claim.instance_id, claim.sequence, now], + [claim.message_id, claim.instance_id, currentClaim.sequence, now], ) - await connection.run( + const released = await connection.run( `UPDATE ${this.table("instances")} SET activation_owner_id = NULL, activation_token = NULL, activation_expires_at_ms = NULL, - updated_at_ms = ? WHERE id = ?`, - [now, claim.instance_id], + updated_at_ms = ? + WHERE id = ? AND activation_owner_id = ? AND activation_token = ? + AND activation_generation = ? AND activation_expires_at_ms <= ?`, + [ + now, + claim.instance_id, + currentClaim.process_id, + currentClaim.activation_token, + currentClaim.activation_generation, + now, + ], ) + if (released.changes !== 1) throw new Error("expired activation could not be released") } } private async assertFence(connection: DatabaseConnection, turn: ClaimedTurn): Promise { + const instance = await this.lockActivationFence(connection, turn.instance.id) const now = await connection.nowMilliseconds() - const fence = await connection.get<{ found: number | bigint }>( - `SELECT 1 AS found FROM ${this.table("instances")} instances - JOIN ${this.table("claimed_messages")} claimed ON claimed.instance_id = instances.id - WHERE instances.id = ? AND instances.activation_owner_id = ? AND instances.activation_token = ? - AND instances.activation_generation = ? AND instances.activation_expires_at_ms > ? - AND claimed.message_id = ? AND claimed.process_id = ? AND claimed.activation_token = ? - AND claimed.activation_generation = ?`, - [ - turn.instance.id, - turn.processId, - turn.activationToken, - turn.activationGeneration, - now, - turn.message.id, - turn.processId, - turn.activationToken, - turn.activationGeneration, - ], - ) - if (!fence) throw new LostActivation("activation fence no longer matches") + if ( + !instance || + instance.activation_owner_id !== turn.processId || + instance.activation_token !== turn.activationToken || + BigInt(instance.activation_generation) !== turn.activationGeneration || + instance.activation_expires_at_ms === null || + Number(instance.activation_expires_at_ms) <= now + ) { + throw new LostActivation("activation fence no longer matches") + } + const claim = await this.lockMessageClaim(connection, turn.message.id) + if ( + !claim || + claim.instance_id !== turn.instance.id || + claim.process_id !== turn.processId || + claim.activation_token !== turn.activationToken || + BigInt(claim.activation_generation) !== turn.activationGeneration + ) { + throw new LostActivation("message claim no longer matches") + } return now } diff --git a/src/version.ts b/src/version.ts index adb2160..46c3fe7 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = "0.14.7" +export const VERSION = "0.14.8" diff --git a/test/mysql.test.ts b/test/mysql.test.ts index 86f40e2..c272127 100644 --- a/test/mysql.test.ts +++ b/test/mysql.test.ts @@ -9,6 +9,7 @@ import { withDatabaseDeadline } from "../src/database/deadline.js" import type { JsonObject } from "../src/types.js" import { createDashboard } from "../src/web/index.js" import { captureAttempt, PausingClaimDatabase } from "./support/pausing-claim-database.js" +import { deferred, delay, waitForActivationExpiration } from "./support/fenced-commit.js" const connectionString = process.env.SOLID_OBJECTS_DATABASE_URL class TransmitProofCounter extends Actor { @@ -94,6 +95,18 @@ class MySQLMigratingActor extends Actor { } } +class MySQLFencedCommitActor extends Actor { + static override readonly actorType = "MySQLFencedCommitActor" + + count = 0 + + increment(): number { + this.count += 1 + this.commitAction("pause_fenced_commit") + return this.count + } +} + let runtime: SolidObjectsRuntime | undefined let database: MySQLDatabase | undefined @@ -133,6 +146,106 @@ describe("MySQL SQL compatibility", () => { }) describeMySQL("MySQL adapter", () => { + it("keeps a fenced commit exclusive after its lease expires", async () => { + if (!connectionString) throw new Error("MySQL connection string is required") + database = mysql({ connectionString, maximumConnections: 5 }) + runtime = configure({ + database, + tableNamePrefix: "mysql_test_", + authorizeMessage: () => true, + authorizeQuery: () => true, + authorizeDestroy: () => true, + leaseDurationMilliseconds: 100, + leaseRenewalIntervalMilliseconds: 20, + logger: quietLogger, + }) + runtime.register(MySQLFencedCommitActor) + const commitStarted = deferred() + const releaseCommit = deferred() + runtime.registerCommitAction("pause_fenced_commit", async () => { + commitStarted.resolve() + await releaseCommit.promise + }) + await runtime.install() + const actorId = `fenced-${crypto.randomUUID()}` + const message = await MySQLFencedCommitActor.ref(actorId).send.increment() + const firstWorker = runtime.worker() + const firstRun = firstWorker.runOnce() + await commitStarted.promise + try { + await waitForActivationExpiration(runtime, MySQLFencedCommitActor.actorType, actorId) + await runtime.repository.registerProcess("replacement-worker", "worker") + const replacementClaim = runtime.repository.claim("replacement-worker") + const replacementBeforeRelease = await Promise.race([ + replacementClaim.then(() => "settled" as const), + delay(50).then(() => "pending" as const), + ]) + releaseCommit.resolve() + + const [firstResult, replacement] = await Promise.all([firstRun, replacementClaim]) + await firstWorker.stop() + + expect(replacementBeforeRelease).toBe("pending") + expect(firstResult).toBe(1) + expect(replacement).toBeUndefined() + await expect(message.result()).resolves.toBe(1) + } finally { + releaseCommit.resolve() + } + }, 15_000) + + it("does not requeue a commit after process cleanup waits for it", async () => { + if (!connectionString) throw new Error("MySQL connection string is required") + database = mysql({ connectionString, maximumConnections: 5 }) + runtime = configure({ + database, + tableNamePrefix: "mysql_test_", + authorizeMessage: () => true, + authorizeQuery: () => true, + authorizeDestroy: () => true, + logger: quietLogger, + }) + runtime.register(MySQLFencedCommitActor) + const commitStarted = deferred() + const releaseCommit = deferred() + runtime.registerCommitAction("pause_fenced_commit", async () => { + commitStarted.resolve() + await releaseCommit.promise + }) + await runtime.install() + const message = await MySQLFencedCommitActor.ref( + `process-cleanup-${crypto.randomUUID()}`, + ).send.increment() + const firstWorker = runtime.worker() + const firstRun = firstWorker.runOnce() + await commitStarted.promise + try { + const processCleanup = runtime.repository.stopProcess(firstWorker.processId) + const cleanupBeforeRelease = await Promise.race([ + processCleanup.then(() => "settled" as const), + delay(50).then(() => "pending" as const), + ]) + releaseCommit.resolve() + + const [firstResult] = await Promise.all([firstRun, processCleanup]) + const ready = await database.connection((connection) => + connection.get<{ found: number | bigint }>( + `SELECT 1 AS found FROM ${runtime?.repository.table("ready_messages")} + WHERE message_id = ?`, + [message.id], + ), + ) + await firstWorker.stop() + + expect(cleanupBeforeRelease).toBe("pending") + expect(firstResult).toBe(1) + expect(ready).toBeUndefined() + await expect(message.result()).resolves.toBe(1) + } finally { + releaseCommit.resolve() + } + }, 15_000) + it("lets concurrent effect claimants skip locked work", async () => { if (!connectionString) throw new Error("MySQL connection string is required") database = mysql({ connectionString, maximumConnections: 5 }) diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index b850d03..812a412 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -16,6 +16,7 @@ import { withDatabaseDeadline } from "../src/database/deadline.js" import type { JsonObject } from "../src/types.js" import { createDashboard } from "../src/web/index.js" import { captureAttempt, PausingClaimDatabase } from "./support/pausing-claim-database.js" +import { deferred, delay, waitForActivationExpiration } from "./support/fenced-commit.js" const connectionString = process.env.SOLID_OBJECTS_DATABASE_URL const describePostgreSQL = connectionString?.startsWith("postgresql:") ? describe : describe.skip @@ -97,6 +98,18 @@ class PostgreSQLMigratingActor extends Actor { } } +class PostgreSQLFencedCommitActor extends Actor { + static override readonly actorType = "PostgreSQLFencedCommitActor" + + count = 0 + + increment(): number { + this.count += 1 + this.commitAction("pause_fenced_commit") + return this.count + } +} + class TransmitProofCounter extends Actor { static override readonly actorType = "TransmitProofCounter" @@ -167,6 +180,106 @@ describe("PostgreSQL SQL parameters", () => { }) describePostgreSQL("PostgreSQL adapter", () => { + it("keeps a fenced commit exclusive after its lease expires", async () => { + if (!connectionString) throw new Error("PostgreSQL connection string is required") + database = postgresql({ connectionString, maximumConnections: 5 }) + runtime = configure({ + database, + tableNamePrefix: "postgresql_test_", + authorizeMessage: () => true, + authorizeQuery: () => true, + authorizeDestroy: () => true, + leaseDurationMilliseconds: 100, + leaseRenewalIntervalMilliseconds: 20, + logger: quietLogger, + }) + runtime.register(PostgreSQLFencedCommitActor) + const commitStarted = deferred() + const releaseCommit = deferred() + runtime.registerCommitAction("pause_fenced_commit", async () => { + commitStarted.resolve() + await releaseCommit.promise + }) + await runtime.install() + const actorId = `fenced-${crypto.randomUUID()}` + const message = await PostgreSQLFencedCommitActor.ref(actorId).send.increment() + const firstWorker = runtime.worker() + const firstRun = firstWorker.runOnce() + await commitStarted.promise + try { + await waitForActivationExpiration(runtime, PostgreSQLFencedCommitActor.actorType, actorId) + await runtime.repository.registerProcess("replacement-worker", "worker") + const replacementClaim = runtime.repository.claim("replacement-worker") + const replacementBeforeRelease = await Promise.race([ + replacementClaim.then(() => "settled" as const), + delay(50).then(() => "pending" as const), + ]) + releaseCommit.resolve() + + const [firstResult, replacement] = await Promise.all([firstRun, replacementClaim]) + await firstWorker.stop() + + expect(replacementBeforeRelease).toBe("pending") + expect(firstResult).toBe(1) + expect(replacement).toBeUndefined() + await expect(message.result()).resolves.toBe(1) + } finally { + releaseCommit.resolve() + } + }, 15_000) + + it("does not requeue a commit after process cleanup waits for it", async () => { + if (!connectionString) throw new Error("PostgreSQL connection string is required") + database = postgresql({ connectionString, maximumConnections: 5 }) + runtime = configure({ + database, + tableNamePrefix: "postgresql_test_", + authorizeMessage: () => true, + authorizeQuery: () => true, + authorizeDestroy: () => true, + logger: quietLogger, + }) + runtime.register(PostgreSQLFencedCommitActor) + const commitStarted = deferred() + const releaseCommit = deferred() + runtime.registerCommitAction("pause_fenced_commit", async () => { + commitStarted.resolve() + await releaseCommit.promise + }) + await runtime.install() + const message = await PostgreSQLFencedCommitActor.ref( + `process-cleanup-${crypto.randomUUID()}`, + ).send.increment() + const firstWorker = runtime.worker() + const firstRun = firstWorker.runOnce() + await commitStarted.promise + try { + const processCleanup = runtime.repository.stopProcess(firstWorker.processId) + const cleanupBeforeRelease = await Promise.race([ + processCleanup.then(() => "settled" as const), + delay(50).then(() => "pending" as const), + ]) + releaseCommit.resolve() + + const [firstResult] = await Promise.all([firstRun, processCleanup]) + const ready = await database.connection((connection) => + connection.get<{ found: number | bigint }>( + `SELECT 1 AS found FROM ${runtime?.repository.table("ready_messages")} + WHERE message_id = ?`, + [message.id], + ), + ) + await firstWorker.stop() + + expect(cleanupBeforeRelease).toBe("pending") + expect(firstResult).toBe(1) + expect(ready).toBeUndefined() + await expect(message.result()).resolves.toBe(1) + } finally { + releaseCommit.resolve() + } + }, 15_000) + it("does not hide broadcast work across concurrent recovery probes", async () => { if (!connectionString) throw new Error("PostgreSQL connection string is required") database = postgresql({ connectionString, maximumConnections: 5 }) diff --git a/test/support/fenced-commit.ts b/test/support/fenced-commit.ts new file mode 100644 index 0000000..142eedc --- /dev/null +++ b/test/support/fenced-commit.ts @@ -0,0 +1,38 @@ +import type { SolidObjectsRuntime } from "../../src/runtime.js" + +export function deferred(): { promise: Promise; resolve: () => void } { + let resolve = () => {} + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise + }) + return { promise, resolve } +} + +export function delay(milliseconds: number): Promise { + return new Promise((resolve) => setTimeout(resolve, milliseconds)) +} + +export async function waitForActivationExpiration( + runtime: SolidObjectsRuntime, + actorType: string, + actorId: string, +): Promise { + const expiration = await runtime.settings.database.connection((connection) => + connection.get<{ activation_expires_at_ms: number | bigint }>( + `SELECT activation_expires_at_ms FROM ${runtime.repository.table("instances")} + WHERE actor_type = ? AND actor_id = ?`, + [actorType, actorId], + ), + ) + if (!expiration) throw new Error("active actor instance was not found") + const expiresAt = Number(expiration.activation_expires_at_ms) + const deadline = Date.now() + 2_000 + while (Date.now() < deadline) { + const now = await runtime.settings.database.connection((connection) => + connection.nowMilliseconds(), + ) + if (now > expiresAt) return + await delay(Math.min(Math.max(expiresAt - now + 1, 1), 10)) + } + throw new Error("activation lease did not expire") +}