serverless-ircd 0.1.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 +47 -0
- package/.github/workflows/deploy-cf.yml +97 -0
- package/LICENSE +29 -0
- package/README.md +323 -0
- package/apps/cf-worker/package.json +37 -0
- package/apps/cf-worker/scripts/smoke.mjs +175 -0
- package/apps/cf-worker/src/worker.ts +59 -0
- package/apps/cf-worker/tests/smoke.test.ts +150 -0
- package/apps/cf-worker/tsconfig.build.json +17 -0
- package/apps/cf-worker/tsconfig.test.json +18 -0
- package/apps/cf-worker/vitest.config.ts +31 -0
- package/apps/cf-worker/wrangler.test.toml +33 -0
- package/apps/cf-worker/wrangler.toml +98 -0
- package/apps/local-cli/package.json +35 -0
- package/apps/local-cli/src/line-scanner.ts +60 -0
- package/apps/local-cli/src/main.ts +145 -0
- package/apps/local-cli/src/motd-file.ts +45 -0
- package/apps/local-cli/src/server.ts +409 -0
- package/apps/local-cli/tests/e2e.test.ts +346 -0
- package/apps/local-cli/tests/id-factory.test.ts +25 -0
- package/apps/local-cli/tests/line-scanner.test.ts +81 -0
- package/apps/local-cli/tests/motd-file.test.ts +71 -0
- package/apps/local-cli/tests/tcp.test.ts +358 -0
- package/apps/local-cli/tsconfig.build.json +17 -0
- package/apps/local-cli/tsconfig.test.json +15 -0
- package/apps/local-cli/vitest.config.ts +24 -0
- package/biome.json +52 -0
- package/package.json +35 -0
- package/packages/cf-adapter/package.json +38 -0
- package/packages/cf-adapter/src/cf-runtime.ts +308 -0
- package/packages/cf-adapter/src/channel-do.ts +374 -0
- package/packages/cf-adapter/src/connection-do.ts +542 -0
- package/packages/cf-adapter/src/env.ts +67 -0
- package/packages/cf-adapter/src/index.ts +38 -0
- package/packages/cf-adapter/src/registry-do.ts +130 -0
- package/packages/cf-adapter/src/serialize.ts +142 -0
- package/packages/cf-adapter/src/sharding.ts +101 -0
- package/packages/cf-adapter/tests/cf-harness.ts +264 -0
- package/packages/cf-adapter/tests/cf-integration.test.ts +73 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +527 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +480 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +329 -0
- package/packages/cf-adapter/tests/integration/harness.test.ts +229 -0
- package/packages/cf-adapter/tests/integration/harness.ts +102 -0
- package/packages/cf-adapter/tests/integration/in-memory-harness.ts +242 -0
- package/packages/cf-adapter/tests/integration/index.ts +17 -0
- package/packages/cf-adapter/tests/integration/scenarios.test.ts +20 -0
- package/packages/cf-adapter/tests/integration/scenarios.ts +495 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +335 -0
- package/packages/cf-adapter/tests/sharding.test.ts +100 -0
- package/packages/cf-adapter/tests/worker/main.ts +33 -0
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +50 -0
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +57 -0
- package/packages/cf-adapter/tsconfig.build.json +16 -0
- package/packages/cf-adapter/tsconfig.test.json +18 -0
- package/packages/cf-adapter/vitest.config.ts +32 -0
- package/packages/cf-adapter/wrangler.test.toml +50 -0
- package/packages/in-memory-runtime/package.json +29 -0
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +245 -0
- package/packages/in-memory-runtime/src/index.ts +9 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +502 -0
- package/packages/in-memory-runtime/tsconfig.build.json +16 -0
- package/packages/in-memory-runtime/tsconfig.test.json +14 -0
- package/packages/in-memory-runtime/vitest.config.ts +21 -0
- package/packages/irc-core/package.json +29 -0
- package/packages/irc-core/src/caps/capabilities.ts +96 -0
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/commands/away.ts +82 -0
- package/packages/irc-core/src/commands/cap.ts +257 -0
- package/packages/irc-core/src/commands/chghost.ts +30 -0
- package/packages/irc-core/src/commands/index.ts +40 -0
- package/packages/irc-core/src/commands/invite.ts +156 -0
- package/packages/irc-core/src/commands/isupport.ts +133 -0
- package/packages/irc-core/src/commands/join.ts +309 -0
- package/packages/irc-core/src/commands/kick.ts +162 -0
- package/packages/irc-core/src/commands/list.ts +126 -0
- package/packages/irc-core/src/commands/mode.ts +655 -0
- package/packages/irc-core/src/commands/motd.ts +146 -0
- package/packages/irc-core/src/commands/names.ts +164 -0
- package/packages/irc-core/src/commands/part.ts +118 -0
- package/packages/irc-core/src/commands/ping.ts +47 -0
- package/packages/irc-core/src/commands/privmsg.ts +256 -0
- package/packages/irc-core/src/commands/quit.ts +70 -0
- package/packages/irc-core/src/commands/registration.ts +251 -0
- package/packages/irc-core/src/commands/topic.ts +171 -0
- package/packages/irc-core/src/commands/who.ts +169 -0
- package/packages/irc-core/src/commands/whois.ts +165 -0
- package/packages/irc-core/src/effects.ts +184 -0
- package/packages/irc-core/src/index.ts +20 -0
- package/packages/irc-core/src/ports.ts +153 -0
- package/packages/irc-core/src/protocol/batch.ts +85 -0
- package/packages/irc-core/src/protocol/index.ts +18 -0
- package/packages/irc-core/src/protocol/messages.ts +36 -0
- package/packages/irc-core/src/protocol/numerics.ts +155 -0
- package/packages/irc-core/src/protocol/outbound.ts +145 -0
- package/packages/irc-core/src/protocol/parser.ts +175 -0
- package/packages/irc-core/src/protocol/serializer.ts +71 -0
- package/packages/irc-core/src/state/channel.ts +293 -0
- package/packages/irc-core/src/state/connection.ts +153 -0
- package/packages/irc-core/src/state/index.ts +25 -0
- package/packages/irc-core/src/state/registry.ts +22 -0
- package/packages/irc-core/src/types.ts +83 -0
- package/packages/irc-core/tests/batch.test.ts +79 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +148 -0
- package/packages/irc-core/tests/commands/away.test.ts +233 -0
- package/packages/irc-core/tests/commands/batch.test.ts +178 -0
- package/packages/irc-core/tests/commands/cap.test.ts +499 -0
- package/packages/irc-core/tests/commands/chghost.test.ts +186 -0
- package/packages/irc-core/tests/commands/echo-message.test.ts +212 -0
- package/packages/irc-core/tests/commands/extended-join.test.ts +147 -0
- package/packages/irc-core/tests/commands/invite-notify.test.ts +160 -0
- package/packages/irc-core/tests/commands/invite.test.ts +321 -0
- package/packages/irc-core/tests/commands/isupport.test.ts +209 -0
- package/packages/irc-core/tests/commands/join.test.ts +687 -0
- package/packages/irc-core/tests/commands/kick.test.ts +316 -0
- package/packages/irc-core/tests/commands/list.test.ts +452 -0
- package/packages/irc-core/tests/commands/mode.test.ts +1048 -0
- package/packages/irc-core/tests/commands/motd.test.ts +322 -0
- package/packages/irc-core/tests/commands/names.test.ts +342 -0
- package/packages/irc-core/tests/commands/part.test.ts +265 -0
- package/packages/irc-core/tests/commands/ping.test.ts +144 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +665 -0
- package/packages/irc-core/tests/commands/quit.test.ts +220 -0
- package/packages/irc-core/tests/commands/registration.test.ts +599 -0
- package/packages/irc-core/tests/commands/topic.test.ts +337 -0
- package/packages/irc-core/tests/commands/who.test.ts +441 -0
- package/packages/irc-core/tests/commands/whois.test.ts +422 -0
- package/packages/irc-core/tests/effects.test.ts +147 -0
- package/packages/irc-core/tests/message-tags.test.ts +182 -0
- package/packages/irc-core/tests/numerics.test.ts +98 -0
- package/packages/irc-core/tests/outbound.test.ts +232 -0
- package/packages/irc-core/tests/parser.test.ts +162 -0
- package/packages/irc-core/tests/ports.test.ts +101 -0
- package/packages/irc-core/tests/serializer.test.ts +164 -0
- package/packages/irc-core/tests/state/channel.test.ts +351 -0
- package/packages/irc-core/tests/state/connection.test.ts +122 -0
- package/packages/irc-core/tests/state/registry.test.ts +20 -0
- package/packages/irc-core/tests/types.test.ts +127 -0
- package/packages/irc-core/tsconfig.build.json +12 -0
- package/packages/irc-core/tsconfig.test.json +10 -0
- package/packages/irc-core/vitest.config.ts +21 -0
- package/packages/irc-server/package.json +28 -0
- package/packages/irc-server/src/actor.ts +449 -0
- package/packages/irc-server/src/dispatch.ts +120 -0
- package/packages/irc-server/src/index.ts +11 -0
- package/packages/irc-server/src/runtime.ts +61 -0
- package/packages/irc-server/tests/actor.test.ts +816 -0
- package/packages/irc-server/tests/dispatch.test.ts +512 -0
- package/packages/irc-server/tests/runtime.test.ts +52 -0
- package/packages/irc-server/tsconfig.build.json +13 -0
- package/packages/irc-server/tsconfig.test.json +11 -0
- package/packages/irc-server/vitest.config.ts +21 -0
- package/pnpm-workspace.yaml +4 -0
- package/tools/tcp-ws-forwarder/package.json +32 -0
- package/tools/tcp-ws-forwarder/src/forwarder.ts +244 -0
- package/tools/tcp-ws-forwarder/src/line-scanner.ts +55 -0
- package/tools/tcp-ws-forwarder/src/main.ts +114 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +618 -0
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +51 -0
- package/tools/tcp-ws-forwarder/tests/line-scanner.test.ts +75 -0
- package/tools/tcp-ws-forwarder/tsconfig.build.json +12 -0
- package/tools/tcp-ws-forwarder/tsconfig.test.json +10 -0
- package/tools/tcp-ws-forwarder/vitest.config.ts +27 -0
- package/tsconfig.base.json +25 -0
- package/turbo.json +29 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { quitReducer } from '../../src/commands/quit';
|
|
3
|
+
import { Effect } from '../../src/effects';
|
|
4
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
+
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
6
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
7
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
8
|
+
|
|
9
|
+
const serverConfig: ServerConfig = {
|
|
10
|
+
serverName: 'irc.example.com',
|
|
11
|
+
networkName: 'ExampleNet',
|
|
12
|
+
maxChannelsPerUser: 30,
|
|
13
|
+
maxTargetsPerCommand: 10,
|
|
14
|
+
maxListEntries: 50,
|
|
15
|
+
nickLen: 30,
|
|
16
|
+
channelLen: 50,
|
|
17
|
+
topicLen: 390,
|
|
18
|
+
quitMessage: 'Client Quit',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function makeCtx(state: ConnectionState, clock = new FakeClock(1_000)): Ctx {
|
|
22
|
+
return buildCtx({
|
|
23
|
+
serverConfig,
|
|
24
|
+
clock,
|
|
25
|
+
ids: new SequentialIdFactory(),
|
|
26
|
+
motd: EmptyMotdProvider,
|
|
27
|
+
connection: state,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeRegisteredState(): ConnectionState {
|
|
32
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
33
|
+
state.registration = 'registered';
|
|
34
|
+
state.nick = 'alice';
|
|
35
|
+
state.user = 'alice';
|
|
36
|
+
state.host = 'example.com';
|
|
37
|
+
state.realname = 'Alice';
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const L = (text: string): RawLine => ({ text });
|
|
42
|
+
|
|
43
|
+
describe('quitReducer', () => {
|
|
44
|
+
it('broadcasts QUIT to every joined channel', () => {
|
|
45
|
+
const state = makeRegisteredState();
|
|
46
|
+
state.joinedChannels.add('#foo');
|
|
47
|
+
state.joinedChannels.add('#bar');
|
|
48
|
+
const ctx = makeCtx(state);
|
|
49
|
+
|
|
50
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
51
|
+
|
|
52
|
+
const broadcastChans = out.effects
|
|
53
|
+
.filter((e): e is Extract<EffectType, { tag: 'Broadcast' }> => e.tag === 'Broadcast')
|
|
54
|
+
.map((e) => e.chan);
|
|
55
|
+
expect(broadcastChans).toEqual(['#foo', '#bar']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('uses the client-provided reason in the broadcast line', () => {
|
|
59
|
+
const state = makeRegisteredState();
|
|
60
|
+
state.joinedChannels.add('#foo');
|
|
61
|
+
const ctx = makeCtx(state);
|
|
62
|
+
|
|
63
|
+
const out = quitReducer(state, { command: 'QUIT', params: ['bye all'], tags: {} }, ctx);
|
|
64
|
+
|
|
65
|
+
expect(out.effects).toEqual<EffectType[]>(
|
|
66
|
+
expect.arrayContaining([
|
|
67
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com QUIT :bye all')], 'c1'),
|
|
68
|
+
]),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('falls back to serverConfig.quitMessage when no reason is supplied', () => {
|
|
73
|
+
const state = makeRegisteredState();
|
|
74
|
+
state.joinedChannels.add('#foo');
|
|
75
|
+
const ctx = makeCtx(state);
|
|
76
|
+
|
|
77
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
78
|
+
|
|
79
|
+
expect(out.effects).toEqual<EffectType[]>(
|
|
80
|
+
expect.arrayContaining([
|
|
81
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com QUIT :Client Quit')], 'c1'),
|
|
82
|
+
]),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('treats an empty reason param the same as no reason param', () => {
|
|
87
|
+
const state = makeRegisteredState();
|
|
88
|
+
state.joinedChannels.add('#foo');
|
|
89
|
+
const ctx = makeCtx(state);
|
|
90
|
+
|
|
91
|
+
const out = quitReducer(state, { command: 'QUIT', params: [''], tags: {} }, ctx);
|
|
92
|
+
|
|
93
|
+
expect(out.effects).toEqual<EffectType[]>(
|
|
94
|
+
expect.arrayContaining([
|
|
95
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com QUIT :Client Quit')], 'c1'),
|
|
96
|
+
]),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('excludes the quitting connection from the broadcast', () => {
|
|
101
|
+
const state = makeRegisteredState();
|
|
102
|
+
state.joinedChannels.add('#foo');
|
|
103
|
+
const ctx = makeCtx(state);
|
|
104
|
+
|
|
105
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
106
|
+
|
|
107
|
+
const broadcasts = out.effects.filter(
|
|
108
|
+
(e): e is Extract<EffectType, { tag: 'Broadcast' }> => e.tag === 'Broadcast',
|
|
109
|
+
);
|
|
110
|
+
expect(broadcasts).toHaveLength(1);
|
|
111
|
+
const only = broadcasts[0];
|
|
112
|
+
if (only === undefined) throw new Error('expected exactly one Broadcast effect');
|
|
113
|
+
expect(only.except).toBe('c1');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('emits an ApplyChannelDelta removing the connection from each joined channel', () => {
|
|
117
|
+
const state = makeRegisteredState();
|
|
118
|
+
state.joinedChannels.add('#foo');
|
|
119
|
+
state.joinedChannels.add('#bar');
|
|
120
|
+
const ctx = makeCtx(state);
|
|
121
|
+
|
|
122
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
123
|
+
|
|
124
|
+
expect(out.effects).toEqual<EffectType[]>(
|
|
125
|
+
expect.arrayContaining([
|
|
126
|
+
Effect.applyChannelDelta('#foo', { memberships: [{ type: 'remove', conn: 'c1' }] }),
|
|
127
|
+
Effect.applyChannelDelta('#bar', { memberships: [{ type: 'remove', conn: 'c1' }] }),
|
|
128
|
+
]),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('releases the reserved nick', () => {
|
|
133
|
+
const state = makeRegisteredState();
|
|
134
|
+
const ctx = makeCtx(state);
|
|
135
|
+
|
|
136
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
137
|
+
|
|
138
|
+
expect(out.effects).toContainEqual<EffectType>(Effect.releaseNick('alice'));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('emits a Disconnect for the connection', () => {
|
|
142
|
+
const state = makeRegisteredState();
|
|
143
|
+
const ctx = makeCtx(state);
|
|
144
|
+
|
|
145
|
+
const out = quitReducer(state, { command: 'QUIT', params: ['bye'], tags: {} }, ctx);
|
|
146
|
+
|
|
147
|
+
expect(out.effects).toContainEqual<EffectType>(Effect.disconnect('c1'));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('still emits ReleaseNick and Disconnect when the connection joined no channels', () => {
|
|
151
|
+
const state = makeRegisteredState();
|
|
152
|
+
const ctx = makeCtx(state);
|
|
153
|
+
|
|
154
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
155
|
+
|
|
156
|
+
const tags = out.effects.map((e) => e.tag);
|
|
157
|
+
expect(tags).not.toContain('Broadcast');
|
|
158
|
+
expect(tags).not.toContain('ApplyChannelDelta');
|
|
159
|
+
expect(tags).toContain('ReleaseNick');
|
|
160
|
+
expect(tags).toContain('Disconnect');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('emits no Broadcast, no ApplyChannelDelta, and no ReleaseNick when unregistered', () => {
|
|
164
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
165
|
+
const ctx = makeCtx(state);
|
|
166
|
+
|
|
167
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
168
|
+
|
|
169
|
+
const tags = out.effects.map((e) => e.tag);
|
|
170
|
+
expect(tags).not.toContain('Broadcast');
|
|
171
|
+
expect(tags).not.toContain('ApplyChannelDelta');
|
|
172
|
+
expect(tags).not.toContain('ReleaseNick');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('emits exactly one Disconnect when the connection was never registered', () => {
|
|
176
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
177
|
+
const ctx = makeCtx(state);
|
|
178
|
+
|
|
179
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
180
|
+
|
|
181
|
+
expect(out.effects).toEqual<EffectType[]>([Effect.disconnect('c1')]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('updates lastSeen to ctx.clock.now()', () => {
|
|
185
|
+
const state = makeRegisteredState();
|
|
186
|
+
const clock = new FakeClock(5_000);
|
|
187
|
+
const ctx = makeCtx(state, clock);
|
|
188
|
+
|
|
189
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
190
|
+
|
|
191
|
+
expect(out.state.lastSeen).toBe(5_000);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns the same state reference (mutation permitted, no copy)', () => {
|
|
195
|
+
const state = makeRegisteredState();
|
|
196
|
+
const ctx = makeCtx(state);
|
|
197
|
+
|
|
198
|
+
const out = quitReducer(state, { command: 'QUIT', params: [], tags: {} }, ctx);
|
|
199
|
+
|
|
200
|
+
expect(out.state).toBe(state);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('emits effects in a deterministic order: broadcasts, deltas, release, disconnect', () => {
|
|
204
|
+
const state = makeRegisteredState();
|
|
205
|
+
state.joinedChannels.add('#foo');
|
|
206
|
+
state.joinedChannels.add('#bar');
|
|
207
|
+
const ctx = makeCtx(state);
|
|
208
|
+
|
|
209
|
+
const out = quitReducer(state, { command: 'QUIT', params: ['gone'], tags: {} }, ctx);
|
|
210
|
+
|
|
211
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
212
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com QUIT :gone')], 'c1'),
|
|
213
|
+
Effect.broadcast('#bar', [L(':alice!alice@example.com QUIT :gone')], 'c1'),
|
|
214
|
+
Effect.applyChannelDelta('#foo', { memberships: [{ type: 'remove', conn: 'c1' }] }),
|
|
215
|
+
Effect.applyChannelDelta('#bar', { memberships: [{ type: 'remove', conn: 'c1' }] }),
|
|
216
|
+
Effect.releaseNick('alice'),
|
|
217
|
+
Effect.disconnect('c1'),
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
});
|