serverless-ircd 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +40 -3
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +474 -0
- package/README.md +219 -22
- package/apps/aws-stack/README.md +33 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +74 -9
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +2 -2
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +59 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +9 -7
- package/apps/local-cli/tests/e2e.test.ts +112 -28
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +2 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +16 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/handlers/default.ts +3 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/serialize.ts +29 -1
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +9 -9
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +90 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/connection-do.ts +3 -0
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/src/caps/capabilities.ts +37 -3
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +34 -0
- package/packages/irc-core/src/commands/isupport.ts +53 -0
- package/packages/irc-core/src/commands/mode.ts +8 -1
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +65 -10
- package/packages/irc-core/src/effects.ts +14 -0
- package/packages/irc-core/src/ports.ts +389 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +15 -0
- package/packages/irc-core/src/protocol/outbound.ts +16 -1
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +112 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/mode.test.ts +72 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +450 -3
- package/packages/irc-server/src/dispatch.ts +108 -16
- package/packages/irc-server/tests/actor.test.ts +1432 -39
- package/packages/irc-server/tests/dispatch.test.ts +263 -2
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* store (record → query round-trip) without needing DynamoDB Local.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import type { StoredMessage } from '@serverless-ircd/irc-core';
|
|
11
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
11
12
|
import { describe, expect, it } from 'vitest';
|
|
12
13
|
import { bindMessageStore } from '../src/message-store.js';
|
|
13
14
|
|
|
@@ -26,7 +27,7 @@ function sampleMessage(chan: string, text: string): StoredMessage {
|
|
|
26
27
|
|
|
27
28
|
describe('bindMessageStore', () => {
|
|
28
29
|
it('returns a store that records and replays a message via LATEST', () => {
|
|
29
|
-
const store = bindMessageStore(
|
|
30
|
+
const store = bindMessageStore(makeTestServerConfig());
|
|
30
31
|
store.record(sampleMessage('#aws', 'hello aws'));
|
|
31
32
|
|
|
32
33
|
const result = store.query({ chan: '#aws', direction: 'latest', limit: 10 });
|
|
@@ -35,7 +36,7 @@ describe('bindMessageStore', () => {
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it('returns a store scoped per channel (no cross-channel leak)', () => {
|
|
38
|
-
const store = bindMessageStore(
|
|
39
|
+
const store = bindMessageStore(makeTestServerConfig());
|
|
39
40
|
store.record(sampleMessage('#a', 'in a'));
|
|
40
41
|
store.record(sampleMessage('#b', 'in b'));
|
|
41
42
|
|
|
@@ -45,8 +46,8 @@ describe('bindMessageStore', () => {
|
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
it('returns an independent store instance per call', () => {
|
|
48
|
-
const a = bindMessageStore(
|
|
49
|
-
const b = bindMessageStore(
|
|
49
|
+
const a = bindMessageStore(makeTestServerConfig());
|
|
50
|
+
const b = bindMessageStore(makeTestServerConfig());
|
|
50
51
|
a.record(sampleMessage('#aws', 'only in a'));
|
|
51
52
|
|
|
52
53
|
expect(a.query({ chan: '#aws', direction: 'latest', limit: 10 })).toHaveLength(1);
|
|
@@ -9,11 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
type ParsedServerConfig,
|
|
15
|
-
StaticMotdProvider,
|
|
16
|
-
} from '@serverless-ircd/irc-core';
|
|
12
|
+
import { type ParsedServerConfig, StaticMotdProvider } from '@serverless-ircd/irc-core';
|
|
13
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
17
14
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
18
15
|
import { AwsRuntime } from '../src/aws-runtime.js';
|
|
19
16
|
import type { PostToConnection } from '../src/aws-runtime.js';
|
|
@@ -30,12 +27,11 @@ import type { TablesConfig } from '../src/tables.js';
|
|
|
30
27
|
const available = process.env.DDB_AVAILABLE === '1';
|
|
31
28
|
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
32
29
|
|
|
33
|
-
const SERVER_CONFIG: ParsedServerConfig = {
|
|
34
|
-
...DEFAULT_SERVER_CONFIG,
|
|
30
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
35
31
|
serverName: 'irc.example.com',
|
|
36
32
|
networkName: 'ExampleNet',
|
|
37
33
|
motdLines: ['Welcome to the test IRC server.'],
|
|
38
|
-
};
|
|
34
|
+
});
|
|
39
35
|
|
|
40
36
|
const MOTD = new StaticMotdProvider(SERVER_CONFIG.motdLines);
|
|
41
37
|
|
|
@@ -461,6 +457,62 @@ describe.skipIf(!available)('NLB stream handler — streaming back', () => {
|
|
|
461
457
|
});
|
|
462
458
|
expect(bSink.some((l) => l.includes('PRIVMSG #room :hello from alice'))).toBe(true);
|
|
463
459
|
});
|
|
460
|
+
|
|
461
|
+
it('refreshChannel tolerates an empty channel snapshot without throwing', async () => {
|
|
462
|
+
// After the last member PARTs, the channel meta row persists but
|
|
463
|
+
// queryMembers returns no rows → snap.members.size === 0. The next
|
|
464
|
+
// frame referencing the channel triggers refreshChannel on the
|
|
465
|
+
// empty-members + existing-cache path. This test ensures that path
|
|
466
|
+
// does not throw and the channel remains usable for a re-JOIN.
|
|
467
|
+
const f = requireFx();
|
|
468
|
+
const opts = () => ({
|
|
469
|
+
dynamo: f.client,
|
|
470
|
+
tables: f.tables,
|
|
471
|
+
serverConfig: SERVER_CONFIG,
|
|
472
|
+
motd: MOTD,
|
|
473
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
474
|
+
managementApi: f.mgmt,
|
|
475
|
+
});
|
|
476
|
+
const a = deriveNlbConnectionId('10.0.0.40', '50040');
|
|
477
|
+
f.mgmt.register(a, () => {});
|
|
478
|
+
// Register + JOIN + PART → cache populated then snapshot empty.
|
|
479
|
+
await handleNlbStream(nlbEvent('10.0.0.40', '50040', ''), opts());
|
|
480
|
+
await handleNlbStream(
|
|
481
|
+
nlbEvent('10.0.0.40', '50040', 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #nlbempty\r\n'),
|
|
482
|
+
opts(),
|
|
483
|
+
);
|
|
484
|
+
await handleNlbStream(nlbEvent('10.0.0.40', '50040', 'PART #nlbempty\r\n'), opts());
|
|
485
|
+
// Re-JOIN hits refreshChannel on the empty-members path; must not throw.
|
|
486
|
+
const res = await handleNlbStream(nlbEvent('10.0.0.40', '50040', 'JOIN #nlbempty\r\n'), opts());
|
|
487
|
+
expect(res.statusCode).toBe(200);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('falls back to "unknown"/"0" when NLB headers are absent', async () => {
|
|
491
|
+
// `readHeader` returns undefined when no header key matches. The
|
|
492
|
+
// caller's `?? 'unknown'` / `?? '0'` fallbacks must apply so the
|
|
493
|
+
// handler does not crash on a malformed event (defensive).
|
|
494
|
+
const f = requireFx();
|
|
495
|
+
const eventWithoutHeaders: NlbStreamEvent = {
|
|
496
|
+
requestContext: {
|
|
497
|
+
elb: { targetGroupArn: 'arn:aws:elasticloadbalancing:::targetgroup/test' },
|
|
498
|
+
},
|
|
499
|
+
version: '2.0',
|
|
500
|
+
body: Buffer.from('PING :tok\r\n', 'utf8').toString('base64'),
|
|
501
|
+
isBase64Encoded: true,
|
|
502
|
+
headers: { 'some-other-header': 'value' },
|
|
503
|
+
};
|
|
504
|
+
const res = await handleNlbStream(eventWithoutHeaders, {
|
|
505
|
+
dynamo: f.client,
|
|
506
|
+
tables: f.tables,
|
|
507
|
+
serverConfig: SERVER_CONFIG,
|
|
508
|
+
motd: MOTD,
|
|
509
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
510
|
+
managementApi: f.mgmt,
|
|
511
|
+
});
|
|
512
|
+
expect(res.statusCode).toBe(200);
|
|
513
|
+
// The fallback id (nlb-unknown-0) still received a PONG.
|
|
514
|
+
expect(res.body).toBeDefined();
|
|
515
|
+
});
|
|
464
516
|
});
|
|
465
517
|
|
|
466
518
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/cf-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Cloudflare Workers adapter: Durable Objects (ConnectionDO, RegistryDO, ChannelDO) and CfRuntime implementing IrcRuntime",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@cloudflare/workers-types": "^5.20260714.1",
|
|
36
36
|
"@serverless-ircd/in-memory-runtime": "workspace:*",
|
|
37
37
|
"@serverless-ircd/irc-test-support": "workspace:*",
|
|
38
|
+
"@vitest/coverage-istanbul": "^4.1.0",
|
|
38
39
|
"@vitest/coverage-v8": "^4.1.0",
|
|
39
40
|
"rimraf": "^6.0.0",
|
|
40
41
|
"typescript": "^5.6.0",
|
|
@@ -466,6 +466,9 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
466
466
|
// deployment-wide uptime, tracked as future work.
|
|
467
467
|
stats: new CfStats(runtime, WORKER_STARTUP_AT),
|
|
468
468
|
transport: makeWsFrameTransport(mode),
|
|
469
|
+
// Cloudflare terminates TLS at the edge before the WebSocket reaches
|
|
470
|
+
// the DO, so every WS connection is secure → user mode `S`.
|
|
471
|
+
secure: true,
|
|
469
472
|
...(accounts !== undefined ? { accounts } : {}),
|
|
470
473
|
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
471
474
|
logger,
|
|
@@ -34,6 +34,13 @@ export interface PersistedConnectionState {
|
|
|
34
34
|
id: string;
|
|
35
35
|
registration: RegistrationState;
|
|
36
36
|
capNegotiating: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Transport TLS fact (user mode `S`). Optional on the persisted shape so
|
|
39
|
+
* records written before the field shipped (schema is otherwise
|
|
40
|
+
* unchanged) deserialize with a `false` default via the coalescing in
|
|
41
|
+
* {@link deserialize}. Always written by {@link serialize}.
|
|
42
|
+
*/
|
|
43
|
+
secure?: boolean;
|
|
37
44
|
caps: string[];
|
|
38
45
|
joinedChannels: ChanName[];
|
|
39
46
|
userModes: UserModes;
|
|
@@ -61,6 +68,7 @@ export function serialize(state: ConnectionState): PersistedConnectionState {
|
|
|
61
68
|
id: state.id,
|
|
62
69
|
registration: state.registration,
|
|
63
70
|
capNegotiating: state.capNegotiating,
|
|
71
|
+
secure: state.secure,
|
|
64
72
|
caps: [...state.caps],
|
|
65
73
|
joinedChannels: [...state.joinedChannels],
|
|
66
74
|
userModes: { ...state.userModes },
|
|
@@ -88,6 +96,9 @@ export function deserialize(record: unknown): ConnectionState {
|
|
|
88
96
|
id: migrated.id,
|
|
89
97
|
registration: migrated.registration,
|
|
90
98
|
capNegotiating: migrated.capNegotiating,
|
|
99
|
+
// `secure` is absent on records persisted before user mode `S` shipped;
|
|
100
|
+
// `migrate` backfills it to false so the cast below is truthful.
|
|
101
|
+
secure: migrated.secure ?? false,
|
|
91
102
|
caps: new Set<string>(migrated.caps),
|
|
92
103
|
joinedChannels: new Set<ChanName>(migrated.joinedChannels),
|
|
93
104
|
userModes: { ...migrated.userModes },
|
|
@@ -119,8 +130,19 @@ export function migrate(record: unknown): PersistedConnectionState {
|
|
|
119
130
|
if (!isPlainObject(record)) {
|
|
120
131
|
throw new Error('persisted connection state is not an object');
|
|
121
132
|
}
|
|
133
|
+
// Backfill additive fields introduced by user mode `S` (TLS connected):
|
|
134
|
+
// `secure` and `userModes.tls`. Both default to false (plain transport),
|
|
135
|
+
// preserving the pre-feature behaviour for records persisted before the
|
|
136
|
+
// mode shipped. The schema version is unchanged because the additions are
|
|
137
|
+
// purely additive with safe defaults — no row transformation is needed
|
|
138
|
+
// beyond coalescing the missing keys.
|
|
139
|
+
if (record.secure === undefined) record.secure = false;
|
|
140
|
+
if (isPlainObject(record.userModes)) {
|
|
141
|
+
const um = record.userModes as Record<string, unknown>;
|
|
142
|
+
if (um.tls === undefined) um.tls = false;
|
|
143
|
+
}
|
|
122
144
|
const version = typeof record.version === 'number' ? record.version : 0;
|
|
123
|
-
// No migrations yet — v1 is the initial schema. Future work
|
|
145
|
+
// No versioned migrations yet — v1 is the initial schema. Future work:
|
|
124
146
|
// if (version < 2) record = migrate_v1_v2(record);
|
|
125
147
|
// if (version < 3) record = migrate_v2_v3(record);
|
|
126
148
|
if (version > PERSISTED_STATE_VERSION) {
|
|
@@ -131,9 +153,8 @@ export function migrate(record: unknown): PersistedConnectionState {
|
|
|
131
153
|
}
|
|
132
154
|
// The cast below is safe: we accept any JSON shape the storage layer
|
|
133
155
|
// returns; production code wrote it via `serialize`, tests wrote it
|
|
134
|
-
// through the same path.
|
|
135
|
-
// `
|
|
136
|
-
// undefined keys out of the persisted record entirely.
|
|
156
|
+
// through the same path. Backfilling above keeps the additive
|
|
157
|
+
// `secure` / `userModes.tls` keys present even on pre-feature records.
|
|
137
158
|
return record as unknown as PersistedConnectionState;
|
|
138
159
|
}
|
|
139
160
|
|
|
@@ -119,7 +119,18 @@ class CfHarness implements IrcHarness {
|
|
|
119
119
|
if (opts?.nick !== undefined) {
|
|
120
120
|
await client.send(`NICK ${opts.nick}`);
|
|
121
121
|
await client.send(`USER ${opts.nick} 0 * :${opts.nick}`);
|
|
122
|
-
|
|
122
|
+
// Wait for the FULL registration block, not just 001. Unlike the
|
|
123
|
+
// in-memory runtime (where the whole welcome fires synchronously
|
|
124
|
+
// inside `send`), CF delivers the 001..376 lines asynchronously
|
|
125
|
+
// through the WebSocket. Returning on 001 leaves 002..376 in flight,
|
|
126
|
+
// which can pollute `received` after a test calls `received.length = 0`
|
|
127
|
+
// and trip "no numeric" assertions on slow/loaded runners. The
|
|
128
|
+
// MOTD tailer (376 RPL_ENDOFMOTD, or 422 ERR_NOMOTD when no MOTD is
|
|
129
|
+
// configured) is the last frame of the registration burst.
|
|
130
|
+
await client.waitForLine(
|
|
131
|
+
(l) => numericEquals(l, '376') || numericEquals(l, '422'),
|
|
132
|
+
DEFAULT_TIMEOUT_MS,
|
|
133
|
+
);
|
|
123
134
|
this.nicks.set(opts.nick.toLowerCase(), client);
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -652,3 +652,272 @@ describe('CfRuntime — reloadConfig (REHASH)', () => {
|
|
|
652
652
|
await expect(rt.reloadConfig()).rejects.toThrow(/networkName/u);
|
|
653
653
|
});
|
|
654
654
|
});
|
|
655
|
+
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
// Tests — broadcastWallops (global +w fan-out across every registry shard)
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Opens a ConnectionDO, registers it with the given nick, and optionally
|
|
662
|
+
* flips user mode `w` on via a direct state mutation (avoids driving the
|
|
663
|
+
* full MODE handshake through the actor). Returns the live WebSocket and
|
|
664
|
+
* the connection's canonical hex id.
|
|
665
|
+
*/
|
|
666
|
+
async function openWallopsCapableConn(
|
|
667
|
+
name: string,
|
|
668
|
+
opts: { wallops: boolean },
|
|
669
|
+
): Promise<{ ws: WebSocket; hexId: string; received: string[] }> {
|
|
670
|
+
const ws = await openConnection(name);
|
|
671
|
+
await new Promise<void>((r) => setTimeout(r, 20));
|
|
672
|
+
ws.send(`NICK ${name}\r\n`);
|
|
673
|
+
ws.send(`USER ${name} 0 * :${name}\r\n`);
|
|
674
|
+
// Drain the registration numerics so they don't interfere with the
|
|
675
|
+
// assertions on wallops delivery below.
|
|
676
|
+
await new Promise<void>((r) => setTimeout(r, 100));
|
|
677
|
+
|
|
678
|
+
const hexId = await getConnectionHexId(name);
|
|
679
|
+
const received = drainSocket(ws);
|
|
680
|
+
|
|
681
|
+
if (opts.wallops) {
|
|
682
|
+
// Flip the +w mode directly on the live cached state. The DO is the
|
|
683
|
+
// authority for this state; broadcastWallops reads it back via
|
|
684
|
+
// getConnState() so what we mutate here is exactly what the fan-out
|
|
685
|
+
// will see.
|
|
686
|
+
const stub = env.CONNECTION_DO.get(env.CONNECTION_DO.idFromString(hexId));
|
|
687
|
+
await runInDurableObject(stub, async (instance: unknown) => {
|
|
688
|
+
const doi = instance as {
|
|
689
|
+
__peekState?: () => Promise<
|
|
690
|
+
import('@serverless-ircd/irc-core').ConnectionState | undefined
|
|
691
|
+
>;
|
|
692
|
+
__pokeState?: (s: import('@serverless-ircd/irc-core').ConnectionState) => void;
|
|
693
|
+
};
|
|
694
|
+
const state = await doi.__peekState?.();
|
|
695
|
+
if (state !== undefined) state.userModes.wallops = true;
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Reserve the nick in the REAL registry under its hex id so
|
|
700
|
+
// broadcastWallops' shard walk finds it.
|
|
701
|
+
await reserveNickInRealRegistry(name, hexId);
|
|
702
|
+
|
|
703
|
+
return { ws, hexId, received };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
describe('CfRuntime — broadcastWallops', () => {
|
|
707
|
+
it('delivers to every +w connection enumerated across all registry shards', async () => {
|
|
708
|
+
const realEnv = envWithRealDOs();
|
|
709
|
+
|
|
710
|
+
const a = await openWallopsCapableConn('wallops-a', { wallops: true });
|
|
711
|
+
const b = await openWallopsCapableConn('wallops-b', { wallops: true });
|
|
712
|
+
// A connection without +w must not receive the wallops.
|
|
713
|
+
const plain = await openWallopsCapableConn('wallops-plain', { wallops: false });
|
|
714
|
+
|
|
715
|
+
const rt = new CfRuntime(realEnv, 'conn-wallops-sender', recordingHandlers());
|
|
716
|
+
await rt.broadcastWallops([{ text: ':oper!u@h WALLOPS :hi fleet' }]);
|
|
717
|
+
|
|
718
|
+
await new Promise<void>((resolve) => {
|
|
719
|
+
const deadline = Date.now() + 1500;
|
|
720
|
+
const tick = (): void => {
|
|
721
|
+
const aGot = a.received.some((l) => l.includes('hi fleet'));
|
|
722
|
+
const bGot = b.received.some((l) => l.includes('hi fleet'));
|
|
723
|
+
if (aGot && bGot && Date.now() > deadline - 1000) {
|
|
724
|
+
resolve();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (Date.now() > deadline) {
|
|
728
|
+
resolve();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
setTimeout(tick, 20);
|
|
732
|
+
};
|
|
733
|
+
tick();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
expect(a.received.some((l) => l.includes('hi fleet'))).toBe(true);
|
|
737
|
+
expect(b.received.some((l) => l.includes('hi fleet'))).toBe(true);
|
|
738
|
+
expect(plain.received.some((l) => l.includes('hi fleet'))).toBe(false);
|
|
739
|
+
|
|
740
|
+
a.ws.close();
|
|
741
|
+
b.ws.close();
|
|
742
|
+
plain.ws.close();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('skips the excepted originator', async () => {
|
|
746
|
+
const realEnv = envWithRealDOs();
|
|
747
|
+
const sender = await openWallopsCapableConn('wallops-except-sender', { wallops: true });
|
|
748
|
+
const other = await openWallopsCapableConn('wallops-except-other', { wallops: true });
|
|
749
|
+
|
|
750
|
+
const rt = new CfRuntime(realEnv, sender.hexId, recordingHandlers());
|
|
751
|
+
await rt.broadcastWallops([{ text: ':oper!u@h WALLOPS :except me' }], sender.hexId);
|
|
752
|
+
|
|
753
|
+
await new Promise<void>((resolve) => {
|
|
754
|
+
const deadline = Date.now() + 1000;
|
|
755
|
+
const tick = (): void => {
|
|
756
|
+
if (other.received.some((l) => l.includes('except me'))) {
|
|
757
|
+
resolve();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (Date.now() > deadline) {
|
|
761
|
+
resolve();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
setTimeout(tick, 20);
|
|
765
|
+
};
|
|
766
|
+
tick();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
expect(other.received.some((l) => l.includes('except me'))).toBe(true);
|
|
770
|
+
expect(sender.received.some((l) => l.includes('except me'))).toBe(false);
|
|
771
|
+
|
|
772
|
+
sender.ws.close();
|
|
773
|
+
other.ws.close();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('is a no-op when the registry has no entries', async () => {
|
|
777
|
+
const realEnv = envWithRealDOs();
|
|
778
|
+
const handlers = recordingHandlers();
|
|
779
|
+
const rt = new CfRuntime(realEnv, 'conn-wallops-empty', handlers);
|
|
780
|
+
// Fresh storage (afterEach reset); no nick reservations anywhere.
|
|
781
|
+
await expect(
|
|
782
|
+
rt.broadcastWallops([{ text: ':oper!u@h WALLOPS :nobody' }]),
|
|
783
|
+
).resolves.toBeUndefined();
|
|
784
|
+
expect(handlers.sent).toEqual([]);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('skips a connection whose persisted state is null without throwing', async () => {
|
|
788
|
+
// Reserve a nick pointing at a hex id whose ConnectionDO has no
|
|
789
|
+
// live state. `newUniqueId()` gives us a valid 64-hex DO id that
|
|
790
|
+
// is guaranteed not to collide with any connection we open here.
|
|
791
|
+
const realEnv = envWithRealDOs();
|
|
792
|
+
const ghostHex = env.CONNECTION_DO.newUniqueId().toString();
|
|
793
|
+
await reserveNickInRealRegistry('wallops-ghost', ghostHex);
|
|
794
|
+
|
|
795
|
+
const other = await openWallopsCapableConn('wallops-alongside-ghost', { wallops: true });
|
|
796
|
+
|
|
797
|
+
const rt = new CfRuntime(realEnv, 'conn-wallops-ghost-caller', recordingHandlers());
|
|
798
|
+
// Must not reject even though one enumerated conn reports null state.
|
|
799
|
+
await expect(
|
|
800
|
+
rt.broadcastWallops([{ text: ':oper!u@h WALLOPS :ghost friendly' }]),
|
|
801
|
+
).resolves.toBeUndefined();
|
|
802
|
+
|
|
803
|
+
await new Promise<void>((resolve) => {
|
|
804
|
+
const deadline = Date.now() + 1000;
|
|
805
|
+
const tick = (): void => {
|
|
806
|
+
if (other.received.some((l) => l.includes('ghost friendly'))) {
|
|
807
|
+
resolve();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (Date.now() > deadline) {
|
|
811
|
+
resolve();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
setTimeout(tick, 20);
|
|
815
|
+
};
|
|
816
|
+
tick();
|
|
817
|
+
});
|
|
818
|
+
expect(other.received.some((l) => l.includes('ghost friendly'))).toBe(true);
|
|
819
|
+
|
|
820
|
+
other.ws.close();
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
// Tests — changeNick shard routing + failure paths
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
|
|
828
|
+
describe('CfRuntime — changeNick shard routing', () => {
|
|
829
|
+
it('uses the single-shard RegistryDO.changeNick when old and new hash to the same shard', async () => {
|
|
830
|
+
// Pin registryShards to 1 so every nick routes to shard 0 — the
|
|
831
|
+
// same-shard branch in CfRuntime.changeNick is taken.
|
|
832
|
+
const realEnv = envWithRealDOs();
|
|
833
|
+
const rt = new CfRuntime(realEnv, 'conn-sameshard', recordingHandlers(), {
|
|
834
|
+
registryShards: 1,
|
|
835
|
+
});
|
|
836
|
+
expect(await rt.reserveNick('sn-old', 'conn-sameshard')).toEqual({ ok: true });
|
|
837
|
+
|
|
838
|
+
const ok = await rt.changeNick('conn-sameshard', 'sn-old', 'sn-new');
|
|
839
|
+
expect(ok).toBe(true);
|
|
840
|
+
|
|
841
|
+
// Look up under the same shard count the runtime used (1).
|
|
842
|
+
const key = registryKeyForNick('sn-new', 1);
|
|
843
|
+
const newOwner = (
|
|
844
|
+
(await env.REGISTRY_DO_REAL.get(
|
|
845
|
+
env.REGISTRY_DO_REAL.idFromName(key),
|
|
846
|
+
)) as unknown as RegistryRpc
|
|
847
|
+
).lookupNick('sn-new');
|
|
848
|
+
expect(await newOwner).toBe('conn-sameshard');
|
|
849
|
+
const oldKey = registryKeyForNick('sn-old', 1);
|
|
850
|
+
const oldOwner = (
|
|
851
|
+
(await env.REGISTRY_DO_REAL.get(
|
|
852
|
+
env.REGISTRY_DO_REAL.idFromName(oldKey),
|
|
853
|
+
)) as unknown as RegistryRpc
|
|
854
|
+
).lookupNick('sn-old');
|
|
855
|
+
expect(await oldOwner).toBeNull();
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('two-phase path returns false when the new nick is already taken cross-shard', async () => {
|
|
859
|
+
// Default shard count (32). Reserve `wn-taken` by another conn first,
|
|
860
|
+
// then attempt to changeNick to it from a different conn.
|
|
861
|
+
const realEnv = envWithRealDOs();
|
|
862
|
+
const rt = new CfRuntime(realEnv, 'conn-newowner', recordingHandlers());
|
|
863
|
+
expect(await rt.reserveNick('wn-current', 'conn-newowner')).toEqual({ ok: true });
|
|
864
|
+
expect(await rt.reserveNick('wn-taken', 'conn-someoneelse')).toEqual({ ok: true });
|
|
865
|
+
|
|
866
|
+
const ok = await rt.changeNick('conn-newowner', 'wn-current', 'wn-taken');
|
|
867
|
+
expect(ok).toBe(false);
|
|
868
|
+
// Old nick untouched because the reserve-new phase failed.
|
|
869
|
+
expect(await registryForNick(realEnv, 'wn-current').lookupNick('wn-current')).toBe(
|
|
870
|
+
'conn-newowner',
|
|
871
|
+
);
|
|
872
|
+
expect(await registryForNick(realEnv, 'wn-taken').lookupNick('wn-taken')).toBe(
|
|
873
|
+
'conn-someoneelse',
|
|
874
|
+
);
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// ---------------------------------------------------------------------------
|
|
879
|
+
// Tests — getConnection / getChannelConnections edge cases
|
|
880
|
+
// ---------------------------------------------------------------------------
|
|
881
|
+
|
|
882
|
+
describe('CfRuntime — getConnection and getChannelConnections branches', () => {
|
|
883
|
+
it('getConnection(this.connId) returns the live snapshot via handlers', async () => {
|
|
884
|
+
const stubEnv = envWithChannelStub();
|
|
885
|
+
const state = createConnection({ id: 'conn-getc-self', connectedSince: 0 });
|
|
886
|
+
state.nick = 'self';
|
|
887
|
+
const handlers: CfConnectionHandlers = {
|
|
888
|
+
send: (): void => {},
|
|
889
|
+
disconnect: (): void => {},
|
|
890
|
+
snapshot: () => state,
|
|
891
|
+
};
|
|
892
|
+
const rt = new CfRuntime(stubEnv, 'conn-getc-self', handlers);
|
|
893
|
+
const conn = await rt.getConnection('conn-getc-self');
|
|
894
|
+
expect(conn).not.toBeNull();
|
|
895
|
+
expect(conn?.nick).toBe('self');
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('getConnection(this.connId) returns null when handlers.snapshot is undefined', async () => {
|
|
899
|
+
const stubEnv = envWithChannelStub();
|
|
900
|
+
const rt = new CfRuntime(stubEnv, 'conn-getc-empty', recordingHandlers());
|
|
901
|
+
expect(await rt.getConnection('conn-getc-empty')).toBeNull();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('getChannelConnections returns every member that reports a persisted state', async () => {
|
|
905
|
+
// A channel roster with one real member; the ConnectionDO is the
|
|
906
|
+
// authority for each member's state. getChannelConnections fans out
|
|
907
|
+
// one getConnState RPC per member and collects the results.
|
|
908
|
+
const realEnv = envWithRealDOs();
|
|
909
|
+
const rt = new CfRuntime(realEnv, 'conn-gcc-caller', recordingHandlers());
|
|
910
|
+
|
|
911
|
+
const member = await openWallopsCapableConn('gcc-real', { wallops: false });
|
|
912
|
+
|
|
913
|
+
await rt.applyChannelDelta('#gcc', {
|
|
914
|
+
memberships: [{ type: 'add' as const, conn: member.hexId, nick: 'gcc-real' }],
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
const conns = await rt.getChannelConnections('#gcc');
|
|
918
|
+
expect(conns.size).toBe(1);
|
|
919
|
+
expect(conns.get(member.hexId)?.nick).toBe('gcc-real');
|
|
920
|
+
|
|
921
|
+
member.ws.close();
|
|
922
|
+
});
|
|
923
|
+
});
|
|
@@ -478,3 +478,120 @@ describe('ChannelDO — scale', () => {
|
|
|
478
478
|
expect(snapAfter?.members.length).toBe(0);
|
|
479
479
|
}, 15_000);
|
|
480
480
|
});
|
|
481
|
+
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
// Edge cases — empty-lines broadcast, cache reload, test hooks
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
describe('ChannelDO — broadcast + cache edge cases', () => {
|
|
487
|
+
it('broadcast is a no-op for an empty lines array', async () => {
|
|
488
|
+
const chan = '#empty-bcast';
|
|
489
|
+
await channel(chan.toLowerCase()).applyChannelDelta({
|
|
490
|
+
memberships: [{ type: 'add' as const, conn: 'ghost', nick: 'g' }],
|
|
491
|
+
});
|
|
492
|
+
// Must not reject and must not prune members (no fan-out attempted).
|
|
493
|
+
await expect(channel(chan.toLowerCase()).broadcast([])).resolves.toBeUndefined();
|
|
494
|
+
const snap = await channel(chan.toLowerCase()).getChannelSnapshot();
|
|
495
|
+
expect(snap?.members.length).toBe(1);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('reloads persisted state from storage after __resetCache (deserializeChannel)', async () => {
|
|
499
|
+
// Mutate the channel, drop the in-memory cache, then read again.
|
|
500
|
+
// The read must go through deserializeChannel to rebuild Map/Set
|
|
501
|
+
// fields from the JSON-friendly persisted shape.
|
|
502
|
+
const chan = '#cache-reload';
|
|
503
|
+
const lower = chan.toLowerCase();
|
|
504
|
+
await channel(lower).applyChannelDelta({
|
|
505
|
+
memberships: [{ type: 'add' as const, conn: 'c-1', nick: 'alice' }],
|
|
506
|
+
topic: { text: 'hello topic', setter: 'alice', setAt: 100 },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Drop the cache so the next read forces a storage reload.
|
|
510
|
+
await runInDurableObject(channelStub(lower), (instance: unknown) => {
|
|
511
|
+
const doi = instance as { __resetCache?: () => void };
|
|
512
|
+
doi.__resetCache?.();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const snap = await channel(lower).getChannelSnapshot();
|
|
516
|
+
expect(snap?.members.length).toBe(1);
|
|
517
|
+
expect(snap?.members[0]?.nick).toBe('alice');
|
|
518
|
+
expect(snap?.topic?.text).toBe('hello topic');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('__peekState returns the live cached state before any reload', async () => {
|
|
522
|
+
const lower = '#peek-state';
|
|
523
|
+
await channel(lower).applyChannelDelta({
|
|
524
|
+
memberships: [{ type: 'add' as const, conn: 'c-peek', nick: 'peeker' }],
|
|
525
|
+
});
|
|
526
|
+
const cached = await runInDurableObject(channelStub(lower), async (instance: unknown) => {
|
|
527
|
+
const doi = instance as { __peekState?: () => Promise<unknown> };
|
|
528
|
+
return doi.__peekState?.();
|
|
529
|
+
});
|
|
530
|
+
expect(cached).toBeDefined();
|
|
531
|
+
// The cached state's members Map carries the entry we just added.
|
|
532
|
+
const members = (cached as { members: Map<string, unknown> }).members;
|
|
533
|
+
expect(members.get('c-peek')).toBeDefined();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('__resetCache clears the in-memory cache', async () => {
|
|
537
|
+
const lower = '#reset-cache';
|
|
538
|
+
await channel(lower).applyChannelDelta({
|
|
539
|
+
memberships: [{ type: 'add' as const, conn: 'c-reset', nick: 'resetter' }],
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await runInDurableObject(channelStub(lower), (instance: unknown) => {
|
|
543
|
+
const doi = instance as { __resetCache?: () => void };
|
|
544
|
+
doi.__resetCache?.();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const cached = await runInDurableObject(channelStub(lower), async (instance: unknown) => {
|
|
548
|
+
const doi = instance as { __peekState?: () => Promise<unknown> };
|
|
549
|
+
return doi.__peekState?.();
|
|
550
|
+
});
|
|
551
|
+
expect(cached).toBeUndefined();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('broadcast with all-live members does not prune the roster', async () => {
|
|
555
|
+
// When every member has an OPEN WebSocket, deliver returns > 0 for
|
|
556
|
+
// each and the `pruned.length > 0` else branch is taken (no prune
|
|
557
|
+
// delta applied).
|
|
558
|
+
const chan = '#no-prune';
|
|
559
|
+
const alice = await connectAndRegister('conn-noprune-alice', 'alice');
|
|
560
|
+
const bob = await connectAndRegister('conn-noprune-bob', 'bob');
|
|
561
|
+
|
|
562
|
+
await channel(chan.toLowerCase()).applyChannelDelta({
|
|
563
|
+
memberships: [
|
|
564
|
+
{ type: 'add', conn: alice.id, nick: 'alice' },
|
|
565
|
+
{ type: 'add', conn: bob.id, nick: 'bob' },
|
|
566
|
+
],
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
await channel(chan.toLowerCase()).broadcast(['PRIVMSG #no-prune :hello']);
|
|
570
|
+
await alice.client.waitForMessage((l) => l.startsWith('PRIVMSG #no-prune'));
|
|
571
|
+
await bob.client.waitForMessage((l) => l.startsWith('PRIVMSG #no-prune'));
|
|
572
|
+
|
|
573
|
+
// Both members survive — no prune occurred.
|
|
574
|
+
const snap = await channel(chan.toLowerCase()).getChannelSnapshot();
|
|
575
|
+
expect(snap?.members.length).toBe(2);
|
|
576
|
+
|
|
577
|
+
await alice.client.close();
|
|
578
|
+
await bob.client.close();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('sweep on a channel with all-live members prunes nobody', async () => {
|
|
582
|
+
const chan = '#sweep-clean';
|
|
583
|
+
const alice = await connectAndRegister('conn-sweep-uniq-001', 'sweepop');
|
|
584
|
+
|
|
585
|
+
await channel(chan.toLowerCase()).applyChannelDelta({
|
|
586
|
+
memberships: [{ type: 'add', conn: alice.id, nick: 'alice' }],
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const pruned = await channel(chan.toLowerCase()).sweep();
|
|
590
|
+
expect(pruned).toBe(0);
|
|
591
|
+
|
|
592
|
+
const snap = await channel(chan.toLowerCase()).getChannelSnapshot();
|
|
593
|
+
expect(snap?.members.length).toBe(1);
|
|
594
|
+
|
|
595
|
+
await alice.client.close();
|
|
596
|
+
});
|
|
597
|
+
});
|