@greendrake/rpc-server 0.5.0 → 0.6.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/README.md +2 -0
- package/package.json +3 -3
- package/src/main.ts +1 -1
- package/src/registry.ts +28 -2
package/README.md
CHANGED
|
@@ -117,6 +117,8 @@ registry.broadcastData('telemetry', reading) // the bare {type, data} form
|
|
|
117
117
|
|
|
118
118
|
Push-producing code takes the `EventSink` interface (`emitToUser` + `broadcastEvent`) rather than the class, so it can be driven by anything. On the client, `@greendrake/rpc`'s `liveBinding` subscribes to these, folds each push in or re-reads, and re-reads again after a dropped socket returns.
|
|
119
119
|
|
|
120
|
+
The registry is also an `Audience`: `size` is how many sockets are open, and `onPresence(listener)` calls the listener with `true` as the first one opens and `false` as the last one closes, returning the function that stops it. It is for a producer whose pushes cost something to produce — a reading taken from somewhere slow — and are worth producing only while somebody is connected to be told.
|
|
121
|
+
|
|
120
122
|
## Error codes
|
|
121
123
|
|
|
122
124
|
Codes, not sentences: the client keys on them. A handler's own outcomes are `throw new ApiError(code, details?)`; anything else it throws is a fault, logged and answered `INTERNAL_ERROR`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greendrake/rpc-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "RPC server over JSON for bun: method dispatch with auth, nonce and Standard Schema argument validation, HTTP and WebSocket transports, connection registry and server push",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eugene Greendrake <eugene@greendrake.nz>",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
},
|
|
35
35
|
"prettier": "@greendrake/dev-config/prettier",
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@greendrake/rpc": "0.
|
|
37
|
+
"@greendrake/rpc": "0.6.0",
|
|
38
38
|
"@standard-schema/spec": "^1.1.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@greendrake/dev-config": "0.
|
|
41
|
+
"@greendrake/dev-config": "0.6.0",
|
|
42
42
|
"@types/bun": "^1.3.5",
|
|
43
43
|
"typescript": "6.0.3"
|
|
44
44
|
}
|
package/src/main.ts
CHANGED
|
@@ -6,5 +6,5 @@ export { bearerToken, keyAuthResolver } from './auth'
|
|
|
6
6
|
export { memoryNonceStore } from './nonce'
|
|
7
7
|
export { clientIp, httpHandler, type HttpHandlerOptions } from './http'
|
|
8
8
|
export { websocketHandler, type RpcWebSocket, type WebSocketHandlerOptions } from './ws'
|
|
9
|
-
export { ConnRegistry, type Connection, type ConnectionData, type EventSink } from './registry'
|
|
9
|
+
export { ConnRegistry, type Audience, type Connection, type ConnectionData, type EventSink } from './registry'
|
|
10
10
|
export type { Codec, RpcRequest, RpcResponse } from './types'
|
package/src/registry.ts
CHANGED
|
@@ -26,12 +26,24 @@ export interface EventSink {
|
|
|
26
26
|
broadcastEvent(event: string, data: unknown): void
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Whether anybody is connected to hear a broadcast, for a producer whose
|
|
30
|
+
// pushes cost something to produce — a reading taken from somewhere slow —
|
|
31
|
+
// and are worth producing only while somebody is there to be told.
|
|
32
|
+
export interface Audience {
|
|
33
|
+
// How many sockets are open.
|
|
34
|
+
readonly size: number
|
|
35
|
+
// Called with true as the first socket opens and false as the last one
|
|
36
|
+
// closes. The function returned stops the calls.
|
|
37
|
+
onPresence(listener: (present: boolean) => void): () => void
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
// Every open socket, and the index from principal to the sockets bound as it —
|
|
30
41
|
// a user with three tabs has three. Holding all of them as well as the index
|
|
31
42
|
// is what lets a broadcast reach anonymous connections too, which a per-user
|
|
32
43
|
// index alone cannot.
|
|
33
|
-
export class ConnRegistry implements EventSink {
|
|
44
|
+
export class ConnRegistry implements EventSink, Audience {
|
|
34
45
|
readonly #connections = new Set<Connection>()
|
|
46
|
+
readonly #presence = new Set<(present: boolean) => void>()
|
|
35
47
|
readonly #byUser = new Map<unknown, Set<Connection>>()
|
|
36
48
|
// Where each connection is currently filed, so a principal that changes
|
|
37
49
|
// mid-connection can be re-indexed without the caller tracking what it
|
|
@@ -50,7 +62,11 @@ export class ConnRegistry implements EventSink {
|
|
|
50
62
|
// called when a socket opens and again whenever an auth frame changes who
|
|
51
63
|
// it is.
|
|
52
64
|
register(conn: Connection): void {
|
|
65
|
+
const arriving = !this.#connections.has(conn)
|
|
53
66
|
this.#connections.add(conn)
|
|
67
|
+
if (arriving && this.#connections.size === 1) {
|
|
68
|
+
this.#presence.forEach(listener => listener(true))
|
|
69
|
+
}
|
|
54
70
|
const userId = conn.data.auth.userId
|
|
55
71
|
if (this.#filedUnder.get(conn) === userId) {
|
|
56
72
|
return
|
|
@@ -69,8 +85,18 @@ export class ConnRegistry implements EventSink {
|
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
unregister(conn: Connection): void {
|
|
72
|
-
this.#connections.delete(conn)
|
|
88
|
+
const leaving = this.#connections.delete(conn)
|
|
73
89
|
this.#unfile(conn)
|
|
90
|
+
if (leaving && this.#connections.size === 0) {
|
|
91
|
+
this.#presence.forEach(listener => listener(false))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onPresence(listener: (present: boolean) => void): () => void {
|
|
96
|
+
this.#presence.add(listener)
|
|
97
|
+
return () => {
|
|
98
|
+
this.#presence.delete(listener)
|
|
99
|
+
}
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
#unfile(conn: Connection): void {
|