serverless-ircd 0.4.0 → 0.6.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 +34 -2
- 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/CHANGELOG.md +435 -0
- package/README.md +206 -27
- package/apps/aws-stack/README.md +37 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +106 -13
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +49 -3
- 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/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.toml +5 -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 +138 -28
- package/apps/local-cli/tests/e2e.test.ts +113 -29
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
- package/biome.json +1 -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 +15 -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/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +69 -0
- package/packages/aws-adapter/src/handlers/connect.ts +36 -5
- package/packages/aws-adapter/src/handlers/default.ts +66 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +40 -2
- package/packages/aws-adapter/src/stats.ts +80 -0
- 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 +140 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +100 -4
- 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 +238 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +6 -1
- package/packages/cf-adapter/src/cf-runtime.ts +66 -1
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/connection-do.ts +185 -54
- package/packages/cf-adapter/src/env.ts +25 -6
- package/packages/cf-adapter/src/index.ts +2 -0
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +1 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
- package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +7 -7
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/cf-adapter/wrangler.test.toml +7 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +38 -4
- 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 +42 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/isupport.ts +59 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +12 -9
- 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/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- 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/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +72 -10
- package/packages/irc-core/src/effects.ts +41 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +568 -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 +57 -11
- package/packages/irc-core/src/protocol/outbound.ts +36 -4
- 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 +120 -1
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/away-store.test.ts +73 -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/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +129 -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/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -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/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -0
- 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 +90 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -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/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +699 -19
- package/packages/irc-server/src/dispatch.ts +109 -16
- package/packages/irc-server/src/routing.ts +3 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +10 -7
- package/packages/irc-server/tests/actor.test.ts +2523 -42
- package/packages/irc-server/tests/dispatch.test.ts +300 -2
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +1 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- 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 +9 -1
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/pnpm-workspace.yaml +1 -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/package.json +6 -1
- package/tools/seed-cf-accounts.ts +4 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parametrized IRC scenarios against the Cloudflare runtime
|
|
2
|
+
* Parametrized IRC scenarios against the Cloudflare runtime.
|
|
3
3
|
*
|
|
4
4
|
* Reuses the scenario runner from `@serverless-ircd/irc-test-support`
|
|
5
|
-
*
|
|
5
|
+
* and registers the CF harness factory built on top of the
|
|
6
6
|
* real ConnectionDO / RegistryDO / ChannelDO worker bindings. Each
|
|
7
7
|
* scenario spawns one or more real WebSockets and drives the full
|
|
8
8
|
* bytes → actor → CfRuntime pipeline inside `workerd`.
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* against the in-memory runtime; CF-specific failures stem from the
|
|
16
16
|
* ConnectionDO's `PassthroughChannelAccess` not yet fetching
|
|
17
17
|
* authoritative roster state from ChannelDO before running channel
|
|
18
|
-
* reducers. That wiring lands in a follow-up to keep this
|
|
18
|
+
* reducers. That wiring lands in a follow-up to keep this work
|
|
19
19
|
* focused on the CfRuntime class itself):
|
|
20
20
|
* - Multi-client channel reads (NAMES of peers, KICK roster refresh)
|
|
21
21
|
* see only the local connection until the actor's channel access
|
|
22
22
|
* is upgraded to read from ChannelDO.
|
|
23
23
|
* - ChannelDO.getChannelSnapshot has an unrelated bug in the parallel
|
|
24
|
-
* ChannelDO
|
|
24
|
+
* ChannelDO work that returns a malformed snapshot;
|
|
25
25
|
* that bug is not introduced here.
|
|
26
26
|
*/
|
|
27
27
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CfRuntime
|
|
2
|
+
* CfRuntime.
|
|
3
3
|
*
|
|
4
4
|
* Verifies that every {@link IrcRuntime} method on {@link CfRuntime} issues
|
|
5
5
|
* the correct DO `stub()` call:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Two envs are used:
|
|
17
17
|
* - `env` — production bindings (recording stubs for the
|
|
18
|
-
*
|
|
18
|
+
* ConnectionDO unit suite; swapped to `_REAL` for
|
|
19
19
|
* the CfRuntime uniqueness + end-to-end tests).
|
|
20
20
|
* - `envWithRealDOs()` — a shim that re-exposes the real RegistryDO +
|
|
21
21
|
* ChannelDO under the production binding names,
|
|
@@ -616,3 +616,308 @@ describe('CfRuntime — IrcRuntime structural conformance', () => {
|
|
|
616
616
|
expect(handlers.sent).toEqual([':server 001 alice :Welcome']);
|
|
617
617
|
});
|
|
618
618
|
});
|
|
619
|
+
|
|
620
|
+
describe('CfRuntime — reloadConfig (REHASH)', () => {
|
|
621
|
+
it('re-reads the bound Workers env and reparses the server config', async () => {
|
|
622
|
+
const realEnv = envWithRealDOs();
|
|
623
|
+
const rt = new CfRuntime(realEnv, 'conn-rehash', recordingHandlers());
|
|
624
|
+
|
|
625
|
+
const cfg = await rt.reloadConfig();
|
|
626
|
+
|
|
627
|
+
expect(cfg.serverName).toBe('irc.example.com');
|
|
628
|
+
expect(cfg.networkName).toBe('ExampleNet');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('reflects a rotated env value on the next reload (no caching)', async () => {
|
|
632
|
+
const rotatingEnv = {
|
|
633
|
+
...envWithRealDOs(),
|
|
634
|
+
OPER_USER: 'admin',
|
|
635
|
+
OPER_PASSWORD: 'old',
|
|
636
|
+
};
|
|
637
|
+
const rt = new CfRuntime(rotatingEnv, 'conn-rehash-2', recordingHandlers());
|
|
638
|
+
|
|
639
|
+
const first = await rt.reloadConfig();
|
|
640
|
+
expect(first.operCreds).toEqual([{ user: 'admin', password: 'old' }]);
|
|
641
|
+
|
|
642
|
+
// Rotate the env (simulate a re-deployed secret) and reload again.
|
|
643
|
+
rotatingEnv.OPER_PASSWORD = 'rotated';
|
|
644
|
+
const second = await rt.reloadConfig();
|
|
645
|
+
expect(second.operCreds).toEqual([{ user: 'admin', password: 'rotated' }]);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('propagates a schema-validation failure so the actor applies the graceful 382', async () => {
|
|
649
|
+
const badEnv: Env = { ...envWithRealDOs(), NETWORK_NAME: '' };
|
|
650
|
+
const rt = new CfRuntime(badEnv, 'conn-rehash-bad', recordingHandlers());
|
|
651
|
+
|
|
652
|
+
await expect(rt.reloadConfig()).rejects.toThrow(/networkName/u);
|
|
653
|
+
});
|
|
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
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChannelDO — roster + modes + fanout via ConnectionDO stubs.
|
|
3
3
|
*
|
|
4
|
-
* TDD outline
|
|
4
|
+
* TDD outline:
|
|
5
5
|
* "Red on 'two members of a channel both receive PRIVMSG'; green;
|
|
6
6
|
* then gone-connection sweep."
|
|
7
7
|
*
|
|
@@ -452,7 +452,7 @@ describe('ChannelDO — instance keying', () => {
|
|
|
452
452
|
});
|
|
453
453
|
|
|
454
454
|
// ---------------------------------------------------------------------------
|
|
455
|
-
// Scale smoke test — exercises the 1k-member ceiling
|
|
455
|
+
// Scale smoke test — exercises the 1k-member ceiling.
|
|
456
456
|
// ---------------------------------------------------------------------------
|
|
457
457
|
|
|
458
458
|
describe('ChannelDO — scale', () => {
|
|
@@ -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
|
+
});
|
|
@@ -169,3 +169,52 @@ describe('loadServerConfigFromCfEnv — failure modes', () => {
|
|
|
169
169
|
}
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// SASL_ACCOUNTS env parsing — malformed entries are skipped, valid ones kept.
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
describe('loadServerConfigFromCfEnv — SASL_ACCOUNTS parsing', () => {
|
|
178
|
+
it('skips blank lines within the seed', () => {
|
|
179
|
+
const cfg = loadServerConfigFromCfEnv({
|
|
180
|
+
...baseEnv(),
|
|
181
|
+
SASL_ACCOUNTS: 'alice:secret\n\nbob:password\n \n',
|
|
182
|
+
});
|
|
183
|
+
expect(cfg.saslAccounts).toEqual([
|
|
184
|
+
{ username: 'alice', password: 'secret' },
|
|
185
|
+
{ username: 'bob', password: 'password' },
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('skips lines with no colon separator', () => {
|
|
190
|
+
const cfg = loadServerConfigFromCfEnv({
|
|
191
|
+
...baseEnv(),
|
|
192
|
+
SASL_ACCOUNTS: 'alice:secret\nnocolonhere\nbob:password',
|
|
193
|
+
});
|
|
194
|
+
expect(cfg.saslAccounts).toHaveLength(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('skips lines where the colon is at position 0 (empty username)', () => {
|
|
198
|
+
const cfg = loadServerConfigFromCfEnv({
|
|
199
|
+
...baseEnv(),
|
|
200
|
+
SASL_ACCOUNTS: ':secret\nalice:valid',
|
|
201
|
+
});
|
|
202
|
+
expect(cfg.saslAccounts).toEqual([{ username: 'alice', password: 'valid' }]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('skips lines with an empty password after the colon', () => {
|
|
206
|
+
const cfg = loadServerConfigFromCfEnv({
|
|
207
|
+
...baseEnv(),
|
|
208
|
+
SASL_ACCOUNTS: 'alice:\nbob:valid',
|
|
209
|
+
});
|
|
210
|
+
expect(cfg.saslAccounts).toEqual([{ username: 'bob', password: 'valid' }]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('passes CREATED_AT verbatim when it overflows Number precision', () => {
|
|
214
|
+
// A digit string so large that Number() produces Infinity — the
|
|
215
|
+
// parser must fall back to the raw string rather than emit Infinity.
|
|
216
|
+
const huge = '9'.repeat(400);
|
|
217
|
+
const cfg = loadServerConfigFromCfEnv({ ...baseEnv(), CREATED_AT: huge });
|
|
218
|
+
expect(cfg.createdAt).toBe(huge);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CF `webSocketMessage` channel-registration: wire ChannelDO
|
|
3
3
|
* registration OR remove the dead loop.
|
|
4
4
|
*
|
|
5
|
-
* Spike conclusion
|
|
5
|
+
* Spike conclusion: the dead loop that
|
|
6
6
|
* iterated `state.joinedChannels` to "register" the ConnectionDO with
|
|
7
7
|
* each ChannelDO is redundant. Channel membership — and therefore
|
|
8
8
|
* broadcast fan-out registration — is driven solely by the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CF outbound batching: implement OR drop the reserved optimization.
|
|
3
3
|
*
|
|
4
|
-
* Spike conclusion
|
|
4
|
+
* Spike conclusion: the speculative per-microtask
|
|
5
5
|
* outbound drain that was reserved as a commented-out field in
|
|
6
6
|
* `ConnectionDO` is dropped. Per-call batching is already implemented —
|
|
7
7
|
* `ConnectionDO.deliver()` joins every line of a ChannelDO broadcast into
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure exported helpers from `connection-do.ts`.
|
|
3
|
+
*
|
|
4
|
+
* These functions are independent of Durable Object state, so they are
|
|
5
|
+
* unit-tested directly without the workerd/miniflare DO harness.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { parseSaslAccountsEnv, wsFrameModeFromTags } from '../src/connection-do';
|
|
10
|
+
|
|
11
|
+
describe('parseSaslAccountsEnv', () => {
|
|
12
|
+
it('returns an empty array for undefined input', () => {
|
|
13
|
+
expect(parseSaslAccountsEnv(undefined)).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns an empty array for an empty string', () => {
|
|
17
|
+
expect(parseSaslAccountsEnv('')).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('parses a single well-formed entry', () => {
|
|
21
|
+
expect(parseSaslAccountsEnv('alice:secret')).toEqual([
|
|
22
|
+
{ username: 'alice', password: 'secret' },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('parses multiple newline-delimited entries', () => {
|
|
27
|
+
expect(parseSaslAccountsEnv('alice:secret\nbob:pw')).toEqual([
|
|
28
|
+
{ username: 'alice', password: 'secret' },
|
|
29
|
+
{ username: 'bob', password: 'pw' },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('skips blank lines', () => {
|
|
34
|
+
expect(parseSaslAccountsEnv('alice:secret\n\nbob:pw\n \n')).toEqual([
|
|
35
|
+
{ username: 'alice', password: 'secret' },
|
|
36
|
+
{ username: 'bob', password: 'pw' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('skips lines with no colon separator', () => {
|
|
41
|
+
expect(parseSaslAccountsEnv('alice:secret\nnocolonhere\nbob:pw')).toEqual([
|
|
42
|
+
{ username: 'alice', password: 'secret' },
|
|
43
|
+
{ username: 'bob', password: 'pw' },
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('skips lines where the colon is at position 0 (empty username)', () => {
|
|
48
|
+
expect(parseSaslAccountsEnv(':secret\nalice:valid')).toEqual([
|
|
49
|
+
{ username: 'alice', password: 'valid' },
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('skips lines with an empty password after the colon', () => {
|
|
54
|
+
expect(parseSaslAccountsEnv('alice:\nbob:valid')).toEqual([
|
|
55
|
+
{ username: 'bob', password: 'valid' },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns an empty array when every line is malformed', () => {
|
|
60
|
+
expect(parseSaslAccountsEnv('\n \n:no-user\nuser:')).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('wsFrameModeFromTags', () => {
|
|
65
|
+
it("returns 'legacy' when no tags are present", () => {
|
|
66
|
+
expect(wsFrameModeFromTags([])).toBe('legacy');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns 'legacy' when no recognized tag matches", () => {
|
|
70
|
+
// Unknown tags (e.g. from an older deployment's tag scheme) fall
|
|
71
|
+
// back to the legacy-tolerant mode.
|
|
72
|
+
expect(wsFrameModeFromTags(['unknown:tag', 'another'])).toBe('legacy');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns 'spec-text' when the spec-text tag is present", () => {
|
|
76
|
+
expect(wsFrameModeFromTags(['ws:spec-text'])).toBe('spec-text');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns 'spec-binary' when the spec-binary tag is present", () => {
|
|
80
|
+
expect(wsFrameModeFromTags(['ws:spec-binary'])).toBe('spec-binary');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns 'legacy' when the legacy tag is present", () => {
|
|
84
|
+
expect(wsFrameModeFromTags(['ws:legacy'])).toBe('legacy');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('picks the first recognized tag when multiple are present', () => {
|
|
88
|
+
// The scan is order-sensitive; the first matching tag wins.
|
|
89
|
+
expect(wsFrameModeFromTags(['ws:spec-text', 'ws:legacy'])).toBe('spec-text');
|
|
90
|
+
});
|
|
91
|
+
});
|