@chaos-overlords/server 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/LICENSE +674 -0
- package/NOTICE +21 -0
- package/README.md +33 -0
- package/dist/app.d.ts +7 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +60 -0
- package/dist/app.js.map +1 -0
- package/dist/container.d.ts +61 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +17 -0
- package/dist/container.js.map +1 -0
- package/dist/http/errorHandler.d.ts +5 -0
- package/dist/http/errorHandler.d.ts.map +1 -0
- package/dist/http/errorHandler.js +49 -0
- package/dist/http/errorHandler.js.map +1 -0
- package/dist/http/guards.d.ts +12 -0
- package/dist/http/guards.d.ts.map +1 -0
- package/dist/http/guards.js +17 -0
- package/dist/http/guards.js.map +1 -0
- package/dist/http/middleware.d.ts +27 -0
- package/dist/http/middleware.d.ts.map +1 -0
- package/dist/http/middleware.js +70 -0
- package/dist/http/middleware.js.map +1 -0
- package/dist/http/routes.d.ts +11 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +11 -0
- package/dist/http/routes.js.map +1 -0
- package/dist/http/types.d.ts +10 -0
- package/dist/http/types.d.ts.map +1 -0
- package/dist/http/types.js +2 -0
- package/dist/http/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/bugReports.d.ts +12 -0
- package/dist/routes/bugReports.d.ts.map +1 -0
- package/dist/routes/bugReports.js +23 -0
- package/dist/routes/bugReports.js.map +1 -0
- package/dist/routes/events.d.ts +5 -0
- package/dist/routes/events.d.ts.map +1 -0
- package/dist/routes/events.js +39 -0
- package/dist/routes/events.js.map +1 -0
- package/dist/routes/lobby.d.ts +13 -0
- package/dist/routes/lobby.d.ts.map +1 -0
- package/dist/routes/lobby.js +63 -0
- package/dist/routes/lobby.js.map +1 -0
- package/dist/routes/snapshots.d.ts +5 -0
- package/dist/routes/snapshots.d.ts.map +1 -0
- package/dist/routes/snapshots.js +22 -0
- package/dist/routes/snapshots.js.map +1 -0
- package/dist/routes/turns.d.ts +5 -0
- package/dist/routes/turns.d.ts.map +1 -0
- package/dist/routes/turns.js +37 -0
- package/dist/routes/turns.js.map +1 -0
- package/dist/sse/LocalEventHub.d.ts +22 -0
- package/dist/sse/LocalEventHub.d.ts.map +1 -0
- package/dist/sse/LocalEventHub.js +43 -0
- package/dist/sse/LocalEventHub.js.map +1 -0
- package/dist/sse/createSseResponse.d.ts +28 -0
- package/dist/sse/createSseResponse.d.ts.map +1 -0
- package/dist/sse/createSseResponse.js +126 -0
- package/dist/sse/createSseResponse.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/routes/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAErF,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/C,4FAA4F;AAC5F,MAAM,UAAU,mBAAmB,CAAC,GAAiB;IACnD,cAAc,CAAC,GAAG,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAClD,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;QACjF,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAC7C,MAAM,MAAM,GAAG,MAAM,CAAC;aACnB,GAAG,CAAC,WAAW,CAAC;aAChB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;QACzE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;IAEF,cAAc,CAAC,GAAG,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACpD,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;QACjF,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;QACjF,OAAO,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC;YACzC,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE;YAC3B,QAAQ;YACR,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM;SACzB,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,WAA+B,EAAE,KAAyB;IAC7E,KAAK,MAAM,SAAS,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAA;QAChC,IAAI,SAAS,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;YAAE,OAAO,MAAM,CAAA;IACvF,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { AppEnv } from '../http/types';
|
|
3
|
+
/**
|
|
4
|
+
* Browse, create and join: the three routes that answer without a token.
|
|
5
|
+
*
|
|
6
|
+
* They are registered before the app's `/matches/:matchId/*` auth mount, whose pattern also matches
|
|
7
|
+
* `/matches/join`. Hono ends the chain at the first matching handler, so being registered first is
|
|
8
|
+
* what keeps joining a lobby from demanding the token a player is joining to get.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerPublicLobbyRoutes(api: Hono<AppEnv>): void;
|
|
11
|
+
/** The rest of the lobby lifecycle, for a seated member. */
|
|
12
|
+
export declare function registerMemberLobbyRoutes(api: Hono<AppEnv>): void;
|
|
13
|
+
//# sourceMappingURL=lobby.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lobby.d.ts","sourceRoot":"","sources":["../../src/routes/lobby.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGhC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAoBjE;AAED,4DAA4D;AAC5D,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAyCjE"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createMatchContract, getMatchContract, joinMatchContract, kickPlayerContract, leaveMatchContract, listLobbiesContract, startMatchContract, takeoverVoteContract, } from '@chaos-overlords/contracts';
|
|
2
|
+
import { NotFoundError } from '@chaos-overlords/kernel';
|
|
3
|
+
import { requireMember } from '../http/guards';
|
|
4
|
+
import { buildHonoRoute } from '../http/routes';
|
|
5
|
+
/**
|
|
6
|
+
* Browse, create and join: the three routes that answer without a token.
|
|
7
|
+
*
|
|
8
|
+
* They are registered before the app's `/matches/:matchId/*` auth mount, whose pattern also matches
|
|
9
|
+
* `/matches/join`. Hono ends the chain at the first matching handler, so being registered first is
|
|
10
|
+
* what keeps joining a lobby from demanding the token a player is joining to get.
|
|
11
|
+
*/
|
|
12
|
+
export function registerPublicLobbyRoutes(api) {
|
|
13
|
+
buildHonoRoute(api, listLobbiesContract, async (c) => {
|
|
14
|
+
const { config, kernel } = c.get('container');
|
|
15
|
+
if (!config.publicListing) {
|
|
16
|
+
throw new NotFoundError('This server does not list public matches', {
|
|
17
|
+
reason: 'listing_disabled',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return c.json({ matches: await kernel.query.listPublicLobbies(50) }, 200);
|
|
21
|
+
});
|
|
22
|
+
buildHonoRoute(api, createMatchContract, async (c) => {
|
|
23
|
+
const membership = await c.get('container').kernel.lobby.createMatch(c.req.valid('json'));
|
|
24
|
+
return c.json(membership, 201);
|
|
25
|
+
});
|
|
26
|
+
buildHonoRoute(api, joinMatchContract, async (c) => {
|
|
27
|
+
const membership = await c.get('container').kernel.lobby.join(c.req.valid('json'));
|
|
28
|
+
return c.json(membership, 201);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** The rest of the lobby lifecycle, for a seated member. */
|
|
32
|
+
export function registerMemberLobbyRoutes(api) {
|
|
33
|
+
buildHonoRoute(api, getMatchContract, async (c) => {
|
|
34
|
+
const principal = requireMember(c.get('principal'), c.req.valid('param').matchId);
|
|
35
|
+
const view = await c.get('container').kernel.query.view(principal.match);
|
|
36
|
+
return c.json({ match: view, joinCode: principal.match.joinCode, you: principal.player.id }, 200);
|
|
37
|
+
});
|
|
38
|
+
buildHonoRoute(api, startMatchContract, async (c) => {
|
|
39
|
+
await c
|
|
40
|
+
.get('container')
|
|
41
|
+
.kernel.lobby.start(requireMember(c.get('principal'), c.req.valid('param').matchId));
|
|
42
|
+
return c.body(null, 204);
|
|
43
|
+
});
|
|
44
|
+
buildHonoRoute(api, leaveMatchContract, async (c) => {
|
|
45
|
+
await c
|
|
46
|
+
.get('container')
|
|
47
|
+
.kernel.lobby.leave(requireMember(c.get('principal'), c.req.valid('param').matchId));
|
|
48
|
+
return c.body(null, 204);
|
|
49
|
+
});
|
|
50
|
+
buildHonoRoute(api, kickPlayerContract, async (c) => {
|
|
51
|
+
const { matchId, playerId } = c.req.valid('param');
|
|
52
|
+
await c.get('container').kernel.lobby.kick(requireMember(c.get('principal'), matchId), playerId);
|
|
53
|
+
return c.body(null, 204);
|
|
54
|
+
});
|
|
55
|
+
buildHonoRoute(api, takeoverVoteContract, async (c) => {
|
|
56
|
+
const { matchId, playerId } = c.req.valid('param');
|
|
57
|
+
await c
|
|
58
|
+
.get('container')
|
|
59
|
+
.kernel.lobby.voteOnTakeover(requireMember(c.get('principal'), matchId), playerId, c.req.valid('json'));
|
|
60
|
+
return c.body(null, 204);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=lobby.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lobby.js","sourceRoot":"","sources":["../../src/routes/lobby.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { AppEnv } from '../http/types';
|
|
3
|
+
/** Native snapshots: the host's repair for a desynced turn, and the read every client resyncs from. */
|
|
4
|
+
export declare function registerSnapshotRoutes(api: Hono<AppEnv>): void;
|
|
5
|
+
//# sourceMappingURL=snapshots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../../src/routes/snapshots.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGhC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C,uGAAuG;AACvG,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAqB9D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { latestSnapshotContract, snapshotContract, uploadSnapshotContract, } from '@chaos-overlords/contracts';
|
|
2
|
+
import { requireMember } from '../http/guards';
|
|
3
|
+
import { buildHonoRoute } from '../http/routes';
|
|
4
|
+
/** Native snapshots: the host's repair for a desynced turn, and the read every client resyncs from. */
|
|
5
|
+
export function registerSnapshotRoutes(api) {
|
|
6
|
+
buildHonoRoute(api, uploadSnapshotContract, async (c) => {
|
|
7
|
+
await c
|
|
8
|
+
.get('container')
|
|
9
|
+
.kernel.snapshots.upload(requireMember(c.get('principal'), c.req.valid('param').matchId), c.req.valid('json'));
|
|
10
|
+
return c.body(null, 204);
|
|
11
|
+
});
|
|
12
|
+
buildHonoRoute(api, latestSnapshotContract, async (c) => {
|
|
13
|
+
const principal = requireMember(c.get('principal'), c.req.valid('param').matchId);
|
|
14
|
+
return c.json(await c.get('container').kernel.snapshots.latest(principal.match.id), 200);
|
|
15
|
+
});
|
|
16
|
+
buildHonoRoute(api, snapshotContract, async (c) => {
|
|
17
|
+
const principal = requireMember(c.get('principal'), c.req.valid('param').matchId);
|
|
18
|
+
const { turn } = c.req.valid('param');
|
|
19
|
+
return c.json(await c.get('container').kernel.snapshots.get(principal.match.id, turn), 200);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=snapshots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshots.js","sourceRoot":"","sources":["../../src/routes/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/C,uGAAuG;AACvG,MAAM,UAAU,sBAAsB,CAAC,GAAiB;IACtD,cAAc,CAAC,GAAG,EAAE,sBAAsB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACtD,MAAM,CAAC;aACJ,GAAG,CAAC,WAAW,CAAC;aAChB,MAAM,CAAC,SAAS,CAAC,MAAM,CACtB,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAC/D,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CACpB,CAAA;QACH,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC1B,CAAC,CAAC,CAAA;IAEF,cAAc,CAAC,GAAG,EAAE,sBAAsB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACtD,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;QACjF,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAA;IAC1F,CAAC,CAAC,CAAA;IAEF,cAAc,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAChD,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;QACjF,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACrC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAA;IAC7F,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { AppEnv } from '../http/types';
|
|
3
|
+
/** The turn barrier: submit privately, read the sealed set, report the resulting hash. */
|
|
4
|
+
export declare function registerTurnRoutes(api: Hono<AppEnv>): void;
|
|
5
|
+
//# sourceMappingURL=turns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turns.d.ts","sourceRoot":"","sources":["../../src/routes/turns.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGhC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CA0C1D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ownSubmissionContract, reportTurnContract, sealedOrdersContract, submitOrdersContract, } from '@chaos-overlords/contracts';
|
|
2
|
+
import { requireMember } from '../http/guards';
|
|
3
|
+
import { buildHonoRoute } from '../http/routes';
|
|
4
|
+
/** The turn barrier: submit privately, read the sealed set, report the resulting hash. */
|
|
5
|
+
export function registerTurnRoutes(api) {
|
|
6
|
+
buildHonoRoute(api, submitOrdersContract, async (c) => {
|
|
7
|
+
const { turn } = c.req.valid('param');
|
|
8
|
+
const view = await c
|
|
9
|
+
.get('container')
|
|
10
|
+
.kernel.turns.submitOrders(requireMember(c.get('principal'), c.req.valid('param').matchId), turn, c.req.valid('json'));
|
|
11
|
+
return c.json(view, 200);
|
|
12
|
+
});
|
|
13
|
+
buildHonoRoute(api, ownSubmissionContract, async (c) => {
|
|
14
|
+
const principal = requireMember(c.get('principal'), c.req.valid('param').matchId);
|
|
15
|
+
const { turn } = c.req.valid('param');
|
|
16
|
+
const view = await c
|
|
17
|
+
.get('container')
|
|
18
|
+
.kernel.query.ownSubmission(principal.match, principal.player.id, turn);
|
|
19
|
+
return c.json(view, 200);
|
|
20
|
+
});
|
|
21
|
+
buildHonoRoute(api, sealedOrdersContract, async (c) => {
|
|
22
|
+
const principal = requireMember(c.get('principal'), c.req.valid('param').matchId);
|
|
23
|
+
const { turn } = c.req.valid('param');
|
|
24
|
+
const view = await c.get('container').kernel.query.sealedOrders(principal.match, turn);
|
|
25
|
+
// A sealed set never changes, so clients and proxies may keep it.
|
|
26
|
+
c.header('Cache-Control', 'private, max-age=31536000, immutable');
|
|
27
|
+
return c.json(view, 200);
|
|
28
|
+
});
|
|
29
|
+
buildHonoRoute(api, reportTurnContract, async (c) => {
|
|
30
|
+
const { turn } = c.req.valid('param');
|
|
31
|
+
await c
|
|
32
|
+
.get('container')
|
|
33
|
+
.kernel.turns.report(requireMember(c.get('principal'), c.req.valid('param').matchId), turn, c.req.valid('json'));
|
|
34
|
+
return c.body(null, 204);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=turns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turns.js","sourceRoot":"","sources":["../../src/routes/turns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EventNotifier, EventRepository, EventStreamOpener, PersistedEvent } from '@chaos-overlords/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* In-process fan-out: notifies every open stream of a match within this process. It is both the
|
|
4
|
+
* notifier and the stream opener on Node, and the fan-out inside a Cloudflare Durable Object.
|
|
5
|
+
*/
|
|
6
|
+
export declare class LocalEventHub implements EventNotifier, EventStreamOpener {
|
|
7
|
+
private readonly events;
|
|
8
|
+
private readonly heartbeatMs;
|
|
9
|
+
private readonly listeners;
|
|
10
|
+
constructor(events: EventRepository, heartbeatMs: number);
|
|
11
|
+
notify(event: PersistedEvent): Promise<void>;
|
|
12
|
+
/** Wake every stream of a match; used when the notification arrives without the event body. */
|
|
13
|
+
wake(matchId: string): void;
|
|
14
|
+
subscribe(matchId: string, wake: () => void): () => void;
|
|
15
|
+
connectionCount(matchId: string): number;
|
|
16
|
+
open(input: {
|
|
17
|
+
matchId: string;
|
|
18
|
+
afterSeq: number;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
}): Promise<Response>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=LocalEventHub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalEventHub.d.ts","sourceRoot":"","sources":["../../src/sse/LocalEventHub.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,cAAc,EACf,MAAM,yBAAyB,CAAA;AAKhC;;;GAGG;AACH,qBAAa,aAAc,YAAW,aAAa,EAAE,iBAAiB;IAIlE,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAJ9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;gBAG5C,MAAM,EAAE,eAAe,EACvB,WAAW,EAAE,MAAM;IAGhC,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,+FAA+F;IAC/F,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAUxD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAIlC,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;CASjG"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createSseResponse } from './createSseResponse';
|
|
2
|
+
const PAGE_SIZE = 200;
|
|
3
|
+
/**
|
|
4
|
+
* In-process fan-out: notifies every open stream of a match within this process. It is both the
|
|
5
|
+
* notifier and the stream opener on Node, and the fan-out inside a Cloudflare Durable Object.
|
|
6
|
+
*/
|
|
7
|
+
export class LocalEventHub {
|
|
8
|
+
events;
|
|
9
|
+
heartbeatMs;
|
|
10
|
+
listeners = new Map();
|
|
11
|
+
constructor(events, heartbeatMs) {
|
|
12
|
+
this.events = events;
|
|
13
|
+
this.heartbeatMs = heartbeatMs;
|
|
14
|
+
}
|
|
15
|
+
async notify(event) {
|
|
16
|
+
this.wake(event.matchId);
|
|
17
|
+
}
|
|
18
|
+
/** Wake every stream of a match; used when the notification arrives without the event body. */
|
|
19
|
+
wake(matchId) {
|
|
20
|
+
for (const listener of this.listeners.get(matchId) ?? [])
|
|
21
|
+
listener();
|
|
22
|
+
}
|
|
23
|
+
subscribe(matchId, wake) {
|
|
24
|
+
const set = this.listeners.get(matchId) ?? new Set();
|
|
25
|
+
set.add(wake);
|
|
26
|
+
this.listeners.set(matchId, set);
|
|
27
|
+
return () => {
|
|
28
|
+
set.delete(wake);
|
|
29
|
+
if (set.size === 0)
|
|
30
|
+
this.listeners.delete(matchId);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
connectionCount(matchId) {
|
|
34
|
+
return this.listeners.get(matchId)?.size ?? 0;
|
|
35
|
+
}
|
|
36
|
+
async open(input) {
|
|
37
|
+
return createSseResponse({
|
|
38
|
+
listAfter: (afterSeq) => this.events.listAfter(input.matchId, afterSeq, PAGE_SIZE),
|
|
39
|
+
subscribe: (wake) => this.subscribe(input.matchId, wake),
|
|
40
|
+
}, { afterSeq: input.afterSeq, heartbeatMs: this.heartbeatMs, signal: input.signal });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=LocalEventHub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalEventHub.js","sourceRoot":"","sources":["../../src/sse/LocalEventHub.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD,MAAM,SAAS,GAAG,GAAG,CAAA;AAErB;;;GAGG;AACH,MAAM,OAAO,aAAa;IAIL;IACA;IAJF,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAA;IAE/D,YACmB,MAAuB,EACvB,WAAmB;QADnB,WAAM,GAAN,MAAM,CAAiB;QACvB,gBAAW,GAAX,WAAW,CAAQ;IACnC,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAAqB;QAChC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC1B,CAAC;IAED,+FAA+F;IAC/F,IAAI,CAAC,OAAe;QAClB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE;YAAE,QAAQ,EAAE,CAAA;IACtE,CAAC;IAED,SAAS,CAAC,OAAe,EAAE,IAAgB;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,CAAA;QACpD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACb,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;QAChC,OAAO,GAAG,EAAE;YACV,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YAChB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;gBAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACpD,CAAC,CAAA;IACH,CAAC;IAED,eAAe,CAAC,OAAe;QAC7B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC,CAAA;IAC/C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAiE;QAC1E,OAAO,iBAAiB,CACtB;YACE,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC;YAClF,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC;SACzD,EACD,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAClF,CAAA;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PersistedEvent } from '@chaos-overlords/kernel';
|
|
2
|
+
export interface EventStreamSource {
|
|
3
|
+
/** Persisted events after `afterSeq`, ascending, at most one page. */
|
|
4
|
+
listAfter(afterSeq: number): Promise<PersistedEvent[]>;
|
|
5
|
+
/** Register a wake-up for new events; returns the unsubscribe. */
|
|
6
|
+
subscribe(wake: () => void): () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface SseOptions {
|
|
9
|
+
afterSeq: number;
|
|
10
|
+
heartbeatMs: number;
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A server-sent event stream over a persisted, sequence-numbered log.
|
|
15
|
+
*
|
|
16
|
+
* The log is the truth and the wake-up is only a hint: every wake drains the log from the last
|
|
17
|
+
* delivered sequence, so a notification lost between persist and fan-out costs latency, never an
|
|
18
|
+
* event, and `Last-Event-ID` resumes exactly. One drain runs at a time per connection.
|
|
19
|
+
*
|
|
20
|
+
* The drain respects backpressure. `enqueue` on a stream nobody is reading never refuses, it just
|
|
21
|
+
* buffers, so a consumer that has stopped reading (a suspended phone, a half-open TCP connection)
|
|
22
|
+
* would otherwise pull the whole event log into this process's memory, once per such connection.
|
|
23
|
+
* Instead the drain parks on `pull` when the queue is full and resumes when the consumer reads
|
|
24
|
+
* again; the log keeps the events in the meantime, which is the whole point of it being the truth.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createSseResponse(source: EventStreamSource, options: SseOptions): Response;
|
|
27
|
+
export declare function formatEvent(event: PersistedEvent): string;
|
|
28
|
+
//# sourceMappingURL=createSseResponse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createSseResponse.d.ts","sourceRoot":"","sources":["../../src/sse/createSseResponse.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IACtD,kEAAkE;IAClE,SAAS,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CACxC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,WAAW,CAAA;CACpB;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,UAAU,GAAG,QAAQ,CAsG1F;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEzD"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { SSE_HEARTBEAT_COMMENT } from '@chaos-overlords/contracts';
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
/**
|
|
4
|
+
* Frames buffered for a consumer that is not keeping up, before the drain parks.
|
|
5
|
+
*
|
|
6
|
+
* The default of one would park after every single event, costing a round-trip per frame on a
|
|
7
|
+
* healthy stream. A small window batches the common case and still bounds what one stalled
|
|
8
|
+
* connection can hold.
|
|
9
|
+
*/
|
|
10
|
+
const STREAM_HIGH_WATER_MARK = 32;
|
|
11
|
+
/**
|
|
12
|
+
* A server-sent event stream over a persisted, sequence-numbered log.
|
|
13
|
+
*
|
|
14
|
+
* The log is the truth and the wake-up is only a hint: every wake drains the log from the last
|
|
15
|
+
* delivered sequence, so a notification lost between persist and fan-out costs latency, never an
|
|
16
|
+
* event, and `Last-Event-ID` resumes exactly. One drain runs at a time per connection.
|
|
17
|
+
*
|
|
18
|
+
* The drain respects backpressure. `enqueue` on a stream nobody is reading never refuses, it just
|
|
19
|
+
* buffers, so a consumer that has stopped reading (a suspended phone, a half-open TCP connection)
|
|
20
|
+
* would otherwise pull the whole event log into this process's memory, once per such connection.
|
|
21
|
+
* Instead the drain parks on `pull` when the queue is full and resumes when the consumer reads
|
|
22
|
+
* again; the log keeps the events in the meantime, which is the whole point of it being the truth.
|
|
23
|
+
*/
|
|
24
|
+
export function createSseResponse(source, options) {
|
|
25
|
+
let lastSeq = options.afterSeq;
|
|
26
|
+
let draining = null;
|
|
27
|
+
let wakeAgain = false;
|
|
28
|
+
let closed = false;
|
|
29
|
+
let shutdown = () => {
|
|
30
|
+
closed = true;
|
|
31
|
+
};
|
|
32
|
+
/** Resolved by `pull` when the consumer has read enough to make room for more. */
|
|
33
|
+
let demand = null;
|
|
34
|
+
const stream = new ReadableStream({
|
|
35
|
+
start(controller) {
|
|
36
|
+
const send = (text) => {
|
|
37
|
+
if (!closed)
|
|
38
|
+
controller.enqueue(encoder.encode(text));
|
|
39
|
+
};
|
|
40
|
+
/** Wait until the consumer wants more, or the stream closes. */
|
|
41
|
+
const awaitDemand = (controller) => new Promise((resolve) => {
|
|
42
|
+
if (closed || (controller.desiredSize ?? 1) > 0)
|
|
43
|
+
return resolve();
|
|
44
|
+
demand = resolve;
|
|
45
|
+
});
|
|
46
|
+
const drain = async () => {
|
|
47
|
+
while (!closed) {
|
|
48
|
+
await awaitDemand(controller);
|
|
49
|
+
if (closed)
|
|
50
|
+
return;
|
|
51
|
+
const events = await source.listAfter(lastSeq);
|
|
52
|
+
if (events.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
for (const event of events) {
|
|
55
|
+
send(formatEvent(event));
|
|
56
|
+
lastSeq = Math.max(lastSeq, event.seq);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const wake = () => {
|
|
61
|
+
if (closed)
|
|
62
|
+
return;
|
|
63
|
+
if (draining) {
|
|
64
|
+
wakeAgain = true;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
draining = (async () => {
|
|
68
|
+
do {
|
|
69
|
+
wakeAgain = false;
|
|
70
|
+
await drain();
|
|
71
|
+
} while (wakeAgain && !closed);
|
|
72
|
+
})()
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
if (!closed)
|
|
75
|
+
controller.error(error);
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
draining = null;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const unsubscribe = source.subscribe(wake);
|
|
82
|
+
const heartbeat = setInterval(() => send(`: ${SSE_HEARTBEAT_COMMENT}\n\n`), options.heartbeatMs);
|
|
83
|
+
shutdown = () => {
|
|
84
|
+
if (closed)
|
|
85
|
+
return;
|
|
86
|
+
closed = true;
|
|
87
|
+
clearInterval(heartbeat);
|
|
88
|
+
unsubscribe();
|
|
89
|
+
// Release a drain parked on backpressure, so it observes `closed` and stops.
|
|
90
|
+
demand?.();
|
|
91
|
+
demand = null;
|
|
92
|
+
try {
|
|
93
|
+
controller.close();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Already errored, or cancelled by the consumer.
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
options.signal.addEventListener('abort', shutdown, { once: true });
|
|
100
|
+
send(': connected\n\n');
|
|
101
|
+
wake();
|
|
102
|
+
},
|
|
103
|
+
// The consumer read enough to want more: let a drain parked on backpressure continue.
|
|
104
|
+
pull() {
|
|
105
|
+
const resume = demand;
|
|
106
|
+
demand = null;
|
|
107
|
+
resume?.();
|
|
108
|
+
},
|
|
109
|
+
// The consumer went away (a dropped TCP connection surfaces here, not as an abort).
|
|
110
|
+
cancel() {
|
|
111
|
+
shutdown();
|
|
112
|
+
},
|
|
113
|
+
}, new CountQueuingStrategy({ highWaterMark: STREAM_HIGH_WATER_MARK }));
|
|
114
|
+
return new Response(stream, {
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
117
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
118
|
+
Connection: 'keep-alive',
|
|
119
|
+
'X-Accel-Buffering': 'no',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export function formatEvent(event) {
|
|
124
|
+
return `id: ${event.seq}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=createSseResponse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createSseResponse.js","sourceRoot":"","sources":["../../src/sse/createSseResponse.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chaos-overlords/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Hono application both runtimes serve: REST intents, the SSE event stream, auth and the error envelope. Takes its ports through a container.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chaos-overlords",
|
|
7
|
+
"multiplayer",
|
|
8
|
+
"game-server",
|
|
9
|
+
"turn-based",
|
|
10
|
+
"hono",
|
|
11
|
+
"rest",
|
|
12
|
+
"server-sent-events",
|
|
13
|
+
"sse",
|
|
14
|
+
"http-api"
|
|
15
|
+
],
|
|
16
|
+
"license": "GPL-3.0-only",
|
|
17
|
+
"author": "kibertoad",
|
|
18
|
+
"homepage": "https://github.com/kibertoad/chaos-overlords-new-chrome/tree/main/multiplayer/packages/server#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/kibertoad/chaos-overlords-new-chrome.git",
|
|
22
|
+
"directory": "multiplayer/packages/server"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/kibertoad/chaos-overlords-new-chrome/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"!dist/.tsbuildinfo",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"NOTICE"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"provenance": true
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@chaos-overlords/bug-reports": "0.1.0",
|
|
52
|
+
"@chaos-overlords/contracts": "0.1.0",
|
|
53
|
+
"@chaos-overlords/kernel": "0.1.0",
|
|
54
|
+
"@toad-contracts/core": "0.4.0",
|
|
55
|
+
"@toad-contracts/hono": "0.3.2",
|
|
56
|
+
"hono": "^4.13.7"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@chaos-overlords/conformance": "0.1.0",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"vitest": "^4.1.11"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc -b tsconfig.build.json",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
66
|
+
"test": "vitest",
|
|
67
|
+
"test:run": "vitest run"
|
|
68
|
+
}
|
|
69
|
+
}
|