Skip to content

Commit af78f8f

Browse files
authored
Merge pull request #68 from MatthewWid/infer-state-type
Allow passing initial session and channel state in constructor
2 parents e51af48 + 1a58c36 commit af78f8f

File tree

7 files changed

+124
-8
lines changed

7 files changed

+124
-8
lines changed

docs/api.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Note that creating a new session will immediately send the initial status code a
4141
|`keepAlive`|`number` \| `null`|`10000`|Time in milliseconds interval for the session to send a comment to keep the connection alive.<br><br>Give as `null` to disable the keep-alive mechanism.|
4242
|`statusCode`|`number`|`200`|Status code to be sent to the client.<br><br>Event stream requests can be redirected using HTTP 301 and 307 status codes. Make sure to set `Location` header when using these status codes (301/307) using the `headers` property.<br><br>A client can be asked to stop reconnecting by send a 204 status code.|
4343
|`headers`|`object`|`{}`|Additional headers to be sent along with the response.|
44+
|`state`|`object`|`{}`|Initial custom state for the session.<br><br>Accessed via the [`state`](#sessionstate-state) property.<br><br>When using TypeScript, providing the initial state structure allows the type of the `state` property to be automatically inferred.|
4445

4546
#### `Session#lastId`: `string`
4647

@@ -60,6 +61,8 @@ Custom state for this session.
6061

6162
Use this object to safely store information related to the session and user.
6263

64+
You may set an initial value for this property using the `state` property in the [constructor `options` object](#new-sessionstate--defaultsessionstatereq-incomingmessage--http2serverrequest-res-serverresponse--http2serverresponse-options--), allowing its type to be automatically inferred.
65+
6366
Use [module augmentation and declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to safely add new properties to the `DefaultSessionState` interface.
6467

6568
#### `Session#push`: `(data: unknown[, eventName: string[, eventId: string]]) => this`
@@ -174,14 +177,24 @@ A `Channel` is used to broadcast events to many sessions at once.
174177
You may use the second generic argument `SessionState` to enforce that only sessions
175178
with the same state type may be registered with this channel.
176179

177-
#### `new Channel<State, SessionState>()`
180+
#### `new Channel<State, SessionState>([options = {}])`
181+
182+
`options` is an object with the following properties:
183+
184+
|Property|Type|Default|Description|
185+
|-|-|-|-|
186+
|`state`|`object`|`{}`|Initial custom state for the channel.<br><br>Accessed via the [`state`](#channelstate-state) property.<br><br>When using TypeScript, providing the initial state structure allows the type of the `state` property to be automatically inferred.|
178187

179188
#### `Channel#state`: `State`
180189

181190
Custom state for this channel.
182191

183192
Use this object to safely store information related to the channel.
184193

194+
You may set an initial value for this property using the `state` property in the [constructor `options` object](#new-channelstate-sessionstate), allowing its type to be automatically inferred.
195+
196+
Use [module augmentation and declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to safely add new properties to the `DefaultChannelState` interface.
197+
185198
#### `Channel#activeSessions`: `ReadonlyArray<Session>`
186199

187200
List of the currently active sessions subscribed to this channel.

examples/create-keys.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {createCertificate} from "pem";
2+
3+
createCertificate({days: 1, selfSigned: true}, (error, result) => {
4+
if (error) {
5+
console.error(error);
6+
return;
7+
}
8+
9+
console.log(result);
10+
});

examples/getting-started/pubsub.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {Session, Channel, createChannel} from "better-sse";
2+
3+
class PubSub {
4+
private events = new Map<string, Channel>();
5+
6+
subscribe(session: Session, event: string): void {
7+
if (!this.events.has(event)) {
8+
const newChannel = createChannel();
9+
10+
this.events.set(event, newChannel);
11+
12+
// Clean up channel if no more subscribers
13+
newChannel.on("session-deregistered", () => {
14+
if (newChannel.sessionCount === 0) {
15+
this.events.delete(event);
16+
}
17+
});
18+
}
19+
20+
const channel = this.events.get(event) as Channel;
21+
22+
channel.register(session);
23+
}
24+
25+
unsubscribe(session: Session, event: string): void {
26+
const channel = this.events.get(event);
27+
28+
if (channel) {
29+
channel.deregister(session);
30+
}
31+
}
32+
33+
publish(data: unknown, event: string): void {
34+
const channel = this.events.get(event);
35+
36+
if (channel) {
37+
channel.broadcast(data, event);
38+
}
39+
}
40+
}
41+
42+
export {PubSub};

src/Channel.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ describe("construction", () => {
4545
});
4646
});
4747

48+
describe("state", () => {
49+
const givenState = {id: "123"};
50+
51+
it("can set the initial state in options", () => {
52+
const channel = new Channel({state: givenState});
53+
54+
expect(channel.state.id).toBe(givenState.id);
55+
});
56+
});
57+
4858
describe("registering", () => {
4959
it("can register and store an active session", () =>
5060
new Promise<void>((done) => {

src/Channel.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import {TypedEmitter, EventMap} from "./lib/TypedEmitter";
33
import {generateId} from "./lib/generateId";
44
import {SseError} from "./lib/SseError";
55

6+
interface ChannelOptions<
7+
State extends Record<string, unknown> = DefaultChannelState
8+
> {
9+
/**
10+
* Custom state for this channel.
11+
*
12+
* Use this object to safely store information related to the channel.
13+
*/
14+
state?: State;
15+
}
16+
617
interface BroadcastOptions<
718
SessionState extends Record<string, unknown> = DefaultSessionState
819
> {
@@ -49,12 +60,14 @@ class Channel<
4960
*
5061
* Use this object to safely store information related to the channel.
5162
*/
52-
state = {} as State;
63+
state: State;
5364

5465
private sessions = new Set<Session<SessionState>>();
5566

56-
constructor() {
67+
constructor(options: ChannelOptions<State> = {}) {
5768
super();
69+
70+
this.state = options.state ?? ({} as State);
5871
}
5972

6073
/**

src/Session.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,25 @@ describe("connection", () => {
265265
}));
266266
});
267267

268+
describe("state", () => {
269+
const givenState = {id: "123"};
270+
271+
it("can set the initial state in options", () =>
272+
new Promise<void>((done) => {
273+
server.on("request", async (req, res) => {
274+
const session = new Session(req, res, {state: givenState});
275+
276+
await waitForConnect(session);
277+
278+
expect(session.state.id).toBe(givenState.id);
279+
280+
done();
281+
});
282+
283+
eventsource = new EventSource(url);
284+
}));
285+
});
286+
268287
describe("retry", () => {
269288
it("writes an initial retry field by default", () =>
270289
new Promise<void>((done) => {

src/Session.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import {serialize, SerializerFunction} from "./lib/serialize";
1313
import {sanitize, SanitizerFunction} from "./lib/sanitize";
1414
import {SseError} from "./lib/SseError";
1515

16-
interface SessionOptions
17-
extends Pick<EventBufferOptions, "serializer" | "sanitizer"> {
16+
interface SessionOptions<
17+
State extends Record<string, unknown> = DefaultSessionState
18+
> extends Pick<EventBufferOptions, "serializer" | "sanitizer"> {
1819
/**
1920
* Whether to trust or ignore the last event ID given by the client in the `Last-Event-ID` request header.
2021
*
@@ -64,6 +65,13 @@ interface SessionOptions
6465
* Additional headers to be sent along with the response.
6566
*/
6667
headers?: OutgoingHttpHeaders;
68+
69+
/**
70+
* Custom state for this session.
71+
*
72+
* Use this object to safely store information related to the session and user.
73+
*/
74+
state?: State;
6775
}
6876

6977
interface DefaultSessionState {
@@ -120,7 +128,7 @@ class Session<
120128
* Use [module augmentation and declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
121129
* to safely add new properties to the `DefaultSessionState` interface.
122130
*/
123-
state = {} as State;
131+
state: State;
124132

125133
private buffer: EventBuffer;
126134

@@ -151,12 +159,11 @@ class Session<
151159
constructor(
152160
req: Http1ServerRequest | Http2ServerRequest,
153161
res: Http1ServerResponse | Http2ServerResponse,
154-
options: SessionOptions = {}
162+
options: SessionOptions<State> = {}
155163
) {
156164
super();
157165

158166
this.req = req;
159-
160167
this.res = res;
161168

162169
const serializer = options.serializer ?? serialize;
@@ -179,6 +186,8 @@ class Session<
179186

180187
this.headers = options.headers ?? {};
181188

189+
this.state = options.state ?? ({} as State);
190+
182191
this.req.once("close", this.onDisconnected);
183192
this.res.once("close", this.onDisconnected);
184193

0 commit comments

Comments
 (0)