@chaos-overlords/server 0.8.1 → 0.8.2
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/dist/app.js +10 -0
- package/dist/app.js.map +1 -1
- package/dist/container.d.ts +16 -0
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +5 -0
- package/dist/container.js.map +1 -1
- package/dist/http/contractJson.d.ts +32 -0
- package/dist/http/contractJson.d.ts.map +1 -0
- package/dist/http/contractJson.js +22 -0
- package/dist/http/contractJson.js.map +1 -0
- package/dist/http/errorHandler.d.ts.map +1 -1
- package/dist/http/errorHandler.js +45 -6
- package/dist/http/errorHandler.js.map +1 -1
- package/dist/http/middleware.d.ts +11 -0
- package/dist/http/middleware.d.ts.map +1 -1
- package/dist/http/middleware.js +71 -5
- package/dist/http/middleware.js.map +1 -1
- package/dist/http/responseValidation.d.ts.map +1 -1
- package/dist/http/responseValidation.js +5 -3
- package/dist/http/responseValidation.js.map +1 -1
- package/dist/http/types.d.ts +20 -0
- package/dist/http/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/routes/bugReports.d.ts.map +1 -1
- package/dist/routes/bugReports.js +13 -1
- package/dist/routes/bugReports.js.map +1 -1
- package/dist/routes/events.d.ts.map +1 -1
- package/dist/routes/events.js +26 -5
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/lobby.d.ts.map +1 -1
- package/dist/routes/lobby.js +19 -7
- package/dist/routes/lobby.js.map +1 -1
- package/dist/routes/protocol.d.ts.map +1 -1
- package/dist/routes/protocol.js +2 -1
- package/dist/routes/protocol.js.map +1 -1
- package/dist/routes/snapshots.d.ts.map +1 -1
- package/dist/routes/snapshots.js +3 -2
- package/dist/routes/snapshots.js.map +1 -1
- package/dist/routes/turns.d.ts.map +1 -1
- package/dist/routes/turns.js +4 -3
- package/dist/routes/turns.js.map +1 -1
- package/dist/sse/LocalEventHub.d.ts +36 -10
- package/dist/sse/LocalEventHub.d.ts.map +1 -1
- package/dist/sse/LocalEventHub.js +75 -16
- package/dist/sse/LocalEventHub.js.map +1 -1
- package/dist/sse/MatchLog.d.ts +85 -0
- package/dist/sse/MatchLog.d.ts.map +1 -0
- package/dist/sse/MatchLog.js +147 -0
- package/dist/sse/MatchLog.js.map +1 -0
- package/dist/sse/createSseResponse.d.ts +27 -2
- package/dist/sse/createSseResponse.d.ts.map +1 -1
- package/dist/sse/createSseResponse.js +96 -14
- package/dist/sse/createSseResponse.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { EventRepository, PersistedEvent } from '@chaos-overlords/kernel';
|
|
2
|
+
/** One event as it goes on the wire, formatted and validated exactly once. */
|
|
3
|
+
export interface EventFrame {
|
|
4
|
+
seq: number;
|
|
5
|
+
/** The SSE frame, or null for a stored row this build's schema cannot read. */
|
|
6
|
+
text: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The shared read and the shared frame of one match's event log.
|
|
10
|
+
*
|
|
11
|
+
* Fan-out used to be per subscriber all the way down: waking eighteen streams for one event meant
|
|
12
|
+
* eighteen `listAfter` queries over the same rows, eighteen schema validations of the same payload
|
|
13
|
+
* and eighteen `JSON.stringify` calls producing the same string. The work is per EVENT, not per
|
|
14
|
+
* reader, so it is done here once and handed out.
|
|
15
|
+
*
|
|
16
|
+
* Three things follow from that, in order of how much they save:
|
|
17
|
+
*
|
|
18
|
+
* 1. **The notification carries the event.** `notify` already holds the durable row, so its frame
|
|
19
|
+
* is formatted and kept here before any stream is woken. A stream whose cursor is exactly one
|
|
20
|
+
* behind — which is every healthy stream of the match — finds it and never reads at all.
|
|
21
|
+
* 2. **Concurrent readers at the same cursor share one query.** A stream that does have to read
|
|
22
|
+
* joins the request already in flight for that cursor instead of issuing its own.
|
|
23
|
+
* 3. **A heartbeat that has nothing to catch up on asks nothing.** The highest sequence this
|
|
24
|
+
* process has been told about is known here, so a caught-up stream skips its periodic re-read.
|
|
25
|
+
* At the default cap of 512 streams that was about twenty-five queries a second finding nothing,
|
|
26
|
+
* on the same event loop that seals turns.
|
|
27
|
+
*
|
|
28
|
+
* None of it changes the contract that the LOG is the truth: every fast path is a memo of rows that
|
|
29
|
+
* were durable before they got here, and anything not covered falls through to a query.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MatchLog {
|
|
32
|
+
private readonly matchId;
|
|
33
|
+
private readonly events;
|
|
34
|
+
/** Ascending by seq; trimmed from the front. A Map iterates in insertion order. */
|
|
35
|
+
private readonly frames;
|
|
36
|
+
private readonly inFlight;
|
|
37
|
+
/**
|
|
38
|
+
* The highest sequence a NOTIFICATION has carried, which is not the same as the highest this has
|
|
39
|
+
* ever read.
|
|
40
|
+
*
|
|
41
|
+
* Only a notification proves that this process is being told about the match's appends, and only
|
|
42
|
+
* that justifies answering "there is nothing further" without asking the database. A hub that is
|
|
43
|
+
* woken without the event body — the Durable Object is notified by match id — never sets this, so
|
|
44
|
+
* every one of its reads goes to the log exactly as before.
|
|
45
|
+
*/
|
|
46
|
+
private notified;
|
|
47
|
+
constructor(matchId: string, events: EventRepository);
|
|
48
|
+
/**
|
|
49
|
+
* Remember a published event and its frame.
|
|
50
|
+
*
|
|
51
|
+
* Formatting here rather than in each stream is what turns a seal's burst from "events times
|
|
52
|
+
* subscribers" units of work into "events".
|
|
53
|
+
*/
|
|
54
|
+
record(event: PersistedEvent): void;
|
|
55
|
+
/**
|
|
56
|
+
* Whether a stream at `lastSeq` holds every event this process has been told about.
|
|
57
|
+
*
|
|
58
|
+
* False whenever nothing has been (see `notified`), so an unsure answer is always the one that
|
|
59
|
+
* does the work. Callers use it to skip a periodic re-read, never to decide what to deliver.
|
|
60
|
+
*/
|
|
61
|
+
caughtUp(lastSeq: number): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The next page after `afterSeq`, from memory when the tail covers it and from the log otherwise.
|
|
64
|
+
*
|
|
65
|
+
* A page served from memory is contiguous by construction: it stops at the first gap, so a
|
|
66
|
+
* caller can never be handed a frame whose predecessor it has not seen, which is the invariant
|
|
67
|
+
* the stream cursor rests on.
|
|
68
|
+
*
|
|
69
|
+
* `force` bypasses memory entirely and asks the log. The caller that passes it is the periodic
|
|
70
|
+
* catch-up, which exists precisely for what this process cannot be told: an append by another
|
|
71
|
+
* process against the same database. Everything else may take the short answer, because the
|
|
72
|
+
* events THIS process publishes are the events it is notified of.
|
|
73
|
+
*
|
|
74
|
+
* Bypassing the memo rather than only its "nothing further" answer is what makes the catch-up
|
|
75
|
+
* happen at all: the drain spends the force on its FIRST page, so a page served out of memory
|
|
76
|
+
* used it up without a read ever reaching the log, and the stream then waited another whole
|
|
77
|
+
* catch-up period to try again. A tail hit is the common case on a woken stream, so that was
|
|
78
|
+
* most of them.
|
|
79
|
+
*/
|
|
80
|
+
page(afterSeq: number, force?: boolean): Promise<EventFrame[]>;
|
|
81
|
+
private pageFromMemory;
|
|
82
|
+
private read;
|
|
83
|
+
private trim;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=MatchLog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MatchLog.d.ts","sourceRoot":"","sources":["../../src/sse/MatchLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAG9E,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,+EAA+E;IAC/E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAcD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,QAAQ;IAgBjB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAhBzB,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IACpE;;;;;;;;OAQG;IACH,OAAO,CAAC,QAAQ,CAAI;IAEpB,YACmB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,eAAe,EACtC;IAEJ;;;;;OAKG;IACH,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAKlC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEjC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,UAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAejE;IAED,OAAO,CAAC,cAAc;YAUR,IAAI;IAalB,OAAO,CAAC,IAAI;CAOb"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { tryFormatEvent } from './createSseResponse';
|
|
2
|
+
/**
|
|
3
|
+
* Frames one match keeps in memory.
|
|
4
|
+
*
|
|
5
|
+
* Only the tail matters: every stream of a match sits within a few events of the others, so what
|
|
6
|
+
* this covers is the burst a seal emits and the moment after a reconnect. A straggler further back
|
|
7
|
+
* than this falls through to a query, which is what the durable log is for.
|
|
8
|
+
*/
|
|
9
|
+
const FRAMES_KEPT = 256;
|
|
10
|
+
/** Events one query pages. */
|
|
11
|
+
const PAGE_SIZE = 200;
|
|
12
|
+
/**
|
|
13
|
+
* The shared read and the shared frame of one match's event log.
|
|
14
|
+
*
|
|
15
|
+
* Fan-out used to be per subscriber all the way down: waking eighteen streams for one event meant
|
|
16
|
+
* eighteen `listAfter` queries over the same rows, eighteen schema validations of the same payload
|
|
17
|
+
* and eighteen `JSON.stringify` calls producing the same string. The work is per EVENT, not per
|
|
18
|
+
* reader, so it is done here once and handed out.
|
|
19
|
+
*
|
|
20
|
+
* Three things follow from that, in order of how much they save:
|
|
21
|
+
*
|
|
22
|
+
* 1. **The notification carries the event.** `notify` already holds the durable row, so its frame
|
|
23
|
+
* is formatted and kept here before any stream is woken. A stream whose cursor is exactly one
|
|
24
|
+
* behind — which is every healthy stream of the match — finds it and never reads at all.
|
|
25
|
+
* 2. **Concurrent readers at the same cursor share one query.** A stream that does have to read
|
|
26
|
+
* joins the request already in flight for that cursor instead of issuing its own.
|
|
27
|
+
* 3. **A heartbeat that has nothing to catch up on asks nothing.** The highest sequence this
|
|
28
|
+
* process has been told about is known here, so a caught-up stream skips its periodic re-read.
|
|
29
|
+
* At the default cap of 512 streams that was about twenty-five queries a second finding nothing,
|
|
30
|
+
* on the same event loop that seals turns.
|
|
31
|
+
*
|
|
32
|
+
* None of it changes the contract that the LOG is the truth: every fast path is a memo of rows that
|
|
33
|
+
* were durable before they got here, and anything not covered falls through to a query.
|
|
34
|
+
*/
|
|
35
|
+
export class MatchLog {
|
|
36
|
+
matchId;
|
|
37
|
+
events;
|
|
38
|
+
/** Ascending by seq; trimmed from the front. A Map iterates in insertion order. */
|
|
39
|
+
frames = new Map();
|
|
40
|
+
inFlight = new Map();
|
|
41
|
+
/**
|
|
42
|
+
* The highest sequence a NOTIFICATION has carried, which is not the same as the highest this has
|
|
43
|
+
* ever read.
|
|
44
|
+
*
|
|
45
|
+
* Only a notification proves that this process is being told about the match's appends, and only
|
|
46
|
+
* that justifies answering "there is nothing further" without asking the database. A hub that is
|
|
47
|
+
* woken without the event body — the Durable Object is notified by match id — never sets this, so
|
|
48
|
+
* every one of its reads goes to the log exactly as before.
|
|
49
|
+
*/
|
|
50
|
+
notified = 0;
|
|
51
|
+
constructor(matchId, events) {
|
|
52
|
+
this.matchId = matchId;
|
|
53
|
+
this.events = events;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Remember a published event and its frame.
|
|
57
|
+
*
|
|
58
|
+
* Formatting here rather than in each stream is what turns a seal's burst from "events times
|
|
59
|
+
* subscribers" units of work into "events".
|
|
60
|
+
*/
|
|
61
|
+
record(event) {
|
|
62
|
+
if (event.seq > this.notified)
|
|
63
|
+
this.notified = event.seq;
|
|
64
|
+
if (this.frames.has(event.seq))
|
|
65
|
+
return;
|
|
66
|
+
this.frames.set(event.seq, { seq: event.seq, text: tryFormatEvent(event) });
|
|
67
|
+
this.trim();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a stream at `lastSeq` holds every event this process has been told about.
|
|
71
|
+
*
|
|
72
|
+
* False whenever nothing has been (see `notified`), so an unsure answer is always the one that
|
|
73
|
+
* does the work. Callers use it to skip a periodic re-read, never to decide what to deliver.
|
|
74
|
+
*/
|
|
75
|
+
caughtUp(lastSeq) {
|
|
76
|
+
return this.notified > 0 && lastSeq >= this.notified;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The next page after `afterSeq`, from memory when the tail covers it and from the log otherwise.
|
|
80
|
+
*
|
|
81
|
+
* A page served from memory is contiguous by construction: it stops at the first gap, so a
|
|
82
|
+
* caller can never be handed a frame whose predecessor it has not seen, which is the invariant
|
|
83
|
+
* the stream cursor rests on.
|
|
84
|
+
*
|
|
85
|
+
* `force` bypasses memory entirely and asks the log. The caller that passes it is the periodic
|
|
86
|
+
* catch-up, which exists precisely for what this process cannot be told: an append by another
|
|
87
|
+
* process against the same database. Everything else may take the short answer, because the
|
|
88
|
+
* events THIS process publishes are the events it is notified of.
|
|
89
|
+
*
|
|
90
|
+
* Bypassing the memo rather than only its "nothing further" answer is what makes the catch-up
|
|
91
|
+
* happen at all: the drain spends the force on its FIRST page, so a page served out of memory
|
|
92
|
+
* used it up without a read ever reaching the log, and the stream then waited another whole
|
|
93
|
+
* catch-up period to try again. A tail hit is the common case on a woken stream, so that was
|
|
94
|
+
* most of them.
|
|
95
|
+
*/
|
|
96
|
+
async page(afterSeq, force = false) {
|
|
97
|
+
if (!force) {
|
|
98
|
+
const memoised = this.pageFromMemory(afterSeq);
|
|
99
|
+
if (memoised.length > 0)
|
|
100
|
+
return memoised;
|
|
101
|
+
if (this.caughtUp(afterSeq))
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
// A read already in flight for this cursor is a read of the log, forced or not, so joining it
|
|
105
|
+
// satisfies a forced caller too.
|
|
106
|
+
const shared = this.inFlight.get(afterSeq);
|
|
107
|
+
if (shared)
|
|
108
|
+
return shared;
|
|
109
|
+
const request = this.read(afterSeq).finally(() => {
|
|
110
|
+
this.inFlight.delete(afterSeq);
|
|
111
|
+
});
|
|
112
|
+
this.inFlight.set(afterSeq, request);
|
|
113
|
+
return request;
|
|
114
|
+
}
|
|
115
|
+
pageFromMemory(afterSeq) {
|
|
116
|
+
const page = [];
|
|
117
|
+
for (let seq = afterSeq + 1; page.length < PAGE_SIZE; seq += 1) {
|
|
118
|
+
const frame = this.frames.get(seq);
|
|
119
|
+
if (!frame)
|
|
120
|
+
break;
|
|
121
|
+
page.push(frame);
|
|
122
|
+
}
|
|
123
|
+
return page;
|
|
124
|
+
}
|
|
125
|
+
async read(afterSeq) {
|
|
126
|
+
const rows = await this.events.listAfter(this.matchId, afterSeq, PAGE_SIZE);
|
|
127
|
+
const page = rows.map((row) => {
|
|
128
|
+
const known = this.frames.get(row.seq);
|
|
129
|
+
if (known)
|
|
130
|
+
return known;
|
|
131
|
+
const frame = { seq: row.seq, text: tryFormatEvent(row) };
|
|
132
|
+
this.frames.set(row.seq, frame);
|
|
133
|
+
return frame;
|
|
134
|
+
});
|
|
135
|
+
this.trim();
|
|
136
|
+
return page;
|
|
137
|
+
}
|
|
138
|
+
trim() {
|
|
139
|
+
while (this.frames.size > FRAMES_KEPT) {
|
|
140
|
+
const oldest = this.frames.keys().next();
|
|
141
|
+
if (oldest.done)
|
|
142
|
+
return;
|
|
143
|
+
this.frames.delete(oldest.value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=MatchLog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MatchLog.js","sourceRoot":"","sources":["../../src/sse/MatchLog.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { PersistedEvent } from '@chaos-overlords/kernel';
|
|
2
|
+
import type { EventFrame } from './MatchLog';
|
|
2
3
|
export interface EventStreamSource {
|
|
3
|
-
/**
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Formatted frames after `afterSeq`, ascending, at most one page.
|
|
6
|
+
*
|
|
7
|
+
* Frames rather than rows because the validation and the serialisation of one event are the same
|
|
8
|
+
* work for every reader of it; see `MatchLog`.
|
|
9
|
+
*/
|
|
10
|
+
page(afterSeq: number, force?: boolean): Promise<EventFrame[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a stream at `lastSeq` is known to have everything.
|
|
13
|
+
*
|
|
14
|
+
* Only ever used to skip a periodic catch-up read that would have found nothing, so an
|
|
15
|
+
* implementation that cannot tell must answer false.
|
|
16
|
+
*/
|
|
17
|
+
caughtUp(lastSeq: number): boolean;
|
|
5
18
|
/**
|
|
6
19
|
* Register a wake-up for new events; returns the unsubscribe.
|
|
7
20
|
*
|
|
@@ -15,6 +28,8 @@ export interface SseOptions {
|
|
|
15
28
|
afterSeq: number;
|
|
16
29
|
heartbeatMs: number;
|
|
17
30
|
signal: AbortSignal;
|
|
31
|
+
/** Told the sequence of a stored event this build cannot validate, so it can be logged. */
|
|
32
|
+
onUnreadable?: (seq: number) => void;
|
|
18
33
|
}
|
|
19
34
|
/**
|
|
20
35
|
* A server-sent event stream over a persisted, sequence-numbered log.
|
|
@@ -43,4 +58,14 @@ export declare function createSseResponse(source: EventStreamSource, options: Ss
|
|
|
43
58
|
* payload, which is unaffected.
|
|
44
59
|
*/
|
|
45
60
|
export declare function formatEvent(event: PersistedEvent): string;
|
|
61
|
+
/** The same frame, or null when the stored row does not fit this build's schema. */
|
|
62
|
+
export declare function tryFormatEvent(event: PersistedEvent): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Whether a stored row fits this build's schema, without building a frame from it.
|
|
65
|
+
*
|
|
66
|
+
* The REST read of the log uses this to leave an unreadable row out of its page. Asking
|
|
67
|
+
* `tryFormatEvent` there meant serialising every event of the page once to throw the string away
|
|
68
|
+
* and once more inside `c.json`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isReadableEvent(event: PersistedEvent): boolean;
|
|
46
71
|
//# sourceMappingURL=createSseResponse.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createSseResponse.d.ts","sourceRoot":"","sources":["../../src/sse/createSseResponse.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"createSseResponse.d.ts","sourceRoot":"","sources":["../../src/sse/createSseResponse.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;IAClC;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC3D;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,WAAW,CAAA;IACnB,2FAA2F;IAC3F,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC;AAkCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,UAAU,GAAG,QAAQ,CAiK1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAIzD;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAMnE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAO9D"}
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { MATCH_EVENT_SSE_NAME, matchEventSchema, SSE_HEARTBEAT_COMMENT, } from '@chaos-overlords/contracts';
|
|
2
2
|
import { validateSync } from '@toad-contracts/core';
|
|
3
|
+
/**
|
|
4
|
+
* Heartbeats between two catch-up reads a caught-up stream cannot talk its way out of.
|
|
5
|
+
*
|
|
6
|
+
* `caughtUp` answers from what THIS process has been told, which is everything on a single Node
|
|
7
|
+
* process and inside a Durable Object, and not necessarily everything when several processes share
|
|
8
|
+
* one database. This bounds how long such a stream can be behind without knowing it, at the cost of
|
|
9
|
+
* one query per stream every hundred seconds or so instead of one every twenty.
|
|
10
|
+
*/
|
|
11
|
+
const CATCH_UP_EVERY_HEARTBEATS = 5;
|
|
12
|
+
/**
|
|
13
|
+
* Heartbeats a consumer may leave the drain parked on backpressure before the stream is dropped.
|
|
14
|
+
*
|
|
15
|
+
* `enqueue` never refuses, so a connection that has stopped being read — a suspended phone, a
|
|
16
|
+
* half-open TCP connection a NAT forgot — is invisible from this side: the drain parks correctly,
|
|
17
|
+
* but the slot, its heartbeat timer and its catch-up reads persist until the OS retransmit timer
|
|
18
|
+
* gives up, which on Linux is fifteen to thirty minutes. Dropping it costs the client nothing it
|
|
19
|
+
* cannot recover: it reconnects and resumes from its `Last-Event-ID`.
|
|
20
|
+
*/
|
|
21
|
+
const STALLED_HEARTBEATS_BEFORE_DROP = 3;
|
|
3
22
|
const encoder = new TextEncoder();
|
|
4
23
|
/**
|
|
5
24
|
* Frames buffered for a consumer that is not keeping up, before the drain parks.
|
|
@@ -25,6 +44,7 @@ const STREAM_HIGH_WATER_MARK = 32;
|
|
|
25
44
|
* again; the log keeps the events in the meantime, which is the whole point of it being the truth.
|
|
26
45
|
*/
|
|
27
46
|
export function createSseResponse(source, options) {
|
|
47
|
+
const { onUnreadable } = options;
|
|
28
48
|
let lastSeq = options.afterSeq;
|
|
29
49
|
let draining = null;
|
|
30
50
|
let wakeAgain = false;
|
|
@@ -41,28 +61,45 @@ export function createSseResponse(source, options) {
|
|
|
41
61
|
controller.enqueue(encoder.encode(text));
|
|
42
62
|
};
|
|
43
63
|
/** Wait until the consumer wants more, or the stream closes. */
|
|
44
|
-
const awaitDemand = (
|
|
64
|
+
const awaitDemand = () => new Promise((resolve) => {
|
|
45
65
|
if (closed || (controller.desiredSize ?? 1) > 0)
|
|
46
66
|
return resolve();
|
|
47
67
|
demand = resolve;
|
|
48
68
|
});
|
|
49
|
-
const drain = async () => {
|
|
69
|
+
const drain = async (force) => {
|
|
70
|
+
let first = force;
|
|
50
71
|
while (!closed) {
|
|
51
|
-
await awaitDemand(
|
|
72
|
+
await awaitDemand();
|
|
52
73
|
if (closed)
|
|
53
74
|
return;
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
// Only the first page of a forced drain has to reach the log — and it does, because
|
|
76
|
+
// `force` bypasses the source's memo rather than only its "nothing further" answer.
|
|
77
|
+
// Once it has, the cursor is beyond whatever this process did not know about.
|
|
78
|
+
const frames = await source.page(lastSeq, first);
|
|
79
|
+
first = false;
|
|
80
|
+
if (frames.length === 0)
|
|
56
81
|
return;
|
|
57
|
-
for (const
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
for (const frame of frames) {
|
|
83
|
+
// A stored row this build cannot validate is skipped, not fatal. Failing the drain
|
|
84
|
+
// errored the stream, and the client reconnected with the same `Last-Event-ID`, read
|
|
85
|
+
// the same row and failed again, for good — and a payload reshaped by a server
|
|
86
|
+
// upgrade is a protocol change, which AGENTS.md says stored matches survive.
|
|
87
|
+
if (frame.text === null) {
|
|
88
|
+
onUnreadable?.(frame.seq);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
send(frame.text);
|
|
92
|
+
}
|
|
93
|
+
lastSeq = Math.max(lastSeq, frame.seq);
|
|
60
94
|
}
|
|
61
95
|
}
|
|
62
96
|
};
|
|
63
|
-
|
|
97
|
+
/** True when a drain must reach the log rather than trust what this process was told. */
|
|
98
|
+
let forceNext = false;
|
|
99
|
+
const wake = (force = false) => {
|
|
64
100
|
if (closed)
|
|
65
101
|
return;
|
|
102
|
+
forceNext ||= force;
|
|
66
103
|
if (draining) {
|
|
67
104
|
wakeAgain = true;
|
|
68
105
|
return;
|
|
@@ -70,7 +107,9 @@ export function createSseResponse(source, options) {
|
|
|
70
107
|
draining = (async () => {
|
|
71
108
|
do {
|
|
72
109
|
wakeAgain = false;
|
|
73
|
-
|
|
110
|
+
const forced = forceNext;
|
|
111
|
+
forceNext = false;
|
|
112
|
+
await drain(forced);
|
|
74
113
|
} while (wakeAgain && !closed);
|
|
75
114
|
})()
|
|
76
115
|
.catch((error) => {
|
|
@@ -100,14 +139,31 @@ export function createSseResponse(source, options) {
|
|
|
100
139
|
// A thunk, not `shutdown` itself: the real one is assigned a few lines below and the
|
|
101
140
|
// placeholder above it only flips `closed`, so passing the reference here would hand the
|
|
102
141
|
// hub a close that leaves the heartbeat running and the subscription in place.
|
|
103
|
-
const unsubscribe = source.subscribe(wake, () => {
|
|
142
|
+
const unsubscribe = source.subscribe(() => wake(), () => {
|
|
104
143
|
shutdown();
|
|
105
144
|
});
|
|
145
|
+
let beats = 0;
|
|
146
|
+
let stalledBeats = 0;
|
|
106
147
|
const heartbeat = setInterval(() => {
|
|
148
|
+
// A consumer that is not reading is not a consumer. The queue is full, so this frame
|
|
149
|
+
// would only buffer; after a few beats of that the connection is gone in every way that
|
|
150
|
+
// matters and the stream is dropped rather than held; see the constant above.
|
|
151
|
+
if ((controller.desiredSize ?? 1) <= 0) {
|
|
152
|
+
stalledBeats += 1;
|
|
153
|
+
if (stalledBeats >= STALLED_HEARTBEATS_BEFORE_DROP)
|
|
154
|
+
shutdown();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
stalledBeats = 0;
|
|
107
158
|
send(`: ${SSE_HEARTBEAT_COMMENT}\n\n`);
|
|
108
159
|
// Fan-out is deliberately best-effort. Re-read the durable log even when the socket is
|
|
109
|
-
// healthy so a failed or process-local notification cannot strand this client forever
|
|
110
|
-
|
|
160
|
+
// healthy so a failed or process-local notification cannot strand this client forever —
|
|
161
|
+
// but only when this process is not already certain there is nothing to read, because an
|
|
162
|
+
// idle server was otherwise issuing one query per stream per heartbeat to find nothing.
|
|
163
|
+
beats += 1;
|
|
164
|
+
const overdue = beats % CATCH_UP_EVERY_HEARTBEATS === 0;
|
|
165
|
+
if (overdue || !source.caughtUp(lastSeq))
|
|
166
|
+
wake(overdue);
|
|
111
167
|
}, options.heartbeatMs);
|
|
112
168
|
shutdown = () => {
|
|
113
169
|
if (closed)
|
|
@@ -127,7 +183,8 @@ export function createSseResponse(source, options) {
|
|
|
127
183
|
};
|
|
128
184
|
options.signal.addEventListener('abort', shutdown, { once: true });
|
|
129
185
|
send(': connected\n\n');
|
|
130
|
-
|
|
186
|
+
// The opening catch-up always reaches the log: a fresh stream has been told nothing.
|
|
187
|
+
wake(true);
|
|
131
188
|
},
|
|
132
189
|
// The consumer read enough to want more: let a drain parked on backpressure continue.
|
|
133
190
|
pull() {
|
|
@@ -164,4 +221,29 @@ export function formatEvent(event) {
|
|
|
164
221
|
const data = JSON.stringify(validated);
|
|
165
222
|
return `id: ${validated.seq}\nevent: ${MATCH_EVENT_SSE_NAME}\ndata: ${data}\n\n`;
|
|
166
223
|
}
|
|
224
|
+
/** The same frame, or null when the stored row does not fit this build's schema. */
|
|
225
|
+
export function tryFormatEvent(event) {
|
|
226
|
+
try {
|
|
227
|
+
return formatEvent(event);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Whether a stored row fits this build's schema, without building a frame from it.
|
|
235
|
+
*
|
|
236
|
+
* The REST read of the log uses this to leave an unreadable row out of its page. Asking
|
|
237
|
+
* `tryFormatEvent` there meant serialising every event of the page once to throw the string away
|
|
238
|
+
* and once more inside `c.json`.
|
|
239
|
+
*/
|
|
240
|
+
export function isReadableEvent(event) {
|
|
241
|
+
try {
|
|
242
|
+
validateSync(matchEventSchema, event);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
167
249
|
//# sourceMappingURL=createSseResponse.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createSseResponse.js","sourceRoot":"","sources":["../../src/sse/createSseResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"createSseResponse.js","sourceRoot":"","sources":["../../src/sse/createSseResponse.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaos-overlords/server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"chaos-overlords",
|
|
@@ -48,16 +48,16 @@
|
|
|
48
48
|
"provenance": true
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@chaos-overlords/bug-reports": "0.8.
|
|
52
|
-
"@chaos-overlords/contracts": "0.8.
|
|
53
|
-
"@chaos-overlords/kernel": "0.8.
|
|
51
|
+
"@chaos-overlords/bug-reports": "0.8.2",
|
|
52
|
+
"@chaos-overlords/contracts": "0.8.2",
|
|
53
|
+
"@chaos-overlords/kernel": "0.8.2",
|
|
54
54
|
"@toad-contracts/core": "1.0.0",
|
|
55
55
|
"@toad-contracts/hono": "1.0.0",
|
|
56
|
-
"hono": "^4.13.
|
|
56
|
+
"hono": "^4.13.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@chaos-overlords/conformance": "0.8.
|
|
60
|
-
"typescript": "^
|
|
59
|
+
"@chaos-overlords/conformance": "0.8.2",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
61
|
"vitest": "^4.1.11"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|