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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { type Clock, createConnection } from '@serverless-ircd/irc-core';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { AwsRuntime } from '../src/aws-runtime.js';
|
|
6
|
+
import type { AwsRuntimeHandlers } from '../src/aws-runtime.js';
|
|
7
|
+
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
8
|
+
import { createDynamoDocumentClient } from '../src/dynamo.js';
|
|
9
|
+
import { AwsStats } from '../src/stats.js';
|
|
10
|
+
import type { TablesConfig } from '../src/tables.js';
|
|
11
|
+
|
|
12
|
+
const available = process.env.DDB_AVAILABLE === '1';
|
|
13
|
+
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
14
|
+
|
|
15
|
+
interface Fixture {
|
|
16
|
+
tables: TablesConfig;
|
|
17
|
+
client: ReturnType<typeof createDynamoDocumentClient>;
|
|
18
|
+
cleanup: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let fx: Fixture;
|
|
22
|
+
|
|
23
|
+
async function makeFixture(): Promise<Fixture> {
|
|
24
|
+
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
25
|
+
const prefix = `t${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
26
|
+
const tables = Object.fromEntries(
|
|
27
|
+
Object.keys(TABLE_DEFS).map((name) => [name, `${prefix}${name}`]),
|
|
28
|
+
) as TablesConfig;
|
|
29
|
+
const client = createDynamoDocumentClient({ endpoint });
|
|
30
|
+
for (const [logical, props] of Object.entries(TABLE_DEFS)) {
|
|
31
|
+
const pkName = props.partitionKey?.name;
|
|
32
|
+
const skName = props.sortKey?.name;
|
|
33
|
+
if (pkName === undefined) throw new Error(`table ${logical} missing partitionKey`);
|
|
34
|
+
const attributeDefinitions: Array<{ AttributeName: string; AttributeType: 'S' }> = [
|
|
35
|
+
{ AttributeName: pkName, AttributeType: 'S' },
|
|
36
|
+
];
|
|
37
|
+
const keySchema: Array<{ AttributeName: string; KeyType: 'HASH' | 'RANGE' }> = [
|
|
38
|
+
{ AttributeName: pkName, KeyType: 'HASH' },
|
|
39
|
+
];
|
|
40
|
+
if (skName !== undefined) {
|
|
41
|
+
attributeDefinitions.push({ AttributeName: skName, AttributeType: 'S' });
|
|
42
|
+
keySchema.push({ AttributeName: skName, KeyType: 'RANGE' });
|
|
43
|
+
}
|
|
44
|
+
await client.send(
|
|
45
|
+
new CreateTableCommand({
|
|
46
|
+
TableName: `${prefix}${logical}`,
|
|
47
|
+
AttributeDefinitions: attributeDefinitions,
|
|
48
|
+
KeySchema: keySchema,
|
|
49
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
tables,
|
|
55
|
+
client,
|
|
56
|
+
cleanup: async () => {
|
|
57
|
+
for (const logical of Object.keys(TABLE_DEFS)) {
|
|
58
|
+
await client.send(new DeleteTableCommand({ TableName: `${prefix}${logical}` }));
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function noopHandlers(): AwsRuntimeHandlers {
|
|
65
|
+
return {
|
|
66
|
+
send: () => {},
|
|
67
|
+
disconnect: () => {},
|
|
68
|
+
snapshot: () => undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function makeRuntime(connId: string, clockFn: () => number = () => 1_000): AwsRuntime {
|
|
73
|
+
const clock: Clock = { now: clockFn };
|
|
74
|
+
return new AwsRuntime({
|
|
75
|
+
dynamo: fx.client,
|
|
76
|
+
tables: fx.tables,
|
|
77
|
+
connId,
|
|
78
|
+
handlers: noopHandlers(),
|
|
79
|
+
managementApi: null,
|
|
80
|
+
clock,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe.skipIf(!available)('AwsStats', () => {
|
|
85
|
+
beforeEach(async () => {
|
|
86
|
+
fx = await makeFixture();
|
|
87
|
+
});
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
if (fx) await fx.cleanup();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns zero counts for an empty deployment', async () => {
|
|
93
|
+
const stats = new AwsStats({
|
|
94
|
+
dynamo: fx.client,
|
|
95
|
+
tables: fx.tables,
|
|
96
|
+
uptimeStartedAt: 1_000,
|
|
97
|
+
});
|
|
98
|
+
const snap = await stats.getStats();
|
|
99
|
+
expect(snap.users).toBe(0);
|
|
100
|
+
expect(snap.channels).toBe(0);
|
|
101
|
+
expect(snap.servers).toBe(1);
|
|
102
|
+
expect(snap.localConns).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('counts registered connections (visible, invisible, opers)', async () => {
|
|
106
|
+
const rt = makeRuntime('self');
|
|
107
|
+
const a = createConnection({ id: 'c1', connectedSince: 0 });
|
|
108
|
+
a.nick = 'alice';
|
|
109
|
+
a.registration = 'registered';
|
|
110
|
+
const b = createConnection({ id: 'c2', connectedSince: 0 });
|
|
111
|
+
b.nick = 'bob';
|
|
112
|
+
b.registration = 'registered';
|
|
113
|
+
b.userModes.invisible = true;
|
|
114
|
+
const c = createConnection({ id: 'c3', connectedSince: 0 });
|
|
115
|
+
c.nick = 'carol';
|
|
116
|
+
c.registration = 'registered';
|
|
117
|
+
c.userModes.oper = true;
|
|
118
|
+
const d = createConnection({ id: 'c4', connectedSince: 0 });
|
|
119
|
+
d.registration = 'pre-registration';
|
|
120
|
+
await rt.persistConnectionState(a);
|
|
121
|
+
await rt.persistConnectionState(b);
|
|
122
|
+
await rt.persistConnectionState(c);
|
|
123
|
+
await rt.persistConnectionState(d);
|
|
124
|
+
|
|
125
|
+
const stats = new AwsStats({
|
|
126
|
+
dynamo: fx.client,
|
|
127
|
+
tables: fx.tables,
|
|
128
|
+
uptimeStartedAt: 1_000,
|
|
129
|
+
});
|
|
130
|
+
const snap = await stats.getStats();
|
|
131
|
+
// 3 registered (2 visible + 1 invisible), 1 oper, 1 unknown, 4 total.
|
|
132
|
+
expect(snap.users).toBe(2);
|
|
133
|
+
expect(snap.invisible).toBe(1);
|
|
134
|
+
expect(snap.opers).toBe(1);
|
|
135
|
+
expect(snap.unknownConnections).toBe(1);
|
|
136
|
+
expect(snap.localConns).toBe(4);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('counts channels via the ChannelMeta table', async () => {
|
|
140
|
+
const rt = makeRuntime('self');
|
|
141
|
+
await rt.applyChannelDelta('#a', {
|
|
142
|
+
memberships: [{ type: 'add', conn: 'c1', nick: 'Alice' }],
|
|
143
|
+
});
|
|
144
|
+
await rt.applyChannelDelta('#b', {
|
|
145
|
+
memberships: [{ type: 'add', conn: 'c1', nick: 'Alice' }],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const stats = new AwsStats({
|
|
149
|
+
dynamo: fx.client,
|
|
150
|
+
tables: fx.tables,
|
|
151
|
+
uptimeStartedAt: 1_000,
|
|
152
|
+
});
|
|
153
|
+
const snap = await stats.getStats();
|
|
154
|
+
expect(snap.channels).toBe(2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('echoes the supplied uptimeStartedAt', async () => {
|
|
158
|
+
const stats = new AwsStats({
|
|
159
|
+
dynamo: fx.client,
|
|
160
|
+
tables: fx.tables,
|
|
161
|
+
uptimeStartedAt: 7_777,
|
|
162
|
+
});
|
|
163
|
+
const snap = await stats.getStats();
|
|
164
|
+
expect(snap.uptimeStartedAt).toBe(7_777);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('honours supplied maxLocalConns / maxGlobalConns overrides', async () => {
|
|
168
|
+
const rt = makeRuntime('self');
|
|
169
|
+
const a = createConnection({ id: 'c1', connectedSince: 0 });
|
|
170
|
+
a.nick = 'alice';
|
|
171
|
+
a.registration = 'registered';
|
|
172
|
+
await rt.persistConnectionState(a);
|
|
173
|
+
|
|
174
|
+
const stats = new AwsStats({
|
|
175
|
+
dynamo: fx.client,
|
|
176
|
+
tables: fx.tables,
|
|
177
|
+
uptimeStartedAt: 1_000,
|
|
178
|
+
maxLocalConns: 99,
|
|
179
|
+
maxGlobalConns: 88,
|
|
180
|
+
});
|
|
181
|
+
const snap = await stats.getStats();
|
|
182
|
+
expect(snap.maxLocalConns).toBe(99);
|
|
183
|
+
expect(snap.maxGlobalConns).toBe(88);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Mock-based unit tests — exercise the defensive Scan fallbacks without
|
|
189
|
+
// requiring DynamoDB Local.
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Builds a stub DocumentClient whose `send` returns canned Scan output.
|
|
194
|
+
* Exercises the `?? []` and `?? 0` fallbacks in {@link AwsStats.getStats}
|
|
195
|
+
* that the DynamoDB-Local integration suite cannot reach (the SDK always
|
|
196
|
+
* returns defined arrays / counts there).
|
|
197
|
+
*
|
|
198
|
+
* Pass `connectionsItems: undefined` to simulate the SDK omitting the
|
|
199
|
+
* `Items` field entirely (the `?? []` fallback path).
|
|
200
|
+
*/
|
|
201
|
+
function stubDynamo(
|
|
202
|
+
connectionsItems: unknown[] | undefined,
|
|
203
|
+
channelCount: number | undefined,
|
|
204
|
+
): DynamoDBDocumentClient {
|
|
205
|
+
const calls: { TableName: string }[] = [];
|
|
206
|
+
const stub = {
|
|
207
|
+
send: async (cmd: {
|
|
208
|
+
input?: { TableName?: string; Select?: string };
|
|
209
|
+
TableName?: string;
|
|
210
|
+
Select?: string;
|
|
211
|
+
}) => {
|
|
212
|
+
// ScanCommand exposes its input via `.input`; both shapes are tolerated.
|
|
213
|
+
const input = (cmd as { input?: { TableName?: string; Select?: string } }).input ?? cmd;
|
|
214
|
+
const tableName = input.TableName ?? '';
|
|
215
|
+
const select = input.Select;
|
|
216
|
+
calls.push({ TableName: tableName });
|
|
217
|
+
void calls;
|
|
218
|
+
// Heuristic: the Connections scan omits `Select`, the ChannelMeta
|
|
219
|
+
// scan sets `Select: 'COUNT'`.
|
|
220
|
+
if (select === 'COUNT') {
|
|
221
|
+
return { Count: channelCount };
|
|
222
|
+
}
|
|
223
|
+
// Returning `{}` (no Items key) exercises the `?? []` fallback.
|
|
224
|
+
return connectionsItems === undefined ? {} : { Items: connectionsItems };
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
// Expose calls for any future assertions.
|
|
228
|
+
(stub as { _calls?: unknown })._calls = calls;
|
|
229
|
+
return stub as unknown as DynamoDBDocumentClient;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
describe('AwsStats — defensive Scan fallbacks (mocked client)', () => {
|
|
233
|
+
// These tests do NOT require DynamoDB Local — they exercise the
|
|
234
|
+
// `?? []` / `?? 0` fallbacks by feeding a stub client.
|
|
235
|
+
it('returns empty arrays when the Connections Scan yields no Items field', async () => {
|
|
236
|
+
const tables: TablesConfig = {
|
|
237
|
+
Connections: 'Connections',
|
|
238
|
+
ChannelMeta: 'ChannelMeta',
|
|
239
|
+
ChannelMembers: 'ChannelMembers',
|
|
240
|
+
Nicks: 'Nicks',
|
|
241
|
+
Accounts: 'Accounts',
|
|
242
|
+
};
|
|
243
|
+
// Pass `undefined` so the stub returns `{}` — no Items key — to
|
|
244
|
+
// exercise the `?? []` fallback on the Connections scan, and
|
|
245
|
+
// `undefined` for Count to exercise the `?? 0` fallback.
|
|
246
|
+
const dynamo = stubDynamo(undefined, undefined);
|
|
247
|
+
const stats = new AwsStats({ dynamo, tables, uptimeStartedAt: 1_000 });
|
|
248
|
+
const snap = await stats.getStats();
|
|
249
|
+
expect(snap.users).toBe(0);
|
|
250
|
+
expect(snap.unknownConnections).toBe(0);
|
|
251
|
+
expect(snap.channels).toBe(0);
|
|
252
|
+
expect(snap.localConns).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('honours maxLocalConns / maxGlobalConns overrides via the stub client', async () => {
|
|
256
|
+
const tables: TablesConfig = {
|
|
257
|
+
Connections: 'Connections',
|
|
258
|
+
ChannelMeta: 'ChannelMeta',
|
|
259
|
+
ChannelMembers: 'ChannelMembers',
|
|
260
|
+
Nicks: 'Nicks',
|
|
261
|
+
Accounts: 'Accounts',
|
|
262
|
+
};
|
|
263
|
+
const dynamo = stubDynamo([], 0);
|
|
264
|
+
const stats = new AwsStats({
|
|
265
|
+
dynamo,
|
|
266
|
+
tables,
|
|
267
|
+
uptimeStartedAt: 1_000,
|
|
268
|
+
maxLocalConns: 42,
|
|
269
|
+
maxGlobalConns: 43,
|
|
270
|
+
});
|
|
271
|
+
const snap = await stats.getStats();
|
|
272
|
+
expect(snap.maxLocalConns).toBe(42);
|
|
273
|
+
expect(snap.maxGlobalConns).toBe(43);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('counts connections returned by the stub client', async () => {
|
|
277
|
+
const tables: TablesConfig = {
|
|
278
|
+
Connections: 'Connections',
|
|
279
|
+
ChannelMeta: 'ChannelMeta',
|
|
280
|
+
ChannelMembers: 'ChannelMembers',
|
|
281
|
+
Nicks: 'Nicks',
|
|
282
|
+
Accounts: 'Accounts',
|
|
283
|
+
};
|
|
284
|
+
// Two registered connection rows.
|
|
285
|
+
const items = [
|
|
286
|
+
{
|
|
287
|
+
connectionId: 'c1',
|
|
288
|
+
registration: 'registered',
|
|
289
|
+
capNegotiating: false,
|
|
290
|
+
caps: [],
|
|
291
|
+
userModes: { invisible: false, oper: false, wallops: false, serverNotices: false },
|
|
292
|
+
lastSeen: 0,
|
|
293
|
+
connectedSince: 0,
|
|
294
|
+
idleSince: 0,
|
|
295
|
+
version: 1,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
connectionId: 'c2',
|
|
299
|
+
registration: 'registered',
|
|
300
|
+
capNegotiating: false,
|
|
301
|
+
caps: [],
|
|
302
|
+
userModes: { invisible: true, oper: false, wallops: false, serverNotices: false },
|
|
303
|
+
lastSeen: 0,
|
|
304
|
+
connectedSince: 0,
|
|
305
|
+
idleSince: 0,
|
|
306
|
+
version: 1,
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
const dynamo = stubDynamo(items, 3);
|
|
310
|
+
const stats = new AwsStats({ dynamo, tables, uptimeStartedAt: 1_000 });
|
|
311
|
+
const snap = await stats.getStats();
|
|
312
|
+
expect(snap.users).toBe(1); // 2 registered − 1 invisible
|
|
313
|
+
expect(snap.invisible).toBe(1);
|
|
314
|
+
expect(snap.channels).toBe(3);
|
|
315
|
+
expect(snap.localConns).toBe(2);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/cf-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./accounts": {
|
|
16
|
+
"types": "./dist/d1-account-store.d.ts",
|
|
17
|
+
"import": "./dist/d1-account-store.js"
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"scripts": {
|
|
@@ -31,6 +35,7 @@
|
|
|
31
35
|
"@cloudflare/workers-types": "^5.20260714.1",
|
|
32
36
|
"@serverless-ircd/in-memory-runtime": "workspace:*",
|
|
33
37
|
"@serverless-ircd/irc-test-support": "workspace:*",
|
|
38
|
+
"@vitest/coverage-istanbul": "^4.1.0",
|
|
34
39
|
"@vitest/coverage-v8": "^4.1.0",
|
|
35
40
|
"rimraf": "^6.0.0",
|
|
36
41
|
"typescript": "^5.6.0",
|
|
@@ -35,9 +35,12 @@ import {
|
|
|
35
35
|
type Nick,
|
|
36
36
|
type RawLine,
|
|
37
37
|
type RosterEntry,
|
|
38
|
+
type ServerConfig,
|
|
38
39
|
toSnapshot,
|
|
39
40
|
} from '@serverless-ircd/irc-core';
|
|
40
41
|
import type { IrcRuntime } from '@serverless-ircd/irc-server';
|
|
42
|
+
import type { CfConfigEnv } from './config-loader.js';
|
|
43
|
+
import { loadServerConfigFromCfEnv } from './config-loader.js';
|
|
41
44
|
import type { ChannelRegistryRpc, Env, RegistryRpc } from './env.js';
|
|
42
45
|
import type { PersistedConnectionState } from './serialize.js';
|
|
43
46
|
import { deserialize } from './serialize.js';
|
|
@@ -144,6 +147,48 @@ export class CfRuntime implements IrcRuntime {
|
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Global cross-connection fan-out for `WALLOPS`. Enumerates every registry
|
|
152
|
+
* shard to collect all live connection ids, fetches each connection's
|
|
153
|
+
* state to honour the `+w` recipient gate, and delivers via the
|
|
154
|
+
* ConnectionDO `deliver` RPC. `except` (the originator) is skipped, as is
|
|
155
|
+
* the bound connection's own live state shortcut.
|
|
156
|
+
*
|
|
157
|
+
* Cost: this is N shard RPCs + one `getConnState` per online connection +
|
|
158
|
+
* one `deliver` per `+w` recipient. At very high connection counts the
|
|
159
|
+
* per-WALLOPS cost grows linearly with the fleet; deployments should bound
|
|
160
|
+
* the oper's WALLOPS rate (an oper flooding WALLOPS is a self-DoS). The
|
|
161
|
+
* shard walk is parallelised via `Promise.all` so wall-clock stays near one
|
|
162
|
+
* round-trip for the enumeration phase.
|
|
163
|
+
*/
|
|
164
|
+
async broadcastWallops(lines: RawLine[], except?: ConnId): Promise<void> {
|
|
165
|
+
// Phase 1 — enumerate every connId across all registry shards.
|
|
166
|
+
const shardEntries = await Promise.all(
|
|
167
|
+
Array.from({ length: this.registryShards }, (_, i) => this.registryShardRpc(i).listEntries()),
|
|
168
|
+
);
|
|
169
|
+
const seen = new Set<ConnId>();
|
|
170
|
+
for (const entries of shardEntries) {
|
|
171
|
+
for (const [, connId] of entries) {
|
|
172
|
+
seen.add(connId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Phase 2 — fetch each candidate's state, filter +w, deliver.
|
|
177
|
+
// Bound the per-call fan-out by skipping the excepted/bound connection
|
|
178
|
+
// up front (the bound connection's own +w never needs a self-RPC).
|
|
179
|
+
await Promise.all(
|
|
180
|
+
Array.from(seen, async (connId) => {
|
|
181
|
+
if (connId === except || connId === this.connId) return;
|
|
182
|
+
const stub = this.env.CONNECTION_DO.get(this.env.CONNECTION_DO.idFromString(connId));
|
|
183
|
+
const dto = await (stub as unknown as ConnectionRpc).getConnState();
|
|
184
|
+
if (dto === null) return;
|
|
185
|
+
const state = deserialize(dto);
|
|
186
|
+
if (!state.userModes.wallops) return;
|
|
187
|
+
await (stub as unknown as ConnectionRpc).deliver(lines);
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
147
192
|
// -------------------------------------------------------------------------
|
|
148
193
|
// Nick registry
|
|
149
194
|
// -------------------------------------------------------------------------
|
|
@@ -265,6 +310,16 @@ export class CfRuntime implements IrcRuntime {
|
|
|
265
310
|
return snapshots.filter((s): s is ChanSnapshot => s !== null);
|
|
266
311
|
}
|
|
267
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Re-reads the bound Workers env (vars + secrets, the config source for
|
|
315
|
+
* Cloudflare deployments) and reparses it through the shared schema. The
|
|
316
|
+
* actor swaps its live config with the result; a rejection leaves the
|
|
317
|
+
* previous config in effect.
|
|
318
|
+
*/
|
|
319
|
+
async reloadConfig(): Promise<ServerConfig> {
|
|
320
|
+
return loadServerConfigFromCfEnv(this.env as unknown as CfConfigEnv);
|
|
321
|
+
}
|
|
322
|
+
|
|
268
323
|
// -------------------------------------------------------------------------
|
|
269
324
|
// Internal
|
|
270
325
|
// -------------------------------------------------------------------------
|
|
@@ -281,6 +336,16 @@ export class CfRuntime implements IrcRuntime {
|
|
|
281
336
|
return stub as unknown as RegistryRpc;
|
|
282
337
|
}
|
|
283
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Returns the registry DO RPC stub for shard index `i` (`0 <= i < shards`).
|
|
341
|
+
* Used by global fan-out paths (WALLOPS) that must enumerate every shard
|
|
342
|
+
* rather than address the one owning a specific nick.
|
|
343
|
+
*/
|
|
344
|
+
private registryShardRpc(i: number): RegistryRpc {
|
|
345
|
+
const stub = this.env.REGISTRY_DO.get(this.env.REGISTRY_DO.idFromName(`s${i}`));
|
|
346
|
+
return stub as unknown as RegistryRpc;
|
|
347
|
+
}
|
|
348
|
+
|
|
284
349
|
/** Returns the channel DO RPC stub for `name` (keyed by lowercased name). */
|
|
285
350
|
private channelRpc(name: ChanName): {
|
|
286
351
|
broadcast(lines: unknown[], except?: string): Promise<void>;
|
|
@@ -319,7 +384,7 @@ export function makeCfRuntime(
|
|
|
319
384
|
handlers: CfConnectionHandlers,
|
|
320
385
|
): IrcRuntime {
|
|
321
386
|
// `_registryKey` is ignored — the production CfRuntime always shards the
|
|
322
|
-
// registry by nick (the prior per-conn key was a
|
|
387
|
+
// registry by nick (the prior per-conn key was a scaffolding
|
|
323
388
|
// convention that the real RegistryDO does not use).
|
|
324
389
|
void _registryKey;
|
|
325
390
|
return new CfRuntime(env, connId, handlers);
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* RPC return-type note: snapshot reads return the JSON-friendly
|
|
31
31
|
* {@link ChannelSnapshotDto} (members/banMasks/pendingInvites as
|
|
32
32
|
* arrays) so they survive the DO RPC boundary without relying on
|
|
33
|
-
* structured-clone of `Map`/`Set`. The CfRuntime
|
|
33
|
+
* structured-clone of `Map`/`Set`. The CfRuntime
|
|
34
34
|
* rehydrates these into a full {@link ChanSnapshot} at the call site.
|
|
35
35
|
*/
|
|
36
36
|
|
|
@@ -89,7 +89,7 @@ export interface ConnectionDeliveryRpc {
|
|
|
89
89
|
* JSON-friendly snapshot DTO returned by `getChannelSnapshot`. Maps and
|
|
90
90
|
* Sets are flattened to arrays so the value survives the DO RPC
|
|
91
91
|
* boundary in both production and the test pool. The CfRuntime
|
|
92
|
-
*
|
|
92
|
+
* rehydrates this into a {@link ChanSnapshot} at the
|
|
93
93
|
* call site.
|
|
94
94
|
*/
|
|
95
95
|
export interface ChannelSnapshotDto {
|