serverless-ircd 0.7.0 → 0.8.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 +3 -3
- package/.gitmodules +1 -1
- package/CHANGELOG.md +273 -29
- package/README.md +155 -55
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +186 -18
- package/apps/aws-stack/tests/stack.test.ts +400 -56
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/worker.ts +4 -4
- package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
- package/apps/cf-worker/tests/smoke.test.ts +5 -5
- package/apps/cf-worker/wrangler.test.toml +6 -6
- package/apps/cf-worker/wrangler.toml +7 -5
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +8 -0
- package/apps/local-cli/src/main.ts +16 -0
- package/apps/local-cli/src/server.ts +1 -0
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/web/landing/index.html +14 -16
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +23 -16
- package/apps/web/src/build-env.ts +1 -1
- package/apps/web/src/config-schema.ts +6 -6
- package/apps/web/tests/build-smoke.test.ts +14 -14
- package/apps/web/tests/config-schema.test.ts +1 -1
- package/docs/AWS-Deployment.md +21 -8
- package/docs/Cloudflare-Deployment-Guide.md +22 -6
- package/docs/PlanExtensions.md +113 -3
- package/docs/Release-Process.md +23 -13
- package/docs/Services.md +546 -0
- package/docs/WebClientGuide.md +32 -31
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +5 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
- package/packages/aws-adapter/src/config-loader.ts +11 -0
- package/packages/aws-adapter/src/connection-counter.ts +89 -0
- package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
- package/packages/aws-adapter/src/handlers/connect.ts +55 -51
- package/packages/aws-adapter/src/handlers/default.ts +36 -4
- package/packages/aws-adapter/src/handlers/index.ts +15 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +15 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
- package/packages/aws-adapter/src/index.ts +4 -0
- package/packages/aws-adapter/src/stats.ts +6 -1
- package/packages/aws-adapter/src/tables.ts +34 -4
- package/packages/aws-adapter/tests/aws-harness.ts +3 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
- package/packages/aws-adapter/tests/connect.test.ts +158 -32
- package/packages/aws-adapter/tests/connection-counter.test.ts +127 -0
- package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
- package/packages/aws-adapter/tests/handlers.test.ts +105 -3
- package/packages/aws-adapter/tests/tables.test.ts +6 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/config-loader.ts +11 -0
- package/packages/cf-adapter/src/connection-do.ts +112 -2
- package/packages/cf-adapter/src/d1-services-store.ts +703 -0
- package/packages/cf-adapter/src/env.ts +8 -0
- package/packages/cf-adapter/src/index.ts +5 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
- package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
- package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
- package/packages/cf-adapter/tests/serialize.test.ts +1 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
- package/packages/irc-core/src/commands/account-auth.ts +172 -0
- package/packages/irc-core/src/commands/chanserv.ts +882 -0
- package/packages/irc-core/src/commands/hostserv.ts +487 -0
- package/packages/irc-core/src/commands/index.ts +12 -0
- package/packages/irc-core/src/commands/join.ts +164 -8
- package/packages/irc-core/src/commands/markread.ts +202 -0
- package/packages/irc-core/src/commands/memoserv.ts +319 -0
- package/packages/irc-core/src/commands/mode.ts +96 -4
- package/packages/irc-core/src/commands/nickserv.ts +390 -0
- package/packages/irc-core/src/commands/oper.ts +18 -1
- package/packages/irc-core/src/commands/operserv.ts +346 -0
- package/packages/irc-core/src/commands/pre-away.ts +3 -1
- package/packages/irc-core/src/commands/privmsg.ts +42 -0
- package/packages/irc-core/src/commands/read-marker.ts +8 -8
- package/packages/irc-core/src/commands/registration.ts +61 -6
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +37 -0
- package/packages/irc-core/src/config.ts +36 -5
- package/packages/irc-core/src/effects.ts +56 -1
- package/packages/irc-core/src/ports.ts +1653 -84
- package/packages/irc-core/src/protocol/numerics.ts +8 -0
- package/packages/irc-core/src/state/channel.ts +21 -1
- package/packages/irc-core/src/state/connection.ts +25 -1
- package/packages/irc-core/src/types.ts +48 -12
- package/packages/irc-core/tests/commands/chanserv.test.ts +1668 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
- package/packages/irc-core/tests/commands/join.test.ts +393 -1
- package/packages/irc-core/tests/commands/markread.test.ts +361 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
- package/packages/irc-core/tests/commands/mode.test.ts +381 -2
- package/packages/irc-core/tests/commands/nickserv.test.ts +807 -0
- package/packages/irc-core/tests/commands/oper.test.ts +13 -0
- package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
- package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
- package/packages/irc-core/tests/commands/registration.test.ts +788 -14
- package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
- package/packages/irc-core/tests/commands/topic.test.ts +94 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
- package/packages/irc-core/tests/config.test.ts +49 -5
- package/packages/irc-core/tests/effects.test.ts +19 -0
- package/packages/irc-core/tests/message-store.test.ts +63 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
- package/packages/irc-core/tests/services-store.test.ts +1289 -0
- package/packages/irc-core/tests/state/channel.test.ts +3 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +71 -16
- package/packages/irc-server/src/dispatch.ts +94 -7
- package/packages/irc-server/src/routing.ts +19 -0
- package/packages/irc-server/tests/actor.test.ts +623 -12
- package/packages/irc-server/tests/dispatch.test.ts +270 -2
- package/packages/irc-server/tests/routing.test.ts +6 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +29 -3
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
- package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
package/docs/WebClientGuide.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Web Client Guide
|
|
2
2
|
|
|
3
3
|
End-to-end contributor/operator doc for the ServerlessIRCd web client: a
|
|
4
|
-
vendored **Kiwi IRC** SPA served at `/
|
|
5
|
-
page served at `/` by the Cloudflare Worker. The browser opens a
|
|
6
|
-
`wss://` WebSocket straight to the Worker's existing
|
|
7
|
-
endpoint — no proxy, gateway, or transport adaptation
|
|
4
|
+
vendored **Kiwi IRC** SPA served at `/webclient/` and a static project
|
|
5
|
+
landing page served at `/` by the Cloudflare Worker. The browser opens a
|
|
6
|
+
native `wss://` WebSocket straight to the Worker's existing
|
|
7
|
+
IRC-over-WebSocket endpoint — no proxy, gateway, or transport adaptation
|
|
8
|
+
in the hot path.
|
|
8
9
|
|
|
9
10
|
Cross-reference: `PLAN.md` §6.1 (CF mapping), `docs/PlanWebClient.md`
|
|
10
11
|
(design), `docs/Cloudflare-Deployment-Guide.md` (Worker deploy),
|
|
@@ -23,7 +24,7 @@ following only this doc.
|
|
|
23
24
|
┌──────────────────────────────────────────────────────────────┐
|
|
24
25
|
│ Browser │
|
|
25
26
|
│ GET / → landing page (project front door) │
|
|
26
|
-
│ GET /
|
|
27
|
+
│ GET /webclient/ → Kiwi IRC SPA (index.html + JS bundle) │
|
|
27
28
|
│ wss://.../ → one IRC message per WS text frame │
|
|
28
29
|
└──────────────────────────────────────────────────────────────┘
|
|
29
30
|
│ │
|
|
@@ -48,8 +49,8 @@ Path layout on the Worker:
|
|
|
48
49
|
| Path | Served by | Content |
|
|
49
50
|
|------------|--------------------------|----------------------------------|
|
|
50
51
|
| `/` | `[assets]` binding | `dist/index.html` (landing page) |
|
|
51
|
-
| `/
|
|
52
|
-
| `/
|
|
52
|
+
| `/webclient/` | `[assets]` binding | `dist/webclient/index.html` (Kiwi SPA) |
|
|
53
|
+
| `/webclient/static/...` | `[assets]` binding | Kiwi JS/CSS/themes + baked config |
|
|
53
54
|
| `/health` | `worker.ts` handler | Plaintext liveness string |
|
|
54
55
|
| `/` (WS upgrade) | `worker.ts` → `ConnectionDO` | IRC session |
|
|
55
56
|
|
|
@@ -94,8 +95,8 @@ pnpm install
|
|
|
94
95
|
pnpm --filter web build:staging
|
|
95
96
|
# Equivalent: pnpm --filter @serverless-ircd/web build:staging
|
|
96
97
|
# → apps/web/dist/index.html (landing page)
|
|
97
|
-
# → apps/web/dist/
|
|
98
|
-
# → apps/web/dist/
|
|
98
|
+
# → apps/web/dist/webclient/index.html (Kiwi SPA)
|
|
99
|
+
# → apps/web/dist/webclient/static/config.json (baked, env-specific)
|
|
99
100
|
|
|
100
101
|
# 4. Build the rest of the workspace (Worker imports compiled dist/).
|
|
101
102
|
pnpm build
|
|
@@ -103,7 +104,7 @@ pnpm build
|
|
|
103
104
|
# 5. Serve everything locally (Worker + SPA + landing page).
|
|
104
105
|
pnpm --filter @serverless-ircd/cf-worker dev
|
|
105
106
|
# → http://localhost:8787/ landing page
|
|
106
|
-
# → http://localhost:8787/
|
|
107
|
+
# → http://localhost:8787/webclient/ Kiwi SPA
|
|
107
108
|
# → ws://localhost:8787/ IRC-over-WebSocket (same endpoint)
|
|
108
109
|
|
|
109
110
|
# 6. Deploy staging (Worker + assets in one command).
|
|
@@ -111,9 +112,9 @@ pnpm deploy:cf:staging
|
|
|
111
112
|
# → wrangler deploy --env staging
|
|
112
113
|
```
|
|
113
114
|
|
|
114
|
-
Open `http://localhost:8787/
|
|
115
|
-
`/
|
|
116
|
-
dev) connection to the same origin.
|
|
115
|
+
Open `http://localhost:8787/webclient/` in a browser — Kiwi boots, reads
|
|
116
|
+
`/webclient/static/config.json`, and opens a `wss://` (or `ws://` in
|
|
117
|
+
local dev) connection to the same origin.
|
|
117
118
|
|
|
118
119
|
> **Forgot the submodule?** `apps/web/scripts/build.mjs` fails fast:
|
|
119
120
|
> `Kiwi upstream missing at .../upstream. Run
|
|
@@ -128,7 +129,7 @@ The build is an orchestrated layering over the upstream Kiwi build —
|
|
|
128
129
|
|
|
129
130
|
### 4.1 Per-environment config matrix
|
|
130
131
|
|
|
131
|
-
| `pnpm ...` command | Reads | Bakes into `dist/
|
|
132
|
+
| `pnpm ...` command | Reads | Bakes into `dist/webclient/static/config.json` |
|
|
132
133
|
|---|---|---|
|
|
133
134
|
| `--filter web build` | `static/config.json` | default (staging-shaped) config |
|
|
134
135
|
| `--filter web build:staging` | `static/config.staging.json` | staging config |
|
|
@@ -149,17 +150,17 @@ malformed config fails the build rather than shipping a broken SPA.
|
|
|
149
150
|
Node ≥ 17 the OpenSSL legacy provider is enabled for webpack 5's md4
|
|
150
151
|
hashing.
|
|
151
152
|
3. **Patch `upstream/vue.config.js`** `publicPath: ''` →
|
|
152
|
-
`publicPath: '/
|
|
153
|
-
in `finally` so the submodule stays clean. This is why every
|
|
154
|
-
asset URL comes out `/
|
|
155
|
-
`static/js/...`, which would 404 at root.
|
|
156
|
-
4. **Copy `upstream/dist/` → `apps/web/dist/
|
|
153
|
+
`publicPath: '/webclient/'`, run `yarn build`, then **restore** the
|
|
154
|
+
file in `finally` so the submodule stays clean. This is why every
|
|
155
|
+
hashed asset URL comes out `/webclient/static/js/app.<hash>.js`
|
|
156
|
+
instead of `static/js/...`, which would 404 at root.
|
|
157
|
+
4. **Copy `upstream/dist/` → `apps/web/dist/webclient/`.**
|
|
157
158
|
5. **Rewrite bare `static/...` refs** in the baked `index.html` (e.g.
|
|
158
159
|
the favicon, which webpack doesn't run through `publicPath`) to
|
|
159
|
-
`/
|
|
160
|
+
`/webclient/static/...`.
|
|
160
161
|
6. **Validate + bake the per-env config**: read
|
|
161
162
|
`static/config.<env>.json`, run `parseKiwiConfig`, re-serialise
|
|
162
|
-
(stable key order), write to `dist/
|
|
163
|
+
(stable key order), write to `dist/webclient/static/config.json`.
|
|
163
164
|
7. **Copy the landing page** `landing/index.html` → `dist/index.html`.
|
|
164
165
|
|
|
165
166
|
Output is reproducible: same checkout + same `--env` → byte-identical
|
|
@@ -308,8 +309,8 @@ pnpm --filter @serverless-ircd/cf-worker dev
|
|
|
308
309
|
Then:
|
|
309
310
|
|
|
310
311
|
- `http://localhost:8787/` — landing page.
|
|
311
|
-
- `http://localhost:8787/
|
|
312
|
-
`/
|
|
312
|
+
- `http://localhost:8787/webclient/` — Kiwi SPA (reads
|
|
313
|
+
`/webclient/static/config.json`, opens `ws://localhost:8787/`).
|
|
313
314
|
- `http://localhost:8787/health` — plaintext liveness.
|
|
314
315
|
|
|
315
316
|
Smoke the WS path directly (no browser needed):
|
|
@@ -403,7 +404,7 @@ deployment; don't change the default config.
|
|
|
403
404
|
|
|
404
405
|
The landing page (`apps/web/landing/index.html`) is the project's front
|
|
405
406
|
door: a visitor hitting `/` learns what ServerlessIRCd is and clicks
|
|
406
|
-
through to `/
|
|
407
|
+
through to `/webclient/` to launch the client. It is intentionally
|
|
407
408
|
framework-free (pure HTML + inline CSS, no JS) so it renders without
|
|
408
409
|
JavaScript and costs nothing to serve.
|
|
409
410
|
|
|
@@ -413,16 +414,16 @@ To customise:
|
|
|
413
414
|
blocks directly. The CSS variables at the top of the `<style>` block
|
|
414
415
|
(`--bg`, `--surface`, `--accent`, …) theme the whole page in one
|
|
415
416
|
place.
|
|
416
|
-
- **Links** — the page links to `/
|
|
417
|
-
source repo. Update the repo URLs and the docs path to match
|
|
418
|
-
deployment.
|
|
417
|
+
- **Links** — the page links to `/webclient/` (the SPA), `docs/`, and
|
|
418
|
+
the source repo. Update the repo URLs and the docs path to match
|
|
419
|
+
your deployment.
|
|
419
420
|
- **Accessibility** — keep the declared viewport meta tag and the
|
|
420
421
|
no-`<script>` invariant; both are asserted by
|
|
421
422
|
`tests/build-smoke.test.ts`. The build copies the file verbatim to
|
|
422
423
|
`dist/index.html`, so what you author is exactly what is served.
|
|
423
424
|
|
|
424
425
|
Re-run `pnpm --filter web build` (any `--env`) after edits; the build
|
|
425
|
-
test suite will fail loudly if you accidentally remove the `/
|
|
426
|
+
test suite will fail loudly if you accidentally remove the `/webclient/`
|
|
426
427
|
link, the docs link, or the viewport tag.
|
|
427
428
|
|
|
428
429
|
---
|
|
@@ -457,7 +458,7 @@ and check the WS upgrade:
|
|
|
457
458
|
`WEB_ORIGINS` if you're cross-origin, or confirm the SPA and Worker
|
|
458
459
|
are same-origin.
|
|
459
460
|
|
|
460
|
-
### 10.3 Asset 404s at `/static/...` (instead of `/
|
|
461
|
+
### 10.3 Asset 404s at `/static/...` (instead of `/webclient/static/...`)
|
|
461
462
|
|
|
462
463
|
The `publicPath` patch in step 3 of the build didn't take. Confirm
|
|
463
464
|
`upstream/vue.config.js` still contains a `publicPath: ''` literal to
|
|
@@ -483,8 +484,8 @@ packages.
|
|
|
483
484
|
### 10.6 Build-smoke tests fail in CI but pass locally
|
|
484
485
|
|
|
485
486
|
The `dist/`-dependent assertions in `tests/build-smoke.test.ts` are
|
|
486
|
-
gated on `existsSync(dist/
|
|
487
|
-
SPA hasn't been built. CI runs `pnpm --filter web build` (or
|
|
487
|
+
gated on `existsSync(dist/webclient/index.html)` and are skipped when
|
|
488
|
+
the SPA hasn't been built. CI runs `pnpm --filter web build` (or
|
|
488
489
|
`pnpm build`) before `pnpm --filter web test`, so the assertions fire
|
|
489
490
|
there. If you run `pnpm --filter web test` locally without building,
|
|
490
491
|
only the landing-page source assertions run — that's expected, not a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-ircd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Serverless IRC daemon with a platform-agnostic core and Cloudflare Workers + AWS adapters",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typescript": "^5.9.3",
|
|
18
18
|
"vite": "^7.3.6",
|
|
19
19
|
"vitest": "^4.1.10",
|
|
20
|
-
"@serverless-ircd/aws-adapter": "0.
|
|
20
|
+
"@serverless-ircd/aws-adapter": "0.8.0"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "turbo run build",
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
toSnapshot as toConnSnapshot,
|
|
58
58
|
} from '@serverless-ircd/irc-core';
|
|
59
59
|
import type { IrcRuntime } from '@serverless-ircd/irc-server';
|
|
60
|
+
import { decrementConnectionCount } from './connection-counter.js';
|
|
60
61
|
import {
|
|
61
62
|
type MarshalledChannelMember,
|
|
62
63
|
type MarshalledChannelMeta,
|
|
@@ -733,6 +734,10 @@ export async function cleanupConnection(
|
|
|
733
734
|
await dynamo.send(
|
|
734
735
|
new DeleteCommand({ TableName: tables.Connections, Key: { connectionId: connId } }),
|
|
735
736
|
);
|
|
737
|
+
// Release the admission slot this connection reserved at `$connect`. The
|
|
738
|
+
// meta counter is best-effort (decrement floors at 0; a missing/zero
|
|
739
|
+
// counter is a silent no-op), so a drifted count never fails teardown.
|
|
740
|
+
await decrementConnectionCount(dynamo, tables.Connections);
|
|
736
741
|
|
|
737
742
|
if (item.nick !== undefined) {
|
|
738
743
|
try {
|
|
@@ -70,4 +70,10 @@ export const TABLE_DEFS: Record<TableName, TableProps> = {
|
|
|
70
70
|
partitionKey: { name: 'account', type: AttributeType.STRING },
|
|
71
71
|
billingMode: BillingMode.PAY_PER_REQUEST,
|
|
72
72
|
},
|
|
73
|
+
Services: {
|
|
74
|
+
tableName: 'Services',
|
|
75
|
+
partitionKey: { name: 'pk', type: AttributeType.STRING },
|
|
76
|
+
sortKey: { name: 'sk', type: AttributeType.STRING },
|
|
77
|
+
billingMode: BillingMode.PAY_PER_REQUEST,
|
|
78
|
+
},
|
|
73
79
|
};
|
|
@@ -35,6 +35,14 @@ export interface LambdaConfigEnv {
|
|
|
35
35
|
CHANNEL_PREFIXES?: string;
|
|
36
36
|
OPER_USER?: string;
|
|
37
37
|
OPER_PASSWORD?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Server-password gate. Treat as an AWS secret (Secrets Manager or
|
|
40
|
+
* SSM `SecureString`); the loader threads it through to
|
|
41
|
+
* `ServerConfig.serverPassword` so the registration reducer requires
|
|
42
|
+
* `PASS <value>` (or a SASL-identified connection) before
|
|
43
|
+
* `001 RPL_WELCOME`. Undefined / empty disables the gate.
|
|
44
|
+
*/
|
|
45
|
+
SERVER_PASSWORD?: string;
|
|
38
46
|
MAX_CHANNELS_PER_USER?: string;
|
|
39
47
|
MAX_TARGETS_PER_COMMAND?: string;
|
|
40
48
|
NICK_LEN?: string;
|
|
@@ -87,6 +95,9 @@ export function buildLambdaConfigInput(env: LambdaConfigEnv): Record<string, unk
|
|
|
87
95
|
},
|
|
88
96
|
];
|
|
89
97
|
}
|
|
98
|
+
if (env.SERVER_PASSWORD !== undefined) {
|
|
99
|
+
input.serverPassword = env.SERVER_PASSWORD;
|
|
100
|
+
}
|
|
90
101
|
if (env.MAX_CHANNELS_PER_USER !== undefined) {
|
|
91
102
|
input.maxChannelsPerUser = Number.parseInt(env.MAX_CHANNELS_PER_USER, 10);
|
|
92
103
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O(1) admission counter for the AWS `$connect` handler.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the previous full-table `Scan COUNT` over `Connections` (which
|
|
5
|
+
* grew linearly with the connection count and made `$connect` latency
|
|
6
|
+
* spike into the multi-second range). The live connection count is kept in
|
|
7
|
+
* a single meta row inside the `Connections` table and updated atomically:
|
|
8
|
+
*
|
|
9
|
+
* - `$connect` reserves a slot with {@link incrementConnectionCount}.
|
|
10
|
+
* - `$disconnect` / the gone-connection sweeper release a slot with
|
|
11
|
+
* {@link decrementConnectionCount} (via `cleanupConnection`).
|
|
12
|
+
*
|
|
13
|
+
* The meta row is keyed by the {@link CONNECTION_COUNT_META_ID} sentinel
|
|
14
|
+
* and deliberately carries **no** `idleSince`, so the sweeper (which only
|
|
15
|
+
* tears down rows with a stale numeric `idleSince`) skips it; stats and
|
|
16
|
+
* the sweeper filter this id out of their connection lists.
|
|
17
|
+
*/
|
|
18
|
+
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
19
|
+
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sentinel partition key for the connection-count meta row. Real APIGW
|
|
23
|
+
* connection ids are opaque alphanumeric tokens, so this `__meta:`-prefixed
|
|
24
|
+
* value never collides.
|
|
25
|
+
*/
|
|
26
|
+
export const CONNECTION_COUNT_META_ID = '__meta:connectionCount__';
|
|
27
|
+
|
|
28
|
+
/** Attribute name holding the integer count on the meta row. */
|
|
29
|
+
export const CONNECTION_COUNT_ATTR = 'count';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Atomically reserves one connection slot: increments the counter and
|
|
33
|
+
* returns the **new** value (i.e. including this reservation). Creates the
|
|
34
|
+
* meta row on first use via `if_not_exists`.
|
|
35
|
+
*
|
|
36
|
+
* Idempotent at the DynamoDB level: a single `UpdateItem` with
|
|
37
|
+
* `ReturnValues=UPDATED_NEW` is the whole operation — no read-modify-write
|
|
38
|
+
* loop, so concurrent `$connect` invocations never lose an increment.
|
|
39
|
+
*/
|
|
40
|
+
export async function incrementConnectionCount(
|
|
41
|
+
dynamo: DynamoDBDocumentClient,
|
|
42
|
+
tableName: string,
|
|
43
|
+
): Promise<number> {
|
|
44
|
+
const res = await dynamo.send(
|
|
45
|
+
new UpdateCommand({
|
|
46
|
+
TableName: tableName,
|
|
47
|
+
Key: { connectionId: CONNECTION_COUNT_META_ID },
|
|
48
|
+
UpdateExpression: 'SET #c = if_not_exists(#c, :zero) + :one',
|
|
49
|
+
ExpressionAttributeNames: { '#c': CONNECTION_COUNT_ATTR },
|
|
50
|
+
ExpressionAttributeValues: { ':zero': 0, ':one': 1 },
|
|
51
|
+
ReturnValues: 'UPDATED_NEW',
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
const updated = res.Attributes as Record<string, unknown> | undefined;
|
|
55
|
+
const count = updated?.[CONNECTION_COUNT_ATTR];
|
|
56
|
+
return typeof count === 'number' ? count : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Atomically releases one connection slot: decrements the counter, floored
|
|
61
|
+
* at 0. The `attribute_exists(count) AND count > :zero` condition makes a
|
|
62
|
+
* stray decrement (e.g. a sweeper pass after a counter-reset deploy) a
|
|
63
|
+
* silent no-op instead of driving the count negative.
|
|
64
|
+
*
|
|
65
|
+
* Errors are swallowed: the counter is best-effort and self-correcting
|
|
66
|
+
* (a drifted count only tightens or loosens the admission cap slightly;
|
|
67
|
+
* it never crashes connection teardown).
|
|
68
|
+
*/
|
|
69
|
+
export async function decrementConnectionCount(
|
|
70
|
+
dynamo: DynamoDBDocumentClient,
|
|
71
|
+
tableName: string,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
await dynamo.send(
|
|
75
|
+
new UpdateCommand({
|
|
76
|
+
TableName: tableName,
|
|
77
|
+
Key: { connectionId: CONNECTION_COUNT_META_ID },
|
|
78
|
+
UpdateExpression: 'SET #c = #c - :one',
|
|
79
|
+
ConditionExpression: 'attribute_exists(#c) AND #c > :zero',
|
|
80
|
+
ExpressionAttributeNames: { '#c': CONNECTION_COUNT_ATTR },
|
|
81
|
+
ExpressionAttributeValues: { ':zero': 0, ':one': 1 },
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
} catch {
|
|
85
|
+
// ConditionalCheckFailedException (counter missing / already 0) → no-op.
|
|
86
|
+
// Any other error is also swallowed: teardown must not fail because the
|
|
87
|
+
// admission counter drifted.
|
|
88
|
+
}
|
|
89
|
+
}
|