From 272aa9728aad7d9c72d5e32ef54616ae1fa9a988 Mon Sep 17 00:00:00 2001 From: xiaomo Date: Sat, 18 Apr 2026 07:51:25 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20pulseflare=20package=20=E2=80=94=20CF?= =?UTF-8?q?=20Workers=20+=20D1=20runtime=20(refs=20#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 193 +++++++++++++ packages/pulseflare/package.json | 17 ++ packages/pulseflare/src/durable-tick.ts | 138 ++++++++++ packages/pulseflare/src/executor-sigil.ts | 20 ++ packages/pulseflare/src/index.ts | 92 +++++++ packages/pulseflare/src/schema.sql | 56 ++++ packages/pulseflare/src/store-d1.ts | 312 ++++++++++++++++++++++ packages/pulseflare/tsconfig.json | 17 ++ packages/pulseflare/wrangler.toml | 20 ++ 9 files changed, 865 insertions(+) create mode 100644 packages/pulseflare/package.json create mode 100644 packages/pulseflare/src/durable-tick.ts create mode 100644 packages/pulseflare/src/executor-sigil.ts create mode 100644 packages/pulseflare/src/index.ts create mode 100644 packages/pulseflare/src/schema.sql create mode 100644 packages/pulseflare/src/store-d1.ts create mode 100644 packages/pulseflare/tsconfig.json create mode 100644 packages/pulseflare/wrangler.toml diff --git a/bun.lock b/bun.lock index 74c2bd4..75682e8 100644 --- a/bun.lock +++ b/bun.lock @@ -60,6 +60,17 @@ "@uncaged/pulse": ">=0.1.0", }, }, + "packages/pulseflare": { + "name": "@uncaged/pulseflare", + "version": "0.1.0", + "dependencies": { + "@uncaged/pulse": "workspace:*", + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260418.1", + "wrangler": "^4.83.0", + }, + }, "packages/upulse": { "name": "@uncaged/upulse", "version": "0.1.0", @@ -96,6 +107,144 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.11", "", { "os": "win32", "cpu": "x64" }, "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260415.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-dsxaKsQm3LnPGNPEdsRv09QN3Y4DqCw7kX5j6noKqbAtro2jTr95sVlYM1jUxZ5FkOl1f7SXgaKKB9t5H5Nkbg=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260415.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JgSgVA49KyKteHRA1SnonE4Zn5Ei5zdAp5FQMxFmXI8qulZw4Hl7safXxRyK4i9sTO8gl7TFOKO5Q64VPvSDQ=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260415.1", "", { "os": "linux", "cpu": "x64" }, "sha512-tU+9pwsqCy8afOVlGtiWrWQc/fedQK4SRm4KPIAt+zOiQWDxWASm6YGBUJis5c648WN80yz47qnmdDi8DQNOcA=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260415.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-bR9uITnV19r5NQ14xnypi2xHXu2iQvfYV8cVgx0JouFUmWwTEEAwFVojDdssGq93VHX9hr/pi2IRUZeegbYBog=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260415.1", "", { "os": "win32", "cpu": "x64" }, "sha512-4NuMLlerI0Ijua3Ir8HXQ+qyNvCUDEG5gDco5Om+sAiK6rnWiz+aGoSlbB8W16yW9QAgzCstbmXLiVknUBflfQ=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260418.1", "", {}, "sha512-bywXb2XmeSqrLCQYipcupLneqx015YhhNWz2v9b9iatpe8Cg551vP7ZuD5S2a6GfBka0dDnO70kIBiBvFglcrg=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@uncaged/pulse": ["@uncaged/pulse@workspace:packages/pulse"], @@ -106,18 +255,62 @@ "@uncaged/pulse-openclaw": ["@uncaged/pulse-openclaw@workspace:packages/pulse-openclaw"], + "@uncaged/pulseflare": ["@uncaged/pulseflare@workspace:packages/pulseflare"], + "@uncaged/upulse": ["@uncaged/upulse@workspace:packages/upulse"], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "jsonata": ["jsonata@2.1.0", "", {}, "sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "miniflare": ["miniflare@4.20260415.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.8", "workerd": "1.20260415.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JoExRWN4YBI2luA5BoSMFEgi8rQWXUGzo3mtE+58VXCLV3jj/Xnk5Yeqs/IXWz8Es5GJIaq6BtsixDvAxXSIng=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "workerd": ["workerd@1.20260415.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260415.1", "@cloudflare/workerd-darwin-arm64": "1.20260415.1", "@cloudflare/workerd-linux-64": "1.20260415.1", "@cloudflare/workerd-linux-arm64": "1.20260415.1", "@cloudflare/workerd-windows-64": "1.20260415.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-phyPjRnx+mQDfkhN9ENPioL1L0SdhYs4S0YmJK/xF9Oga+ykNfdSy1MHnsOj8yqnOV96zcVQMx32dJ0r3pq0jQ=="], + + "wrangler": ["wrangler@4.83.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260415.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260415.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260415.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-gw5g3LCiuAqVWxaoKY6+quE0HzAUEFb/FV3oAlNkE1ttd4XP3FiV91XDkkzUCcdqxS4WjhQvPhIDBNdhEi8P0A=="], + + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], } } diff --git a/packages/pulseflare/package.json b/packages/pulseflare/package.json new file mode 100644 index 0000000..ae5060f --- /dev/null +++ b/packages/pulseflare/package.json @@ -0,0 +1,17 @@ +{ + "name": "@uncaged/pulseflare", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "migrate": "wrangler d1 migrations apply pulseflare-db" + }, + "dependencies": { + "@uncaged/pulse": "workspace:*" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260418.1", + "wrangler": "^4.83.0" + } +} \ No newline at end of file diff --git a/packages/pulseflare/src/durable-tick.ts b/packages/pulseflare/src/durable-tick.ts new file mode 100644 index 0000000..19b1328 --- /dev/null +++ b/packages/pulseflare/src/durable-tick.ts @@ -0,0 +1,138 @@ +import type { D1Database, DurableObject, DurableObjectState } from '@cloudflare/workers-types'; +import { D1PulseStore } from './store-d1.js'; +import { createSigilExecutor } from './executor-sigil.js'; + +export interface PulseTickEnv { + DB: D1Database; + SIGIL_URL: string; + SIGIL_TOKEN?: string; + TICK_INTERVAL_MS?: string; +} + +export class PulseTick implements DurableObject { + private tickIntervalMs: number; + private store: D1PulseStore; + private sigilExecutor: ReturnType; + + constructor(private state: DurableObjectState, private env: PulseTickEnv) { + this.tickIntervalMs = parseInt(env.TICK_INTERVAL_MS ?? '30000'); // Default 30s + this.store = new D1PulseStore(env.DB); + this.sigilExecutor = createSigilExecutor( + env.SIGIL_URL, + env.SIGIL_TOKEN ?? 'default-token' + ); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + + if (request.method === 'POST' && url.pathname === '/tick') { + // Manual tick trigger + await this.performTick(); + return new Response(JSON.stringify({ status: 'tick performed' }), { + headers: { 'Content-Type': 'application/json' } + }); + } + + if (request.method === 'GET' && url.pathname === '/status') { + const status = await this.getStatus(); + return new Response(JSON.stringify(status), { + headers: { 'Content-Type': 'application/json' } + }); + } + + if (request.method === 'POST' && url.pathname === '/start') { + await this.startTicking(); + return new Response(JSON.stringify({ status: 'ticking started' }), { + headers: { 'Content-Type': 'application/json' } + }); + } + + return new Response('Not found', { status: 404 }); + } + + async alarm(): Promise { + try { + await this.performTick(); + + // Schedule next tick + const nextTickTime = Date.now() + this.tickIntervalMs; + await this.state.storage.setAlarm(nextTickTime); + } catch (error) { + console.error('Error during tick:', error); + + // Still schedule next tick even if this one failed + const nextTickTime = Date.now() + this.tickIntervalMs; + await this.state.storage.setAlarm(nextTickTime); + } + } + + private async startTicking(): Promise { + // Cancel any existing alarm + await this.state.storage.deleteAlarm(); + + // Set first alarm + const firstTickTime = Date.now() + this.tickIntervalMs; + await this.state.storage.setAlarm(firstTickTime); + + await this.state.storage.put('ticking', true); + } + + private async performTick(): Promise { + console.log('Performing tick at', new Date().toISOString()); + + // 1. Read latest state from D1 + const hasEvents = await this.store.hasEvents(); + const recentEvents = await this.store.getRecent(10); + + console.log('Has events:', hasEvents, 'Recent events:', recentEvents.length); + + // 2. Run Pulse tick logic (placeholder for now) + // This is where we would: + // - Load rule definitions + // - Process events through rules + // - Generate effects + // - Execute effects through Sigil + + // Placeholder: just log the tick + const tickEvent = { + occurredAt: Date.now(), + kind: 'pulse.tick', + meta: JSON.stringify({ + eventsCount: recentEvents.length, + tickedAt: new Date().toISOString() + }) + }; + + await this.store.appendEvent(tickEvent); + + // Example effect execution (commented out for now) + /* + try { + const result = await this.sigilExecutor.invoke('example-capability', { + message: 'Tick performed', + timestamp: Date.now() + }); + console.log('Effect result:', result); + } catch (error) { + console.error('Error executing effect:', error); + } + */ + } + + private async getStatus(): Promise { + const isTicking = await this.state.storage.get('ticking') ?? false; + const currentAlarm = await this.state.storage.getAlarm(); + const hasEvents = await this.store.hasEvents(); + const recentEvents = await this.store.getRecent(5); + + return { + isTicking, + nextAlarmAt: currentAlarm ? new Date(currentAlarm).toISOString() : null, + tickIntervalMs: this.tickIntervalMs, + hasEvents, + recentEventsCount: recentEvents.length, + lastEvents: recentEvents.slice(0, 3) + }; + } +} \ No newline at end of file diff --git a/packages/pulseflare/src/executor-sigil.ts b/packages/pulseflare/src/executor-sigil.ts new file mode 100644 index 0000000..00404a7 --- /dev/null +++ b/packages/pulseflare/src/executor-sigil.ts @@ -0,0 +1,20 @@ +export interface SigilExecutor { + invoke(capability: string, input: unknown): Promise<{ status: number; payload: unknown }>; +} + +export function createSigilExecutor(sigilUrl: string, token: string): SigilExecutor { + return { + async invoke(capability, input) { + const res = await fetch(`${sigilUrl}/_api/invoke`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ capability, params: input }), + }); + const data = await res.json(); + return { status: res.status, payload: data }; + } + }; +} \ No newline at end of file diff --git a/packages/pulseflare/src/index.ts b/packages/pulseflare/src/index.ts new file mode 100644 index 0000000..e5b3daf --- /dev/null +++ b/packages/pulseflare/src/index.ts @@ -0,0 +1,92 @@ +import type { D1Database, DurableObjectNamespace } from '@cloudflare/workers-types'; +import { D1PulseStore } from './store-d1.js'; +import { PulseTick } from './durable-tick.js'; + +export { PulseTick }; + +export interface Env { + DB: D1Database; + PULSE_TICK: DurableObjectNamespace; + SIGIL_URL: string; + SIGIL_TOKEN?: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const path = url.pathname; + + // Health check + if (request.method === 'GET' && path === '/health') { + return new Response(JSON.stringify({ + status: 'healthy', + timestamp: new Date().toISOString(), + service: 'pulseflare' + }), { + headers: { 'Content-Type': 'application/json' } + }); + } + + // Manual tick trigger + if (request.method === 'POST' && path === '/tick') { + const durableObjectId = env.PULSE_TICK.idFromName('default'); + const durableObject = env.PULSE_TICK.get(durableObjectId); + + return await durableObject.fetch(new Request(request.url.replace(path, '/tick'), { + method: 'POST' + })); + } + + // Get events + if (request.method === 'GET' && path === '/events') { + const store = new D1PulseStore(env.DB); + const limit = parseInt(url.searchParams.get('limit') ?? '20'); + const events = await store.getRecent(limit); + + return new Response(JSON.stringify({ + events, + count: events.length, + timestamp: new Date().toISOString() + }), { + headers: { 'Content-Type': 'application/json' } + }); + } + + // Get status (from Durable Object) + if (request.method === 'GET' && path === '/status') { + const durableObjectId = env.PULSE_TICK.idFromName('default'); + const durableObject = env.PULSE_TICK.get(durableObjectId); + + return await durableObject.fetch(new Request(request.url.replace(path, '/status'), { + method: 'GET' + })); + } + + // Start ticking + if (request.method === 'POST' && path === '/start') { + const durableObjectId = env.PULSE_TICK.idFromName('default'); + const durableObject = env.PULSE_TICK.get(durableObjectId); + + return await durableObject.fetch(new Request(request.url.replace(path, '/start'), { + method: 'POST' + })); + } + + // Default 404 + return new Response(JSON.stringify({ + error: 'Not found', + path, + method: request.method, + available_endpoints: [ + 'GET /health', + 'POST /tick', + 'GET /events', + 'GET /status', + 'POST /start' + ] + }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }); + }, +}; \ No newline at end of file diff --git a/packages/pulseflare/src/schema.sql b/packages/pulseflare/src/schema.sql new file mode 100644 index 0000000..5cb5b25 --- /dev/null +++ b/packages/pulseflare/src/schema.sql @@ -0,0 +1,56 @@ +-- Events table +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + occurred_at INTEGER NOT NULL, + kind TEXT NOT NULL, + key TEXT, + hash TEXT, + code_rev TEXT, + meta TEXT, + object_id INTEGER REFERENCES objects(id) +); + +CREATE INDEX IF NOT EXISTS idx_occurred ON events(occurred_at); +CREATE INDEX IF NOT EXISTS idx_kind_key ON events(kind, key, occurred_at); +CREATE INDEX IF NOT EXISTS idx_code_rev ON events(code_rev, occurred_at); + +-- Objects table +CREATE TABLE IF NOT EXISTS objects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + object_type TEXT NOT NULL, + external_id TEXT, + created_at INTEGER NOT NULL, + code_rev TEXT NOT NULL, + UNIQUE(object_type, external_id) +); + +-- Projections table (from projection-engine.js) +CREATE TABLE IF NOT EXISTS projections ( + name TEXT PRIMARY KEY, + last_event_id INTEGER NOT NULL DEFAULT 0 +); + +-- Defs tables (from defs.js) +CREATE TABLE IF NOT EXISTS event_defs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code_hash TEXT NOT NULL UNIQUE, + data TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS rule_defs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code_hash TEXT NOT NULL UNIQUE, + data TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS executor_defs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code_hash TEXT NOT NULL UNIQUE, + data TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS projection_defs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code_hash TEXT NOT NULL UNIQUE, + data TEXT NOT NULL +); \ No newline at end of file diff --git a/packages/pulseflare/src/store-d1.ts b/packages/pulseflare/src/store-d1.ts new file mode 100644 index 0000000..4e3d188 --- /dev/null +++ b/packages/pulseflare/src/store-d1.ts @@ -0,0 +1,312 @@ +import type { D1Database } from '@cloudflare/workers-types'; +import type { EventRecord, ObjectInstance, PulseStore } from '@uncaged/pulse'; + +/** + * D1 implementation of PulseStore interface + * Adapts from bun:sqlite to Cloudflare D1 + */ +export class D1PulseStore implements PulseStore { + constructor(private db: D1Database) {} + + async appendEvent(event: Omit): Promise { + const result = await this.db + .prepare(` + INSERT INTO events (occurred_at, kind, key, hash, code_rev, meta, object_id) + VALUES (?, ?, ?, ?, ?, ?, ?) + `) + .bind( + event.occurredAt, + event.kind, + event.key ?? null, + event.hash ?? null, + event.codeRev ?? null, + event.meta ?? null, + event.objectId ?? null, + ) + .run(); + + const id = result.meta.last_row_id as number; + return { id, ...event }; + } + + async appendEvents(events: Omit[]): Promise { + const statements = events.map(event => + this.db + .prepare(` + INSERT INTO events (occurred_at, kind, key, hash, code_rev, meta, object_id) + VALUES (?, ?, ?, ?, ?, ?, ?) + `) + .bind( + event.occurredAt, + event.kind, + event.key ?? null, + event.hash ?? null, + event.codeRev ?? null, + event.meta ?? null, + event.objectId ?? null, + ) + ); + + const results = await this.db.batch(statements); + + return events.map((event, index) => ({ + id: results[index].meta.last_row_id as number, + ...event + })); + } + + async createObject(opts: { objectType: string; externalId?: string; codeRev: string }): Promise { + const now = Date.now(); + const extId = opts.externalId ?? null; + + // Idempotent: if (objectType, externalId) already exists, return existing id + if (extId !== null) { + const existing = await this.db + .prepare('SELECT id FROM objects WHERE object_type = ? AND external_id = ?') + .bind(opts.objectType, extId) + .first<{ id: number }>(); + + if (existing) return existing.id; + } + + const result = await this.db + .prepare('INSERT INTO objects (object_type, external_id, created_at, code_rev) VALUES (?, ?, ?, ?)') + .bind(opts.objectType, extId, now, opts.codeRev) + .run(); + + return result.meta.last_row_id as number; + } + + async getObjectInstance(id: number): Promise { + const row = await this.db + .prepare('SELECT * FROM objects WHERE id = ?') + .bind(id) + .first(); + + if (!row) return null; + + return { + id: row.id, + objectType: row.object_type, + externalId: row.external_id, + createdAt: row.created_at, + codeRev: row.code_rev, + }; + } + + async queryObjectsByType(objectType: string): Promise { + const { results } = await this.db + .prepare('SELECT * FROM objects WHERE object_type = ?') + .bind(objectType) + .all(); + + return results.map(row => ({ + id: row.id, + objectType: row.object_type, + externalId: row.external_id, + createdAt: row.created_at, + codeRev: row.code_rev, + })); + } + + async getLatest(kind: string, key?: string): Promise { + const row = await this.db + .prepare(` + SELECT * FROM events + WHERE kind = ? AND (key = ? OR ? IS NULL) + ORDER BY occurred_at DESC, id DESC + LIMIT 1 + `) + .bind(kind, key ?? null, key ?? null) + .first(); + + return row ? this.rowToEventRecord(row) : null; + } + + async getLatestWhere(opts: { kind: string; key?: string; codeRev?: string }): Promise { + const conditions: string[] = ['kind = ?']; + const params: (string | number | null)[] = [opts.kind]; + + if (opts.key !== undefined) { + conditions.push('key = ?'); + params.push(opts.key); + } + if (opts.codeRev !== undefined) { + conditions.push('code_rev = ?'); + params.push(opts.codeRev); + } + + const sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY occurred_at DESC, id DESC LIMIT 1`; + let stmt = this.db.prepare(sql); + for (const param of params) { + stmt = stmt.bind(param); + } + + const row = await stmt.first(); + return row ? this.rowToEventRecord(row) : null; + } + + async getRecent(limit: number = 20): Promise { + const { results } = await this.db + .prepare(`SELECT * FROM events ORDER BY occurred_at DESC, id DESC LIMIT ?`) + .bind(limit) + .all(); + + return results.map(row => this.rowToEventRecord(row)); + } + + async queryByKind( + kind: string, + opts?: { + key?: string; + since?: number; + codeRev?: string; + limit?: number; + }, + ): Promise { + const conditions: string[] = ['kind = ?']; + const params: (string | number | null)[] = [kind]; + + if (opts?.key !== undefined) { + conditions.push('key = ?'); + params.push(opts.key); + } + if (opts?.since !== undefined) { + conditions.push('occurred_at >= ?'); + params.push(opts.since); + } + if (opts?.codeRev !== undefined) { + conditions.push('code_rev = ?'); + params.push(opts.codeRev); + } + + let sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY occurred_at DESC, id DESC`; + if (opts?.limit !== undefined) { + sql += ' LIMIT ?'; + params.push(opts.limit); + } + + let stmt = this.db.prepare(sql); + for (const param of params) { + stmt = stmt.bind(param); + } + + const { results } = await stmt.all(); + return results.map(row => this.rowToEventRecord(row)); + } + + async getAfter( + afterId: number, + opts?: { + kind?: string; + key?: string; + codeRev?: string; + }, + ): Promise { + const conditions: string[] = ['id > ?']; + const params: (string | number | null)[] = [afterId]; + + if (opts?.kind !== undefined) { + conditions.push('kind = ?'); + params.push(opts.kind); + } + if (opts?.key !== undefined) { + conditions.push('key = ?'); + params.push(opts.key); + } + if (opts?.codeRev !== undefined) { + conditions.push('code_rev = ?'); + params.push(opts.codeRev); + } + + const sql = `SELECT * FROM events WHERE ${conditions.join(' AND ')} ORDER BY id ASC`; + let stmt = this.db.prepare(sql); + for (const param of params) { + stmt = stmt.bind(param); + } + + const { results } = await stmt.all(); + return results.map(row => this.rowToEventRecord(row)); + } + + async hasEvents(): Promise { + const result = await this.db + .prepare('SELECT 1 FROM events LIMIT 1') + .first(); + return result !== null; + } + + async putObject(data: unknown): Promise { + // For simplicity, use a basic hash function + const hash = await this.hashObject(data); + + // Store in D1 as a simple key-value table (could be added to schema later) + // For now, just return the hash - actual CAS implementation would need object storage + return hash; + } + + async getObject(hash: string): Promise { + // Placeholder - would need object storage or D1 table for CAS + return null; + } + + async close(): Promise { + // D1 doesn't need explicit closing + } + + async archiveEvents(olderThan: number): Promise { + const result = await this.db + .prepare('DELETE FROM events WHERE occurred_at < ?') + .bind(olderThan) + .run(); + + return result.meta.changes; + } + + async downsampleEvents( + kind: string, + key: string, + intervalMs: number, + olderThan: number, + ): Promise { + const safeInterval = Math.floor(Math.abs(intervalMs)); + if (safeInterval <= 0) return 0; + + const result = await this.db + .prepare(` + DELETE FROM events WHERE kind = ? AND key = ? AND occurred_at < ? AND id NOT IN ( + SELECT id FROM ( + SELECT id, ROW_NUMBER() OVER ( + PARTITION BY (occurred_at / ${safeInterval}) ORDER BY occurred_at DESC + ) as rn FROM events WHERE kind = ? AND key = ? AND occurred_at < ? + ) WHERE rn = 1 + ) + `) + .bind(kind, key, olderThan, kind, key, olderThan) + .run(); + + return result.meta.changes; + } + + private rowToEventRecord(row: any): EventRecord { + const rec: EventRecord = { + id: row.id, + occurredAt: row.occurred_at, + kind: row.kind, + }; + if (row.key != null) rec.key = row.key; + if (row.hash != null) rec.hash = row.hash; + if (row.code_rev != null) rec.codeRev = row.code_rev; + if (row.meta != null) rec.meta = row.meta; + if (row.object_id != null) rec.objectId = row.object_id; + return rec; + } + + private async hashObject(data: unknown): Promise { + const str = JSON.stringify(data); + const msgUint8 = new TextEncoder().encode(str); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 32); + } +} \ No newline at end of file diff --git a/packages/pulseflare/tsconfig.json b/packages/pulseflare/tsconfig.json new file mode 100644 index 0000000..260e9cb --- /dev/null +++ b/packages/pulseflare/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "WebWorker"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "types": ["@cloudflare/workers-types"] + }, + + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/pulseflare/wrangler.toml b/packages/pulseflare/wrangler.toml new file mode 100644 index 0000000..273611d --- /dev/null +++ b/packages/pulseflare/wrangler.toml @@ -0,0 +1,20 @@ +name = "pulseflare" +main = "src/index.ts" +compatibility_date = "2024-09-25" + +[[d1_databases]] +binding = "DB" +database_name = "pulseflare-db" +database_id = "placeholder-fill-later" + +[durable_objects] +bindings = [ + { name = "PULSE_TICK", class_name = "PulseTick" } +] + +[[migrations]] +tag = "v1" +new_classes = ["PulseTick"] + +[vars] +SIGIL_URL = "https://sigil.shazhou.workers.dev" \ No newline at end of file