@kyneta/sse-transport 1.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 +21 -0
- package/README.md +334 -0
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/chunk-7D4SUZUM.js.map +1 -0
- package/dist/chunk-TR4Y3HFB.js +255 -0
- package/dist/chunk-TR4Y3HFB.js.map +1 -0
- package/dist/client.d.ts +144 -0
- package/dist/client.js +460 -0
- package/dist/client.js.map +1 -0
- package/dist/express.d.ts +135 -0
- package/dist/express.js +23021 -0
- package/dist/express.js.map +1 -0
- package/dist/server-transport-BrMRLsmp.d.ts +180 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +1 -0
- package/dist/types-BTgljZGe.d.ts +83 -0
- package/package.json +60 -0
- package/src/__tests__/client-state-machine.test.ts +201 -0
- package/src/__tests__/connection.test.ts +184 -0
- package/src/__tests__/sse-handler.test.ts +145 -0
- package/src/client-state-machine.ts +69 -0
- package/src/client-transport.ts +722 -0
- package/src/client.ts +30 -0
- package/src/connection.ts +181 -0
- package/src/express-router.ts +231 -0
- package/src/express.ts +29 -0
- package/src/server-transport.ts +229 -0
- package/src/server.ts +33 -0
- package/src/sse-handler.ts +116 -0
- package/src/types.ts +108 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// server — barrel export for @kyneta/sse-network-adapter/server.
|
|
2
|
+
//
|
|
3
|
+
// This is the server-side entry point. It exports everything needed
|
|
4
|
+
// to create an SSE server adapter with any framework.
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Server adapter
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
SseServerTransport,
|
|
12
|
+
type SseServerTransportOptions,
|
|
13
|
+
} from "./server-transport.js"
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Connection
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
DEFAULT_FRAGMENT_THRESHOLD,
|
|
21
|
+
SseConnection,
|
|
22
|
+
type SseConnectionConfig,
|
|
23
|
+
} from "./connection.js"
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Shared types
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
DisconnectReason,
|
|
31
|
+
SseConnectionHandle,
|
|
32
|
+
SseConnectionResult,
|
|
33
|
+
} from "./types.js"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// sse-handler — framework-agnostic SSE POST handler (Functional Core).
|
|
2
|
+
//
|
|
3
|
+
// This module provides pure functions for handling text POST requests
|
|
4
|
+
// in SSE adapters. Framework-specific adapters (Express, Hono, etc.)
|
|
5
|
+
// use these functions and handle the HTTP-specific concerns.
|
|
6
|
+
//
|
|
7
|
+
// Design: Functional Core / Imperative Shell
|
|
8
|
+
// - This module parses and decodes, returning a result describing what to do
|
|
9
|
+
// - Framework adapters execute side effects (delivering messages, sending responses)
|
|
10
|
+
//
|
|
11
|
+
// The POST body is a text wire frame string (JSON array with "0c"/"0f" prefix).
|
|
12
|
+
// Decoding is two-step:
|
|
13
|
+
// 1. TextReassembler.receive(body) → Frame<string>
|
|
14
|
+
// 2. JSON.parse(frame.content.payload) → textCodec.decode(parsed) → ChannelMsg[]
|
|
15
|
+
|
|
16
|
+
import type { ChannelMsg } from "@kyneta/exchange"
|
|
17
|
+
import { type TextReassembler, textCodec } from "@kyneta/wire"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Result types
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Response to send back to the client after processing a POST.
|
|
25
|
+
*/
|
|
26
|
+
export interface SsePostResponse {
|
|
27
|
+
status: 200 | 202 | 400
|
|
28
|
+
body: { ok: true } | { pending: true } | { error: string }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result of parsing a text POST body.
|
|
33
|
+
*
|
|
34
|
+
* Discriminated union describing what happened:
|
|
35
|
+
* - "messages": Complete message(s) decoded, ready to deliver
|
|
36
|
+
* - "pending": Fragment received, waiting for more
|
|
37
|
+
* - "error": Decode/reassembly error
|
|
38
|
+
*/
|
|
39
|
+
export type SsePostResult =
|
|
40
|
+
| { type: "messages"; messages: ChannelMsg[]; response: SsePostResponse }
|
|
41
|
+
| { type: "pending"; response: SsePostResponse }
|
|
42
|
+
| { type: "error"; response: SsePostResponse }
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// parseTextPostBody
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a text POST body through the reassembler.
|
|
50
|
+
*
|
|
51
|
+
* This is the functional core of POST handling. It:
|
|
52
|
+
* 1. Passes the body through the reassembler (handles fragmentation)
|
|
53
|
+
* 2. If complete, decodes the text frame payload to ChannelMsg(s)
|
|
54
|
+
* 3. Returns a result describing what happened
|
|
55
|
+
*
|
|
56
|
+
* The caller (framework adapter) executes side effects based on the result.
|
|
57
|
+
*
|
|
58
|
+
* @param reassembler - The connection's text fragment reassembler
|
|
59
|
+
* @param body - Text wire frame string (JSON array with "0c"/"0f" prefix)
|
|
60
|
+
* @returns Result describing what to do
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // In Express router (imperative shell)
|
|
65
|
+
* const result = parseTextPostBody(connection.reassembler, req.body)
|
|
66
|
+
*
|
|
67
|
+
* if (result.type === "messages") {
|
|
68
|
+
* for (const msg of result.messages) {
|
|
69
|
+
* connection.receive(msg)
|
|
70
|
+
* }
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* res.status(result.response.status).json(result.response.body)
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function parseTextPostBody(
|
|
77
|
+
reassembler: TextReassembler,
|
|
78
|
+
body: string,
|
|
79
|
+
): SsePostResult {
|
|
80
|
+
const result = reassembler.receive(body)
|
|
81
|
+
|
|
82
|
+
if (result.status === "complete") {
|
|
83
|
+
try {
|
|
84
|
+
// Two-step decode:
|
|
85
|
+
// 1. Frame<string> → extract payload string
|
|
86
|
+
// 2. JSON.parse → textCodec.decode → ChannelMsg[]
|
|
87
|
+
const parsed = JSON.parse(result.frame.content.payload)
|
|
88
|
+
const messages = textCodec.decode(parsed)
|
|
89
|
+
return {
|
|
90
|
+
type: "messages",
|
|
91
|
+
messages,
|
|
92
|
+
response: { status: 200, body: { ok: true } },
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const errorMessage = err instanceof Error ? err.message : "decode_failed"
|
|
96
|
+
return {
|
|
97
|
+
type: "error",
|
|
98
|
+
response: { status: 400, body: { error: errorMessage } },
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else if (result.status === "pending") {
|
|
102
|
+
return {
|
|
103
|
+
type: "pending",
|
|
104
|
+
response: { status: 202, body: { pending: true } },
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// result.status === "error"
|
|
108
|
+
return {
|
|
109
|
+
type: "error",
|
|
110
|
+
response: {
|
|
111
|
+
status: 400,
|
|
112
|
+
body: { error: result.error.type },
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// types — SSE-specific types for @kyneta/sse-network-adapter.
|
|
2
|
+
//
|
|
3
|
+
// SSE has a simpler lifecycle than WebSocket — no "ready" state because
|
|
4
|
+
// there's no transport-level handshake. The connection is usable as soon
|
|
5
|
+
// as EventSource.onopen fires.
|
|
6
|
+
//
|
|
7
|
+
// DisconnectReason is SSE-specific: no "closed" variant (SSE doesn't have
|
|
8
|
+
// close codes) and no "not-started" variant (no "ready" gate).
|
|
9
|
+
|
|
10
|
+
import type { PeerId } from "@kyneta/exchange"
|
|
11
|
+
|
|
12
|
+
// Re-export specialized transition types from generic state machine
|
|
13
|
+
export type { StateTransition, TransitionListener } from "@kyneta/exchange"
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Disconnect reason
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discriminated union describing why an SSE connection was lost.
|
|
21
|
+
*
|
|
22
|
+
* Unlike WebSocket's DisconnectReason, SSE does not have:
|
|
23
|
+
* - `{ type: "closed"; code; reason }` — SSE has no close codes
|
|
24
|
+
* - `{ type: "not-started" }` — SSE has no "ready" gate
|
|
25
|
+
*/
|
|
26
|
+
export type DisconnectReason =
|
|
27
|
+
| { type: "intentional" }
|
|
28
|
+
| { type: "error"; error: Error }
|
|
29
|
+
| { type: "max-retries-exceeded"; attempts: number }
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Connection state (for client adapter observability)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* All possible states of the SSE client.
|
|
37
|
+
*
|
|
38
|
+
* State machine transitions (4 states, no "ready"):
|
|
39
|
+
* ```
|
|
40
|
+
* disconnected → connecting → connected
|
|
41
|
+
* ↓ ↓
|
|
42
|
+
* reconnecting ← ─ ─┘
|
|
43
|
+
* ↓
|
|
44
|
+
* connecting (retry)
|
|
45
|
+
* ↓
|
|
46
|
+
* disconnected (max retries)
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export type SseClientState =
|
|
50
|
+
| { status: "disconnected"; reason?: DisconnectReason }
|
|
51
|
+
| { status: "connecting"; attempt: number }
|
|
52
|
+
| { status: "connected" }
|
|
53
|
+
| { status: "reconnecting"; attempt: number; nextAttemptMs: number }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A state transition event for SSE client states.
|
|
57
|
+
* Specialized from the generic `StateTransition<S>`.
|
|
58
|
+
*/
|
|
59
|
+
export type { StateTransition as SseClientStateTransition } from "@kyneta/exchange"
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Connection handle — used by server adapter
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handle for an active SSE connection (server-side).
|
|
67
|
+
*/
|
|
68
|
+
export interface SseConnectionHandle {
|
|
69
|
+
/** The peer ID for this connection. */
|
|
70
|
+
readonly peerId: PeerId
|
|
71
|
+
/** The channel ID for this connection. */
|
|
72
|
+
readonly channelId: number
|
|
73
|
+
/** Disconnect this connection. */
|
|
74
|
+
disconnect(): void
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Result of registering an SSE connection on the server.
|
|
79
|
+
*/
|
|
80
|
+
export interface SseConnectionResult {
|
|
81
|
+
/** The connection handle for managing this peer. */
|
|
82
|
+
connection: SseConnectionHandle
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Client options
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Lifecycle event callbacks for the SSE client.
|
|
91
|
+
*/
|
|
92
|
+
export interface SseClientLifecycleEvents {
|
|
93
|
+
/** Called on every state transition (delivered async via microtask). */
|
|
94
|
+
onStateChange?: (transition: {
|
|
95
|
+
from: SseClientState
|
|
96
|
+
to: SseClientState
|
|
97
|
+
timestamp: number
|
|
98
|
+
}) => void
|
|
99
|
+
|
|
100
|
+
/** Called when the connection is lost. */
|
|
101
|
+
onDisconnect?: (reason: DisconnectReason) => void
|
|
102
|
+
|
|
103
|
+
/** Called when a reconnection attempt is scheduled. */
|
|
104
|
+
onReconnecting?: (attempt: number, nextAttemptMs: number) => void
|
|
105
|
+
|
|
106
|
+
/** Called when reconnection succeeds after a previous connection. */
|
|
107
|
+
onReconnected?: () => void
|
|
108
|
+
}
|