diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ba6155ba --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,40 @@ + +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node +{ + "name": "Node.js", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + "settings": {}, + "extensions": [ + "streetsidesoftware.code-spell-checker" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [3000], + + // Use 'portsAttributes' to set default properties for specific forwarded ports. + // More info: https://containers.dev/implementors/json_reference/#port-attributes + "portsAttributes": { + "3000": { + "label": "Hello Remote World", + "onAutoForward": "notify" + } + }, + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install" + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/bun.lock b/bun.lock index 1e3a6425..1068e59e 100644 --- a/bun.lock +++ b/bun.lock @@ -826,6 +826,7 @@ "strip-ansi": "7.1.2", }, "devDependencies": { + "@happy-dom/global-registrator": "20.0.11", "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", "@types/luxon": "catalog:", @@ -1442,8 +1443,6 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="], "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.12.0", "", { "dependencies": { "@bufbuild/protobuf": "2.12.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow=="], @@ -3002,8 +3001,6 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.8", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.8", "vitest": "4.1.8" }, "optionalPeers": ["@vitest/browser"] }, "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/mocker": ["@vitest/mocker@4.1.7", "", { "dependencies": { "@vitest/spy": "4.1.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA=="], @@ -3124,8 +3121,6 @@ "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg=="], - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -3610,8 +3605,6 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "engine.io-client": ["engine.io-client@6.6.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], @@ -4132,12 +4125,6 @@ "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], - - "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], - - "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], - "iterate-iterator": ["iterate-iterator@1.0.2", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="], "iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="], @@ -4306,8 +4293,6 @@ "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], - "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], @@ -6122,10 +6107,6 @@ "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], - "@standard-community/standard-json/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - - "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -6154,10 +6135,6 @@ "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], - - "@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], @@ -6208,8 +6185,6 @@ "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], "astro/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -6300,8 +6275,6 @@ "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "esast-util-from-js/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -6352,8 +6325,6 @@ "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - "istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], @@ -6960,18 +6931,12 @@ "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], - "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@storybook/csf-plugin/unplugin/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], - "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 65a884f5..c901ce41 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -29,6 +29,7 @@ "test": "bun test src --only-failures" }, "devDependencies": { + "@happy-dom/global-registrator": "20.0.11", "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", "@types/luxon": "catalog:", diff --git a/packages/session-ui/src/pierre/file-runtime.test.ts b/packages/session-ui/src/pierre/file-runtime.test.ts new file mode 100644 index 00000000..e61372e2 --- /dev/null +++ b/packages/session-ui/src/pierre/file-runtime.test.ts @@ -0,0 +1,143 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test" +import { GlobalRegistrator } from "@happy-dom/global-registrator" + +import { createReadyWatcher, notifyShadowReady } from "./file-runtime" + +beforeAll(() => { + GlobalRegistrator.register() + + globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => { + callback(0) + return 1 + }) as typeof requestAnimationFrame +}) + +afterAll(() => { + GlobalRegistrator.unregister() +}) + +describe("notifyShadowReady", () => { + test("calls onReady when the shadow root already exists and is ready", () => { + const state = createReadyWatcher() + const container = document.createElement("div") + const host = document.createElement("div") + const root = host.attachShadow({ mode: "open" }) + + let readyCalled = false + + notifyShadowReady({ + state, + container, + getRoot: () => root, + isReady: () => true, + onReady: () => { + readyCalled = true + }, + }) + + expect(readyCalled).toBe(true) + }) + + test("waits until an existing shadow root becomes ready", async () => { + const state = createReadyWatcher() + const container = document.createElement("div") + const host = document.createElement("div") + const root = host.attachShadow({ mode: "open" }) + + let ready = false + let readyCalled = false + + notifyShadowReady({ + state, + container, + getRoot: () => root, + isReady: () => ready, + onReady: () => { + readyCalled = true + }, + }) + + expect(readyCalled).toBe(false) + + ready = true + root.appendChild(document.createElement("span")) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(readyCalled).toBe(true) + }) + + test("waits for the shadow root to become available", async () => { + const state = createReadyWatcher() + const container = document.createElement("div") + + let root: ShadowRoot | undefined + let readyCalled = false + + notifyShadowReady({ + state, + container, + getRoot: () => root, + isReady: () => true, + onReady: () => { + readyCalled = true + }, + }) + + expect(readyCalled).toBe(false) + + const host = document.createElement("div") + root = host.attachShadow({ mode: "open" }) + + container.appendChild(host) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(readyCalled).toBe(true) + }) + + test("does not call onReady for an outdated watcher", () => { + const state = createReadyWatcher() + const container = document.createElement("div") + const host = document.createElement("div") + const root = host.attachShadow({ mode: "open" }) + + const callbacks: FrameRequestCallback[] = [] + + globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => { + callbacks.push(callback) + return callbacks.length + }) as typeof requestAnimationFrame + + let firstReadyCalled = false + let secondReadyCalled = false + + notifyShadowReady({ + state, + container, + getRoot: () => root, + isReady: () => true, + onReady: () => { + firstReadyCalled = true + }, + settleFrames: 1, + }) + + notifyShadowReady({ + state, + container, + getRoot: () => root, + isReady: () => true, + onReady: () => { + secondReadyCalled = true + }, + }) + + while (callbacks.length > 0) { + callbacks.shift()?.(0) + } + + expect(firstReadyCalled).toBe(false) + expect(secondReadyCalled).toBe(true) + }) +}) \ No newline at end of file diff --git a/packages/session-ui/src/pierre/file-runtime.ts b/packages/session-ui/src/pierre/file-runtime.ts index a2072100..ffad1c24 100644 --- a/packages/session-ui/src/pierre/file-runtime.ts +++ b/packages/session-ui/src/pierre/file-runtime.ts @@ -1,8 +1,132 @@ +// type ReadyWatcher = { +// observer?: MutationObserver +// token: number +// } + +// export function createReadyWatcher(): ReadyWatcher { +// return { token: 0 } +// } + +// export function clearReadyWatcher(state: ReadyWatcher) { +// state.observer?.disconnect() +// state.observer = undefined +// } + +// export function getViewerHost(container: HTMLElement | undefined) { +// if (!container) return +// const host = container.querySelector("diffs-container") +// if (!(host instanceof HTMLElement)) return +// return host +// } + +// export function getViewerRoot(container: HTMLElement | undefined) { +// return getViewerHost(container)?.shadowRoot ?? undefined +// } + +// export function applyViewerScheme(host: HTMLElement | undefined) { +// if (!host) return +// if (typeof document === "undefined") return + +// const scheme = document.documentElement.dataset.colorScheme +// if (scheme === "dark" || scheme === "light") { +// host.dataset.colorScheme = scheme +// return +// } + +// host.removeAttribute("data-color-scheme") +// } + +// export function observeViewerScheme(getHost: () => HTMLElement | undefined) { +// if (typeof document === "undefined") return () => {} + +// applyViewerScheme(getHost()) +// if (typeof MutationObserver === "undefined") return () => {} + +// const root = document.documentElement +// const monitor = new MutationObserver(() => applyViewerScheme(getHost())) +// monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] }) +// return () => monitor.disconnect() +// } + +// export function notifyShadowReady(opts: { +// state: ReadyWatcher +// container: HTMLElement +// getRoot: () => ShadowRoot | undefined +// isReady: (root: ShadowRoot) => boolean +// onReady: () => void +// settleFrames?: number +// }) { +// clearReadyWatcher(opts.state) +// opts.state.token += 1 + +// const token = opts.state.token +// const settle = Math.max(0, opts.settleFrames ?? 0) + +// const runReady = () => { +// const step = (left: number) => { +// if (token !== opts.state.token) return +// if (left <= 0) { +// opts.onReady() +// return +// } +// requestAnimationFrame(() => step(left - 1)) +// } + +// requestAnimationFrame(() => step(settle)) +// } + +// const observeRoot = (root: ShadowRoot) => { +// if (opts.isReady(root)) { +// runReady() +// return +// } + +// if (typeof MutationObserver === "undefined") return + +// clearReadyWatcher(opts.state) +// opts.state.observer = new MutationObserver(() => { +// if (token !== opts.state.token) return +// if (!opts.isReady(root)) return + +// clearReadyWatcher(opts.state) +// runReady() +// }) +// opts.state.observer.observe(root, { childList: true, subtree: true }) +// } + +// const root = opts.getRoot() +// if (!root) { +// if (typeof MutationObserver === "undefined") return + +// opts.state.observer = new MutationObserver(() => { +// if (token !== opts.state.token) return + +// const next = opts.getRoot() +// if (!next) return + +// observeRoot(next) +// }) +// opts.state.observer.observe(opts.container, { childList: true, subtree: true }) +// return +// } + +// observeRoot(root) +// } + type ReadyWatcher = { observer?: MutationObserver token: number } +type ReadyOptions = { + state: ReadyWatcher + container: HTMLElement + getRoot: () => ShadowRoot | undefined + isReady: (root: ShadowRoot) => boolean + onReady: () => void + settleFrames?: number +} + export function createReadyWatcher(): ReadyWatcher { return { token: 0 } } @@ -14,8 +138,11 @@ export function clearReadyWatcher(state: ReadyWatcher) { export function getViewerHost(container: HTMLElement | undefined) { if (!container) return + const host = container.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + return host } @@ -28,6 +155,7 @@ export function applyViewerScheme(host: HTMLElement | undefined) { if (typeof document === "undefined") return const scheme = document.documentElement.dataset.colorScheme + if (scheme === "dark" || scheme === "light") { host.dataset.colorScheme = scheme return @@ -36,79 +164,110 @@ export function applyViewerScheme(host: HTMLElement | undefined) { host.removeAttribute("data-color-scheme") } -export function observeViewerScheme(getHost: () => HTMLElement | undefined) { +export function observeViewerScheme( + getHost: () => HTMLElement | undefined, +) { if (typeof document === "undefined") return () => {} applyViewerScheme(getHost()) + if (typeof MutationObserver === "undefined") return () => {} const root = document.documentElement - const monitor = new MutationObserver(() => applyViewerScheme(getHost())) - monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] }) + + const monitor = new MutationObserver(() => + applyViewerScheme(getHost()), + ) + + monitor.observe(root, { + attributes: true, + attributeFilter: ["data-color-scheme"], + }) + return () => monitor.disconnect() } -export function notifyShadowReady(opts: { - state: ReadyWatcher - container: HTMLElement - getRoot: () => ShadowRoot | undefined - isReady: (root: ShadowRoot) => boolean - onReady: () => void - settleFrames?: number -}) { - clearReadyWatcher(opts.state) - opts.state.token += 1 +function scheduleReady( + opts: ReadyOptions, + token: number, + settle: number, +) { + const step = (left: number) => { + if (token !== opts.state.token) return - const token = opts.state.token - const settle = Math.max(0, opts.settleFrames ?? 0) - - const runReady = () => { - const step = (left: number) => { - if (token !== opts.state.token) return - if (left <= 0) { - opts.onReady() - return - } - requestAnimationFrame(() => step(left - 1)) + if (left <= 0) { + opts.onReady() + return } - requestAnimationFrame(() => step(settle)) + requestAnimationFrame(() => step(left - 1)) } - const observeRoot = (root: ShadowRoot) => { - if (opts.isReady(root)) { - runReady() - return - } + requestAnimationFrame(() => step(settle)) +} + +function watchRoot( + opts: ReadyOptions, + root: ShadowRoot, + token: number, + settle: number, +) { + if (opts.isReady(root)) { + scheduleReady(opts, token, settle) + return + } + + if (typeof MutationObserver === "undefined") return - if (typeof MutationObserver === "undefined") return + clearReadyWatcher(opts.state) + + opts.state.observer = new MutationObserver(() => { + if (token !== opts.state.token) return + if (!opts.isReady(root)) return clearReadyWatcher(opts.state) - opts.state.observer = new MutationObserver(() => { - if (token !== opts.state.token) return - if (!opts.isReady(root)) return - - clearReadyWatcher(opts.state) - runReady() - }) - opts.state.observer.observe(root, { childList: true, subtree: true }) - } + scheduleReady(opts, token, settle) + }) - const root = opts.getRoot() - if (!root) { - if (typeof MutationObserver === "undefined") return + opts.state.observer.observe(root, { + childList: true, + subtree: true, + }) +} - opts.state.observer = new MutationObserver(() => { - if (token !== opts.state.token) return +function watchForRoot( + opts: ReadyOptions, + token: number, + settle: number, +) { + if (typeof MutationObserver === "undefined") return - const next = opts.getRoot() - if (!next) return + opts.state.observer = new MutationObserver(() => { + if (token !== opts.state.token) return - observeRoot(next) - }) - opts.state.observer.observe(opts.container, { childList: true, subtree: true }) - return - } + const root = opts.getRoot() + if (!root) return + + watchRoot(opts, root, token, settle) + }) - observeRoot(root) + opts.state.observer.observe(opts.container, { + childList: true, + subtree: true, + }) } + +export function notifyShadowReady(opts: ReadyOptions) { + clearReadyWatcher(opts.state) + opts.state.token += 1 + + const token = opts.state.token + const settle = Math.max(0, opts.settleFrames ?? 0) + const root = opts.getRoot() + + if (root) { + watchRoot(opts, root, token, settle) + } else { + watchForRoot(opts, token, settle) + } +} \ No newline at end of file