serverless-ircd 0.4.0 → 0.5.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/CHANGELOG.md +175 -0
- package/README.md +88 -19
- package/apps/aws-stack/README.md +4 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +32 -4
- package/apps/aws-stack/tests/stack.test.ts +47 -1
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +129 -21
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -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 +63 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/connect.test.ts +97 -1
- package/packages/aws-adapter/tests/handlers.test.ts +148 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +2 -0
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +5 -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 +182 -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/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 +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- 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-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/registry-do.test.ts +4 -4
- 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/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 +1 -1
- package/packages/irc-core/src/commands/index.ts +8 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/isupport.ts +6 -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 +4 -8
- 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/rehash.ts +119 -0
- 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/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/config.ts +7 -0
- package/packages/irc-core/src/effects.ts +27 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +179 -0
- package/packages/irc-core/src/protocol/numerics.ts +42 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +8 -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/tests/admission.test.ts +18 -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 +57 -0
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- 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/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- 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/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/stats-store.test.ts +222 -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 +249 -16
- package/packages/irc-server/src/dispatch.ts +1 -0
- 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 +1089 -1
- package/packages/irc-server/tests/dispatch.test.ts +37 -0
- 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/scenarios.ts +9 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +1 -1
- 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
|
@@ -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 {
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
* - `alarm()` PING sweep; missing PONG disconnects.
|
|
17
17
|
*
|
|
18
18
|
* Cross-DO coordination:
|
|
19
|
-
* - `REGISTRY_DO` — sharded nick registry
|
|
20
|
-
* - `CHANNEL_DO` per lowercased channel — authoritative roster/modes
|
|
21
|
-
* (035).
|
|
19
|
+
* - `REGISTRY_DO` — sharded nick registry.
|
|
20
|
+
* - `CHANNEL_DO` per lowercased channel — authoritative roster/modes.
|
|
22
21
|
*
|
|
23
22
|
* The actor's `ActorChannelAccess` is a passthrough that returns
|
|
24
23
|
* throwaway {@link ChannelState} objects — reducers mutate the local
|
|
@@ -52,35 +51,40 @@ import {
|
|
|
52
51
|
type ServerConfig,
|
|
53
52
|
SystemClock,
|
|
54
53
|
UuidIdFactory,
|
|
54
|
+
type WsFrameMode,
|
|
55
55
|
createChannel,
|
|
56
56
|
createConnection,
|
|
57
|
+
frameToLines,
|
|
58
|
+
isWithinWsByteBudget,
|
|
59
|
+
parseSecWsProtocolOffers,
|
|
60
|
+
sanitizeForTextMode,
|
|
61
|
+
selectSubprotocol,
|
|
57
62
|
toSnapshot,
|
|
63
|
+
wsFrameModeFor,
|
|
58
64
|
} from '@serverless-ircd/irc-core';
|
|
59
|
-
import { ConnectionActor } from '@serverless-ircd/irc-server';
|
|
65
|
+
import { ConnectionActor, type Transport } from '@serverless-ircd/irc-server';
|
|
60
66
|
import { makeCfRuntime } from './cf-runtime.js';
|
|
61
67
|
import type { CfConnectionHandlers } from './cf-runtime.js';
|
|
68
|
+
import { loadServerConfigFromCfEnv } from './config-loader.js';
|
|
62
69
|
import { resolveAccountStore } from './d1-account-store.js';
|
|
63
70
|
import type { Env } from './env.js';
|
|
64
71
|
import { STATE_STORAGE_KEY, deserialize, serialize } from './serialize.js';
|
|
65
72
|
import type { PersistedConnectionState } from './serialize.js';
|
|
73
|
+
import { CfStats } from './stats.js';
|
|
66
74
|
|
|
67
75
|
/** Default PING cadence (ms) — tuned for typical IRC client behavior. */
|
|
68
76
|
export const DEFAULT_PING_INTERVAL_MS = 60_000;
|
|
69
77
|
/** Default no-PONG disconnect threshold (ms). */
|
|
70
78
|
export const DEFAULT_PONG_TIMEOUT_MS = 90_000;
|
|
71
79
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
channelLen: 50,
|
|
81
|
-
topicLen: 390,
|
|
82
|
-
quitMessage: 'Client Quit',
|
|
83
|
-
};
|
|
80
|
+
/**
|
|
81
|
+
* Wall-clock timestamp captured at this worker isolate's first module load
|
|
82
|
+
* (cold start). Used as the `uptimeStartedAt` anchor for {@link CfStats} so
|
|
83
|
+
* `STATS u` reports uptime for the current isolate. A dedicated stats DO
|
|
84
|
+
* would supply a deployment-wide uptime (future work); for now each
|
|
85
|
+
* isolate reports its own.
|
|
86
|
+
*/
|
|
87
|
+
const WORKER_STARTUP_AT = Date.now();
|
|
84
88
|
|
|
85
89
|
/**
|
|
86
90
|
* ConnectionDO instance.
|
|
@@ -91,6 +95,13 @@ const DEFAULT_SERVER_CONFIG: ServerConfig = {
|
|
|
91
95
|
export class ConnectionDO extends DurableObject<Env> {
|
|
92
96
|
/** Cached connection state; reloaded from storage after hibernation. */
|
|
93
97
|
private cached: ConnectionState | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Cached reducer-facing server config. Parsed once from `this.env`
|
|
100
|
+
* (bindings are stable for the DO's lifetime) so the schema-validated
|
|
101
|
+
* `loadServerConfigFromCfEnv` runs a single time rather than on every
|
|
102
|
+
* WebSocket frame.
|
|
103
|
+
*/
|
|
104
|
+
private cachedServerConfig: ServerConfig | undefined;
|
|
94
105
|
/**
|
|
95
106
|
* Per-instance channel-state cache. Reused across every WebSocket frame
|
|
96
107
|
* the connection handles (within a single hibernation cycle) so that
|
|
@@ -150,15 +161,17 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
150
161
|
private readonly ids: IdFactory = new UuidIdFactory();
|
|
151
162
|
private readonly pingIntervalMs: number = DEFAULT_PING_INTERVAL_MS;
|
|
152
163
|
private readonly pongTimeoutMs: number = DEFAULT_PONG_TIMEOUT_MS;
|
|
153
|
-
// Outbound lines are batched per call, not via a reserved
|
|
154
|
-
// field: `deliver()` joins every line of a ChannelDO
|
|
155
|
-
// single `WebSocket.send()`, and the actor `send`
|
|
156
|
-
// `buildActor` does the same for one frame's response.
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
164
|
+
// Outbound lines are batched per call (legacy mode), not via a reserved
|
|
165
|
+
// instance field: a legacy `deliver()` joins every line of a ChannelDO
|
|
166
|
+
// broadcast into a single `WebSocket.send()`, and the legacy actor `send`
|
|
167
|
+
// handler in `buildActor` does the same for one frame's response. An
|
|
168
|
+
// IRCv3 spec-mode connection instead sends one line per WS message with
|
|
169
|
+
// no trailing CR-LF (see `sendOutbound`). Cross-call per-microtask
|
|
170
|
+
// coalescing (the optimization previously reserved here) was dropped — it
|
|
171
|
+
// would add a drain queue and ordering hazards between independent
|
|
172
|
+
// fan-out calls for no measured benefit (see ADR-003 and the CF adapter
|
|
173
|
+
// load-test report). Revisit only if WS round-trips become a demonstrated
|
|
174
|
+
// bottleneck.
|
|
162
175
|
|
|
163
176
|
// -------------------------------------------------------------------------
|
|
164
177
|
// WebSocket upgrade
|
|
@@ -180,12 +193,23 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
180
193
|
if (cf !== undefined && typeof cf.tlsClientAuthCertSubject === 'string') {
|
|
181
194
|
this.mtlsCertSubject = cf.tlsClientAuthCertSubject;
|
|
182
195
|
}
|
|
196
|
+
// IRCv3 WebSocket subprotocol negotiation: read the offered
|
|
197
|
+
// `Sec-WebSocket-Protocol` list, select the first supported entry, and
|
|
198
|
+
// echo it back when one was agreed. The negotiated frame mode is tagged
|
|
199
|
+
// onto the hibernated socket so it survives eviction and can be
|
|
200
|
+
// recovered in every event handler (webSocketMessage / deliver / alarm).
|
|
201
|
+
const offerHeader = request.headers.get('Sec-WebSocket-Protocol');
|
|
202
|
+
const chosen = selectSubprotocol(parseSecWsProtocolOffers(offerHeader));
|
|
203
|
+
const mode = wsFrameModeFor(chosen ?? undefined);
|
|
183
204
|
// Hibernation: `acceptWebSocket` accepts the server side implicitly.
|
|
184
|
-
// Calling `server.accept()` first throws "already accepted".
|
|
185
|
-
|
|
205
|
+
// Calling `server.accept()` first throws "already accepted". The tag
|
|
206
|
+
// records the negotiated frame mode for the outbound + inbound paths.
|
|
207
|
+
this.ctx.acceptWebSocket(server, [`ws:${mode}`]);
|
|
186
208
|
// Schedule the first PING sweep.
|
|
187
209
|
await this.ctx.storage.setAlarm(Date.now() + this.pingIntervalMs);
|
|
188
|
-
|
|
210
|
+
const headers = new Headers();
|
|
211
|
+
if (chosen !== null) headers.set('Sec-WebSocket-Protocol', chosen);
|
|
212
|
+
return new Response(null, { status: 101, webSocket: client, headers });
|
|
189
213
|
}
|
|
190
214
|
|
|
191
215
|
// -------------------------------------------------------------------------
|
|
@@ -194,12 +218,33 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
194
218
|
|
|
195
219
|
/** Frame → actor → dispatch → persist. */
|
|
196
220
|
override async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
|
|
197
|
-
const
|
|
221
|
+
const mode = this.wsFrameMode(ws);
|
|
222
|
+
// text.ircv3.net is a text-only subprotocol: a binary frame on such a
|
|
223
|
+
// connection is a protocol violation. RFC 6455 close code 1003 (unsupported
|
|
224
|
+
// data) is the spec-mandated response.
|
|
225
|
+
if (mode === 'spec-text' && typeof message !== 'string') {
|
|
226
|
+
ws.close(1003, 'binary frame not permitted on text.ircv3.net');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const raw = typeof message === 'string' ? message : new TextDecoder().decode(message);
|
|
230
|
+
// IRCv3 WebSocket spec: a single message MUST fit the 510-byte budget
|
|
231
|
+
// (the 512-byte IRC line limit minus the omitted trailing CR-LF). An
|
|
232
|
+
// oversize message is a protocol violation; RFC 6455 close code 1009
|
|
233
|
+
// (Message Too Big) is the spec-mandated response. Legacy connections
|
|
234
|
+
// keep the parser's existing 512-byte line cap, so the budget is
|
|
235
|
+
// enforced for negotiated spec modes only.
|
|
236
|
+
if ((mode === 'spec-text' || mode === 'spec-binary') && !isWithinWsByteBudget(raw)) {
|
|
237
|
+
ws.close(1009, 'Message Too Big');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// text.ircv3.net frames are UTF-8 text; lone surrogates have no valid
|
|
241
|
+
// encoding and are substituted with U+FFFD before framing.
|
|
242
|
+
const text = mode === 'spec-text' ? sanitizeForTextMode(raw) : raw;
|
|
198
243
|
const state = await this.loadState();
|
|
199
244
|
const beforeNick = state.nick;
|
|
200
245
|
|
|
201
246
|
await this.loadAccountStore();
|
|
202
|
-
const actor = this.buildActor(ws, state);
|
|
247
|
+
const actor = this.buildActor(ws, state, mode);
|
|
203
248
|
try {
|
|
204
249
|
await actor.receiveTextFrame(text);
|
|
205
250
|
} finally {
|
|
@@ -242,7 +287,7 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
242
287
|
* been idle past the PONG timeout, closes it.
|
|
243
288
|
*
|
|
244
289
|
* PLAN §6.1 — DO alarms are the canonical PING/idle mechanism on CF;
|
|
245
|
-
* equivalent to EventBridge Scheduler on AWS
|
|
290
|
+
* equivalent to EventBridge Scheduler on AWS.
|
|
246
291
|
*/
|
|
247
292
|
override async alarm(): Promise<void> {
|
|
248
293
|
const state = await this.loadState();
|
|
@@ -260,7 +305,8 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
260
305
|
// Send PING. PONG replies update lastSeen via the actor.
|
|
261
306
|
const token = this.ids.nonce();
|
|
262
307
|
for (const ws of this.ctx.getWebSockets()) {
|
|
263
|
-
|
|
308
|
+
const socket = ws as WebSocket;
|
|
309
|
+
this.sendOutbound(socket, [{ text: `PING :${token}` }], this.wsFrameMode(socket));
|
|
264
310
|
}
|
|
265
311
|
await this.ctx.storage.setAlarm(now + this.pingIntervalMs);
|
|
266
312
|
}
|
|
@@ -291,14 +337,11 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
291
337
|
*/
|
|
292
338
|
async deliver(lines: RawLine[]): Promise<{ delivered: number }> {
|
|
293
339
|
let delivered = 0;
|
|
294
|
-
let text: string | undefined;
|
|
295
|
-
if (lines.length > 0) {
|
|
296
|
-
text = `${lines.map((l) => l.text).join('\r\n')}\r\n`;
|
|
297
|
-
}
|
|
298
340
|
for (const ws of this.ctx.getWebSockets()) {
|
|
299
341
|
const socket = ws as WebSocket;
|
|
300
342
|
if (socket.readyState !== WebSocket.OPEN) continue;
|
|
301
|
-
|
|
343
|
+
const mode = this.wsFrameMode(socket);
|
|
344
|
+
this.sendOutbound(socket, lines, mode);
|
|
302
345
|
delivered++;
|
|
303
346
|
}
|
|
304
347
|
return { delivered };
|
|
@@ -308,6 +351,44 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
308
351
|
// Internal helpers
|
|
309
352
|
// -------------------------------------------------------------------------
|
|
310
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Recovers the negotiated WebSocket frame mode for `ws` from its
|
|
356
|
+
* hibernation tag. The tag is written once in `fetch` and survives DO
|
|
357
|
+
* eviction, so this returns the correct mode on every wake. Defaults to
|
|
358
|
+
* the legacy-tolerant mode when no tag is present (e.g. a socket accepted
|
|
359
|
+
* before subprotocol negotiation existed).
|
|
360
|
+
*/
|
|
361
|
+
private wsFrameMode(ws: WebSocket): WsFrameMode {
|
|
362
|
+
return wsFrameModeFromTags(this.ctx.getTags(ws));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Writes `lines` to `ws` according to `mode`:
|
|
367
|
+
* - `spec-text` — one `ws.send` per line, no trailing CR-LF, lone
|
|
368
|
+
* surrogates substituted with U+FFFD (UTF-8 safety).
|
|
369
|
+
* - `spec-binary` — one `ws.send` per line as a UTF-8 binary frame,
|
|
370
|
+
* no trailing CR-LF.
|
|
371
|
+
* - `legacy` — all lines joined with `\r\n` plus a trailing
|
|
372
|
+
* `\r\n`, sent as a single WebSocket message.
|
|
373
|
+
*
|
|
374
|
+
* No-ops when the socket is closed or `lines` is empty (preserving the
|
|
375
|
+
* empty-payload probe contract of {@link deliver}).
|
|
376
|
+
*/
|
|
377
|
+
private sendOutbound(ws: WebSocket, lines: RawLine[], mode: WsFrameMode): void {
|
|
378
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
379
|
+
if (lines.length === 0) return;
|
|
380
|
+
if (mode === 'spec-text') {
|
|
381
|
+
for (const line of lines) ws.send(sanitizeForTextMode(line.text));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (mode === 'spec-binary') {
|
|
385
|
+
const enc = new TextEncoder();
|
|
386
|
+
for (const line of lines) ws.send(enc.encode(line.text));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
ws.send(`${lines.map((l) => l.text).join('\r\n')}\r\n`);
|
|
390
|
+
}
|
|
391
|
+
|
|
311
392
|
/** Loads the connection state from storage, caching for the event. */
|
|
312
393
|
private async loadState(): Promise<ConnectionState> {
|
|
313
394
|
if (this.cached !== undefined) return this.cached;
|
|
@@ -330,27 +411,28 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
330
411
|
}
|
|
331
412
|
|
|
332
413
|
/**
|
|
333
|
-
* Constructs an actor wired to this DO's CfRuntime.
|
|
414
|
+
* Constructs an actor wired to this DO's CfRuntime. `mode` drives both the
|
|
415
|
+
* inbound line-framing transport (spec mode: one IRC message per frame,
|
|
416
|
+
* never split; legacy: split on `\r\n`) and the outbound delivery shape
|
|
417
|
+
* (spec mode: one `WebSocket.send` per line, no trailing CR-LF; legacy:
|
|
418
|
+
* batched into a single `\r\n`-joined frame).
|
|
334
419
|
*/
|
|
335
|
-
private buildActor(ws: WebSocket, state: ConnectionState): ConnectionActor {
|
|
420
|
+
private buildActor(ws: WebSocket, state: ConnectionState, mode: WsFrameMode): ConnectionActor {
|
|
336
421
|
const serverConfig = this.serverConfig();
|
|
337
422
|
const motd = this.motdProvider();
|
|
338
423
|
const handlers = {
|
|
339
424
|
send: (lines: RawLine[]): void => {
|
|
340
|
-
|
|
341
|
-
const text = `${lines.map((l) => l.text).join('\r\n')}\r\n`;
|
|
342
|
-
ws.send(text);
|
|
343
|
-
}
|
|
425
|
+
this.sendOutbound(ws, lines, mode);
|
|
344
426
|
},
|
|
345
427
|
disconnect: (reason?: string): void => {
|
|
346
|
-
if (reason !== undefined
|
|
347
|
-
|
|
428
|
+
if (reason !== undefined) {
|
|
429
|
+
this.sendOutbound(ws, [{ text: `ERROR :Closing link: (${reason})` }], mode);
|
|
348
430
|
}
|
|
349
431
|
ws.close();
|
|
350
432
|
},
|
|
351
433
|
snapshot: (): ConnectionState | undefined => this.cached,
|
|
352
434
|
};
|
|
353
|
-
// The CfRuntime shards the registry by nick
|
|
435
|
+
// The CfRuntime shards the registry by nick; the per-connection
|
|
354
436
|
// key passed here is ignored by `makeCfRuntime` (kept in the signature
|
|
355
437
|
// for call-site stability).
|
|
356
438
|
const registryKey = this.ctx.id.toString();
|
|
@@ -372,11 +454,18 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
372
454
|
runtime,
|
|
373
455
|
channels: this.channelAccess,
|
|
374
456
|
serverConfig,
|
|
457
|
+
configSource: 'KV',
|
|
375
458
|
clock: this.clock,
|
|
376
459
|
ids: this.ids,
|
|
377
460
|
motd,
|
|
378
461
|
messages: this.messageStore(),
|
|
379
462
|
history: this.historyStore(),
|
|
463
|
+
// CfStats fans out across the channel + connection RPCs the runtime
|
|
464
|
+
// already exposes. `uptimeStartedAt` is captured at the worker's
|
|
465
|
+
// module load (cold start); a dedicated stats DO would give a
|
|
466
|
+
// deployment-wide uptime, tracked as future work.
|
|
467
|
+
stats: new CfStats(runtime, WORKER_STARTUP_AT),
|
|
468
|
+
transport: makeWsFrameTransport(mode),
|
|
380
469
|
...(accounts !== undefined ? { accounts } : {}),
|
|
381
470
|
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
382
471
|
logger,
|
|
@@ -387,7 +476,7 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
387
476
|
private async tearDown(ws: WebSocket): Promise<void> {
|
|
388
477
|
const state = await this.loadState();
|
|
389
478
|
await this.loadAccountStore();
|
|
390
|
-
const actor = this.buildActor(ws, state);
|
|
479
|
+
const actor = this.buildActor(ws, state, this.wsFrameMode(ws));
|
|
391
480
|
// Drive QUIT through the actor so the same effect pipeline handles
|
|
392
481
|
// fanout as during normal operation.
|
|
393
482
|
await actor.receiveTextFrame('QUIT\r\n');
|
|
@@ -415,13 +504,17 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
415
504
|
}
|
|
416
505
|
|
|
417
506
|
private serverConfig(): ServerConfig {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
507
|
+
// Single source of truth: delegate to the schema-validated loader so
|
|
508
|
+
// every reducer-facing field (operCreds, serverPassword, motdLines,
|
|
509
|
+
// limit knobs, …) is populated from `this.env` exactly as the shared
|
|
510
|
+
// `ServerConfigSchema` prescribes. The previous hand-rolled literal
|
|
511
|
+
// silently dropped fields like operCreds, which left OPER unable to
|
|
512
|
+
// authenticate (491 ERR_NOOPERHOST) even when OPER_USER/OPER_PASSWORD
|
|
513
|
+
// were bound. See config-loader.ts and config.ts.
|
|
514
|
+
if (this.cachedServerConfig === undefined) {
|
|
515
|
+
this.cachedServerConfig = loadServerConfigFromCfEnv(this.env);
|
|
516
|
+
}
|
|
517
|
+
return this.cachedServerConfig;
|
|
425
518
|
}
|
|
426
519
|
|
|
427
520
|
private motdProvider(): MotdProvider {
|
|
@@ -522,6 +615,7 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
522
615
|
runtime,
|
|
523
616
|
channels: this.channelAccess,
|
|
524
617
|
serverConfig: this.serverConfig(),
|
|
618
|
+
configSource: 'KV',
|
|
525
619
|
clock: this.clock,
|
|
526
620
|
ids: this.ids,
|
|
527
621
|
motd: this.motdProvider(),
|
|
@@ -724,3 +818,37 @@ export function parseSaslAccountsEnv(raw: string | undefined): SaslAccountCreden
|
|
|
724
818
|
}
|
|
725
819
|
return out;
|
|
726
820
|
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Hibernation-tag prefixes written by {@link ConnectionDO.fetch}. Each tag
|
|
824
|
+
* encodes the negotiated {@link WsFrameMode} so it survives DO eviction and
|
|
825
|
+
* can be recovered in every WebSocket event handler via `ctx.getTags(ws)`.
|
|
826
|
+
*/
|
|
827
|
+
const WS_TAG_LEGACY = 'ws:legacy';
|
|
828
|
+
const WS_TAG_SPEC_TEXT = 'ws:spec-text';
|
|
829
|
+
const WS_TAG_SPEC_BINARY = 'ws:spec-binary';
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Recovers the {@link WsFrameMode} from a hibernated socket's tags.
|
|
833
|
+
* Defaults to the legacy-tolerant mode when no recognized tag is present.
|
|
834
|
+
*/
|
|
835
|
+
export function wsFrameModeFromTags(tags: readonly string[]): WsFrameMode {
|
|
836
|
+
for (const tag of tags) {
|
|
837
|
+
if (tag === WS_TAG_SPEC_TEXT) return 'spec-text';
|
|
838
|
+
if (tag === WS_TAG_SPEC_BINARY) return 'spec-binary';
|
|
839
|
+
if (tag === WS_TAG_LEGACY) return 'legacy';
|
|
840
|
+
}
|
|
841
|
+
return 'legacy';
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Builds the inbound line-framing {@link Transport} for `mode`. Spec modes
|
|
846
|
+
* treat one WebSocket message as exactly one IRC line (a single trailing
|
|
847
|
+
* CR-LF is stripped and the frame is never split); legacy mode splits the
|
|
848
|
+
* frame on `\r\n` so older clients that concatenate messages keep working.
|
|
849
|
+
*/
|
|
850
|
+
export function makeWsFrameTransport(mode: WsFrameMode): Transport {
|
|
851
|
+
return {
|
|
852
|
+
feed: (chunk: string): string[] => frameToLines(chunk, mode),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* The production {@link Env} for `apps/cf-worker`. ConnectionDO uses the
|
|
11
11
|
* four DO namespaces below to coordinate with the registry, channel, and
|
|
12
|
-
* channel-registry authorities. The CfRuntime
|
|
12
|
+
* channel-registry authorities. The CfRuntime wires them together
|
|
13
13
|
* behind the platform-agnostic {@link IrcRuntime} port; ConnectionDO
|
|
14
|
-
*
|
|
14
|
+
* only needs `CONNECTION_DO` itself plus the three collaborator
|
|
15
15
|
* namespaces.
|
|
16
16
|
*
|
|
17
17
|
* RPC method shapes on {@link RegistryRpc} and {@link ChannelRpc} are
|
|
@@ -23,7 +23,7 @@ export interface Env {
|
|
|
23
23
|
CONNECTION_DO: DurableObjectNamespace;
|
|
24
24
|
/**
|
|
25
25
|
* Nick registry authority. RPC: reserveNick / changeNick / releaseNick /
|
|
26
|
-
* lookupNick. Backed by the sharded {@link RegistryDO}
|
|
26
|
+
* lookupNick. Backed by the sharded {@link RegistryDO}, which keys
|
|
27
27
|
* instances by `hash(nick) % N` so the single-threaded DO gives the nick
|
|
28
28
|
* uniqueness invariant for free.
|
|
29
29
|
*
|
|
@@ -34,7 +34,7 @@ export interface Env {
|
|
|
34
34
|
/**
|
|
35
35
|
* Channel authority, keyed by lowercased channel name. RPC: broadcast /
|
|
36
36
|
* applyChannelDelta / getChannelSnapshot / listMembers. Backed by the
|
|
37
|
-
* {@link ChannelDO}
|
|
37
|
+
* {@link ChannelDO}, which owns the authoritative roster, modes,
|
|
38
38
|
* topic, and ban list and fans out broadcasts to each member's
|
|
39
39
|
* ConnectionDO.
|
|
40
40
|
*/
|
|
@@ -77,22 +77,41 @@ export interface Env {
|
|
|
77
77
|
* end-to-end. Empty/undefined disables SASL account verification.
|
|
78
78
|
*/
|
|
79
79
|
SASL_ACCOUNTS?: string;
|
|
80
|
+
/**
|
|
81
|
+
* IRC operator name consulted by the `OPER` reducer. Pair with
|
|
82
|
+
* {@link OPER_PASSWORD}; when either is set the connection-do threads
|
|
83
|
+
* both into the reducer-facing `ServerConfig.operCreds` via
|
|
84
|
+
* `loadServerConfigFromCfEnv`.
|
|
85
|
+
*/
|
|
86
|
+
OPER_USER?: string;
|
|
87
|
+
/**
|
|
88
|
+
* IRC operator password paired with {@link OPER_USER}. Treat as a
|
|
89
|
+
* Cloudflare secret (never a plaintext `[vars]` entry) in production.
|
|
90
|
+
*/
|
|
91
|
+
OPER_PASSWORD?: string;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
/**
|
|
83
95
|
* RPC contract ConnectionDO expects from the registry. The real
|
|
84
|
-
* RegistryDO
|
|
96
|
+
* RegistryDO and the test stub both implement this.
|
|
85
97
|
*/
|
|
86
98
|
export interface RegistryRpc {
|
|
87
99
|
reserveNick(nick: string, conn: string): Promise<{ ok: true } | { ok: false }>;
|
|
88
100
|
changeNick(conn: string, oldNick: string, newNick: string): Promise<boolean>;
|
|
89
101
|
releaseNick(nick: string): Promise<void>;
|
|
90
102
|
lookupNick(nick: string): Promise<string | null>;
|
|
103
|
+
/**
|
|
104
|
+
* Enumerates every `(nickLower, connectionId)` entry in this shard. Used
|
|
105
|
+
* by global fan-out paths (e.g. `WALLOPS`) that need to reach connections
|
|
106
|
+
* regardless of shared channel membership. Returns a plain array so the
|
|
107
|
+
* RPC marshals cleanly across the DO boundary.
|
|
108
|
+
*/
|
|
109
|
+
listEntries(): Promise<Array<[nickLower: string, connId: string]>>;
|
|
91
110
|
}
|
|
92
111
|
|
|
93
112
|
/**
|
|
94
113
|
* RPC contract ConnectionDO expects from each channel instance. The real
|
|
95
|
-
* ChannelDO
|
|
114
|
+
* ChannelDO and the test stub both implement this.
|
|
96
115
|
*
|
|
97
116
|
* Note: `broadcast` accepts already-formatted raw line texts (no
|
|
98
117
|
* `RawLine` wrapper) because that's what `cf-runtime.broadcast`
|
|
@@ -23,6 +23,8 @@ export {
|
|
|
23
23
|
} from './sharding.js';
|
|
24
24
|
export { makeCfRuntime } from './cf-runtime.js';
|
|
25
25
|
export type { CfConnectionHandlers } from './cf-runtime.js';
|
|
26
|
+
export { CfStats } from './stats.js';
|
|
27
|
+
export type { CfStatsRuntime } from './stats.js';
|
|
26
28
|
export type { ChannelRpc, ChannelRegistryRpc, Env, RegistryRpc } from './env.js';
|
|
27
29
|
export {
|
|
28
30
|
CREATE_ACCOUNTS_TABLE_SQL,
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* and the second sees the new owner and fails.
|
|
24
24
|
*
|
|
25
25
|
* RPC surface — implements {@link RegistryRpc}, the same shape the
|
|
26
|
-
* stub `RecordingRegistryDO` exposed for
|
|
27
|
-
* (`CfRuntime
|
|
26
|
+
* stub `RecordingRegistryDO` exposed for the connection tests. Consumers
|
|
27
|
+
* (`CfRuntime`) call these methods via `env.REGISTRY_DO`
|
|
28
28
|
* after computing the shard key with {@link registryKeyForNick}.
|
|
29
29
|
*/
|
|
30
30
|
|
|
@@ -85,7 +85,7 @@ export class RegistryDO extends DurableObject implements RegistryRpc {
|
|
|
85
85
|
* `oldNick` after this conn abandoned it (defence in depth).
|
|
86
86
|
*
|
|
87
87
|
* Cross-shard changes (old and new hash to different shards) are
|
|
88
|
-
* orchestrated by `CfRuntime`
|
|
88
|
+
* orchestrated by `CfRuntime` as a two-phase
|
|
89
89
|
* reserve-new-then-release-old. This DO only sees single-shard
|
|
90
90
|
* changes and crash-recovery paths (where `oldNick` may not be
|
|
91
91
|
* present); the latter is why the conditional delete is a soft check.
|
|
@@ -127,4 +127,23 @@ export class RegistryDO extends DurableObject implements RegistryRpc {
|
|
|
127
127
|
const owner = await this.ctx.storage.get<string>(nickKey(nick.toLowerCase()));
|
|
128
128
|
return owner ?? null;
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Enumerates every `(nickLower, connectionId)` pair in this shard.
|
|
133
|
+
* Backs the global WALLOPS fan-out: the caller scans every shard, collects
|
|
134
|
+
* all connection ids, and delivers to those carrying the `+w` mode. The
|
|
135
|
+
* walk is a single `list` over the `nick:` prefix; cost is proportional to
|
|
136
|
+
* the slice's live nick count (sharded, so bounded per DO).
|
|
137
|
+
*/
|
|
138
|
+
async listEntries(): Promise<Array<[nickLower: string, connId: string]>> {
|
|
139
|
+
const out: Array<[nickLower: string, connId: string]> = [];
|
|
140
|
+
// `storage.list` resolves to a Map of `key → value` for the prefix.
|
|
141
|
+
// Each key is `nick:<lower>`; we strip the prefix to recover the nick.
|
|
142
|
+
const entries = await this.ctx.storage.list<string>({ prefix: NICK_KEY_PREFIX });
|
|
143
|
+
for (const [key, connId] of entries) {
|
|
144
|
+
const nickLower = key.slice(NICK_KEY_PREFIX.length);
|
|
145
|
+
out.push([nickLower, connId]);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
130
149
|
}
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
* `N` defaults to {@link DEFAULT_REGISTRY_SHARDS} (32) and is
|
|
23
23
|
* configurable per deployment via the worker env. Changing N after
|
|
24
24
|
* production traffic requires a migration (re-sharding every existing
|
|
25
|
-
* nick→conn mapping); document this in `docs/deployment-cf.md
|
|
26
|
-
* (038).
|
|
25
|
+
* nick→conn mapping); document this in `docs/deployment-cf.md`.
|
|
27
26
|
*/
|
|
28
27
|
|
|
29
28
|
/**
|