@chidchanun/bcp 0.2.12 → 0.2.14

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.
@@ -0,0 +1,447 @@
1
+ # Realtime Platform
2
+
3
+ BCP Framework `0.2.13` adds the server-only `bcp/realtime` entrypoint for application realtime delivery without forcing one WebSocket or pub/sub provider.
4
+
5
+ The platform separates four concerns:
6
+
7
+ ```text
8
+ connection/session
9
+ |
10
+ +--> channel membership
11
+ +--> authentication / authorization
12
+ +--> heartbeat
13
+ |
14
+ v
15
+ RealtimeHub
16
+ |
17
+ +--> RealtimeBroker
18
+ | -> cross-hub delivery
19
+ |
20
+ +--> RealtimePresenceStore
21
+ | -> room/channel presence
22
+ |
23
+ +--> RealtimeSocket adapter
24
+ | -> WebSocket provider integration
25
+ |
26
+ +--> SSE Response
27
+ -> built-in HTTP streaming
28
+ ```
29
+
30
+ ## Create a hub
31
+
32
+ ```ts
33
+ import {
34
+ createRealtime,
35
+ } from "bcp/realtime";
36
+
37
+ export const realtime =
38
+ createRealtime();
39
+ ```
40
+
41
+ The default broker and presence store are process-local and intended for development, tests and single-process deployments.
42
+
43
+ ## Channels and rooms
44
+
45
+ BCP treats rooms as named channels.
46
+
47
+ ```ts
48
+ const connection =
49
+ await realtime.connect();
50
+
51
+ await connection.join(
52
+ "orders:42"
53
+ );
54
+ ```
55
+
56
+ Broadcast to everyone currently joined:
57
+
58
+ ```ts
59
+ await realtime.broadcast(
60
+ "orders:42",
61
+ "order.updated",
62
+ {
63
+ status: "paid",
64
+ }
65
+ );
66
+ ```
67
+
68
+ You can exclude one connection when echo suppression is needed:
69
+
70
+ ```ts
71
+ await realtime.broadcast(
72
+ "chat:general",
73
+ "chat.message",
74
+ message,
75
+ {
76
+ excludeConnectionId:
77
+ sender.id,
78
+ }
79
+ );
80
+ ```
81
+
82
+ ## Presence
83
+
84
+ Presence is stored separately from message delivery.
85
+
86
+ ```ts
87
+ await connection.join(
88
+ "project:7",
89
+ {
90
+ presence: {
91
+ status: "online",
92
+ },
93
+ }
94
+ );
95
+
96
+ const members =
97
+ await realtime.members(
98
+ "project:7"
99
+ );
100
+ ```
101
+
102
+ The default `createMemoryRealtimePresenceStore()` is local-only. Multi-instance deployments should use a shared store whose `join`, `leave`, `leaveConnection`, `touch` and `list` operations are visible to every instance.
103
+
104
+ ## Authentication
105
+
106
+ A realtime connection may be authenticated when it is created:
107
+
108
+ ```ts
109
+ const realtime =
110
+ createRealtime({
111
+ authenticate:
112
+ async ({ request }) => {
113
+ if (!request) {
114
+ return null;
115
+ }
116
+
117
+ return loadRealtimeUser(
118
+ request
119
+ );
120
+ },
121
+
122
+ getUserId:
123
+ user => user.id,
124
+ });
125
+ ```
126
+
127
+ The authenticated value is available as:
128
+
129
+ ```ts
130
+ connection.user
131
+ ```
132
+
133
+ BCP does not automatically turn a failed authentication callback into a rejected connection. Applications that require authentication should enforce that policy in their authentication callback/adapter or channel authorization policy.
134
+
135
+ ## Channel authorization
136
+
137
+ ```ts
138
+ const realtime =
139
+ createRealtime({
140
+ authorizeChannel:
141
+ async ({
142
+ connection,
143
+ channel,
144
+ }) => {
145
+ if (
146
+ channel.startsWith(
147
+ "private:"
148
+ )
149
+ ) {
150
+ return Boolean(
151
+ connection.user
152
+ );
153
+ }
154
+
155
+ return true;
156
+ },
157
+ });
158
+ ```
159
+
160
+ Authorization is evaluated before a connection joins a channel.
161
+
162
+ ## Client-originated events
163
+
164
+ Register a server event handler:
165
+
166
+ ```ts
167
+ realtime.on(
168
+ "chat.message",
169
+ async ({
170
+ connection,
171
+ channel,
172
+ payload,
173
+ }) => {
174
+ await saveMessage(
175
+ payload
176
+ );
177
+
178
+ await realtime.broadcast(
179
+ channel,
180
+ "chat.message",
181
+ payload,
182
+ {
183
+ excludeConnectionId:
184
+ connection.id,
185
+ }
186
+ );
187
+ }
188
+ );
189
+ ```
190
+
191
+ Calling `connection.emit()` runs registered server handlers. It does not automatically broadcast the message. This keeps application authorization and mutation logic explicit.
192
+
193
+ ## WebSocket adapter contract
194
+
195
+ BCP does not install `ws`, Socket.IO, uWebSockets.js or another WebSocket server package.
196
+
197
+ Adapt your selected provider to this shape:
198
+
199
+ ```ts
200
+ interface RealtimeSocket {
201
+ send(
202
+ data: string
203
+ ): void | Promise<void>;
204
+
205
+ close?(
206
+ code?: number,
207
+ reason?: string
208
+ ): void | Promise<void>;
209
+
210
+ onMessage(
211
+ listener: (
212
+ data: string
213
+ ) => void | Promise<void>
214
+ ): () => void;
215
+
216
+ onClose(
217
+ listener: () =>
218
+ void | Promise<void>
219
+ ): () => void;
220
+
221
+ onError?(
222
+ listener: (
223
+ error: unknown
224
+ ) => void | Promise<void>
225
+ ): () => void;
226
+ }
227
+ ```
228
+
229
+ Attach it:
230
+
231
+ ```ts
232
+ const connection =
233
+ await realtime.attachSocket(
234
+ socketAdapter,
235
+ {
236
+ request,
237
+ }
238
+ );
239
+ ```
240
+
241
+ ### Socket message protocol
242
+
243
+ Inbound JSON messages use these shapes:
244
+
245
+ ```json
246
+ {
247
+ "type": "join",
248
+ "channel": "chat:general",
249
+ "presence": {
250
+ "status": "online"
251
+ }
252
+ }
253
+ ```
254
+
255
+ ```json
256
+ {
257
+ "type": "leave",
258
+ "channel": "chat:general"
259
+ }
260
+ ```
261
+
262
+ ```json
263
+ {
264
+ "type": "event",
265
+ "channel": "chat:general",
266
+ "event": "chat.message",
267
+ "payload": {
268
+ "text": "hello"
269
+ }
270
+ }
271
+ ```
272
+
273
+ Heartbeat input:
274
+
275
+ ```json
276
+ {
277
+ "type": "ping"
278
+ }
279
+ ```
280
+
281
+ The server answers with a `realtime.pong` event.
282
+
283
+ ## Server-Sent Events
284
+
285
+ SSE does not require a WebSocket provider.
286
+
287
+ ```ts
288
+ export function GET(
289
+ request: Request
290
+ ) {
291
+ return realtime.sse(
292
+ "jobs:42",
293
+ {
294
+ signal:
295
+ request.signal,
296
+ }
297
+ );
298
+ }
299
+ ```
300
+
301
+ Equivalent standalone helper:
302
+
303
+ ```ts
304
+ import {
305
+ createRealtimeSseResponse,
306
+ } from "bcp/realtime";
307
+
308
+ return createRealtimeSseResponse(
309
+ realtime,
310
+ "jobs:42",
311
+ {
312
+ signal:
313
+ request.signal,
314
+ }
315
+ );
316
+ ```
317
+
318
+ The response includes:
319
+
320
+ ```text
321
+ Content-Type: text/event-stream
322
+ Cache-Control: no-cache, no-transform
323
+ Connection: keep-alive
324
+ X-Accel-Buffering: no
325
+ ```
326
+
327
+ Optional settings include `retryMs`, `keepAliveMs`, an event-name filter and additional response headers.
328
+
329
+ ## Heartbeat and stale connections
330
+
331
+ ```ts
332
+ const realtime =
333
+ createRealtime({
334
+ heartbeatTimeoutMs:
335
+ 60_000,
336
+ });
337
+
338
+ const heartbeat =
339
+ realtime.startHeartbeat({
340
+ intervalMs: 20_000,
341
+ });
342
+ ```
343
+
344
+ The heartbeat runner sends `realtime.ping` and calls `sweepStale()` periodically.
345
+
346
+ A client `ping` message or any valid socket message updates the connection's `lastSeenAt` timestamp.
347
+
348
+ Shutdown:
349
+
350
+ ```ts
351
+ await heartbeat.stop();
352
+ await realtime.close();
353
+ ```
354
+
355
+ ## Cross-instance broker
356
+
357
+ `RealtimeBroker` is the pub/sub boundary:
358
+
359
+ ```ts
360
+ interface RealtimeBroker {
361
+ publish(
362
+ message: RealtimeEnvelope
363
+ ): Promise<void>;
364
+
365
+ subscribe(
366
+ listener: (
367
+ message: RealtimeEnvelope
368
+ ) => void | Promise<void>
369
+ ): () => void;
370
+ }
371
+ ```
372
+
373
+ The built-in memory broker can be shared by multiple hubs in one process and is useful for tests.
374
+
375
+ For multiple Node processes or containers, implement the broker using shared infrastructure such as Redis Pub/Sub, NATS or another application-selected service.
376
+
377
+ A production topology may look like:
378
+
379
+ ```text
380
+ Browser A -> App A ----\
381
+ Shared Broker
382
+ Browser B -> App B ----/
383
+ | |
384
+ +---- Shared Presence Store
385
+ ```
386
+
387
+ BCP intentionally does not install or own a Redis/NATS connection.
388
+
389
+ ## Jobs, workflows and events
390
+
391
+ Realtime is designed to be the delivery edge for existing backend systems.
392
+
393
+ Job progress:
394
+
395
+ ```ts
396
+ await realtime.broadcast(
397
+ `jobs:${job.id}`,
398
+ "job.progress",
399
+ {
400
+ progress: 60,
401
+ }
402
+ );
403
+ ```
404
+
405
+ Workflow progress:
406
+
407
+ ```ts
408
+ await realtime.broadcast(
409
+ `workflow:${run.id}`,
410
+ "workflow.updated",
411
+ run
412
+ );
413
+ ```
414
+
415
+ Outbox/event delivery can broadcast after durable publication/consumption according to application semantics.
416
+
417
+ Do not treat a transient realtime broadcast as a durable event store. Important business events should continue to use the transactional outbox/durable queue path.
418
+
419
+ ## Delivery semantics
420
+
421
+ Realtime delivery is transient. A disconnected client can miss a broadcast.
422
+
423
+ Use:
424
+
425
+ - database/outbox for durable business facts,
426
+ - jobs/workflows for durable background execution,
427
+ - realtime for live client delivery.
428
+
429
+ For reconnect catch-up, expose application state or an event history endpoint and let the client refetch after reconnecting.
430
+
431
+ ## Security
432
+
433
+ `bcp/realtime` is server-only.
434
+
435
+ Applications should:
436
+
437
+ - authenticate connections where required,
438
+ - authorize every private channel join,
439
+ - validate client event payloads,
440
+ - rate-limit untrusted client messages when appropriate,
441
+ - avoid placing secrets in broadcast payloads,
442
+ - use TLS (`wss:` / HTTPS) in production,
443
+ - apply origin checks at the WebSocket upgrade boundary when browser-origin restrictions are required.
444
+
445
+ ## 0.2.13 scope
446
+
447
+ `0.2.13` provides the provider-neutral realtime runtime contract. It does not add a built-in Redis broker, WebSocket server dependency, durable message replay or browser client SDK.
@@ -0,0 +1,122 @@
1
+ # BCP Framework 0.2.13
2
+
3
+ Status: unreleased development target.
4
+
5
+ ## Realtime Platform
6
+
7
+ `0.2.13` introduces `bcp/realtime`, a server-only provider-neutral realtime layer for channels/rooms, presence, cross-hub broker delivery, WebSocket adapter integration, SSE responses and heartbeat lifecycle.
8
+
9
+ ## Public API
10
+
11
+ New entrypoint:
12
+
13
+ ```ts
14
+ import {
15
+ createMemoryRealtimeBroker,
16
+ createMemoryRealtimePresenceStore,
17
+ createRealtime,
18
+ createRealtimeSseResponse,
19
+ } from "bcp/realtime";
20
+ ```
21
+
22
+ Key contracts:
23
+
24
+ - `RealtimeHub`
25
+ - `RealtimeConnection`
26
+ - `RealtimeBroker`
27
+ - `RealtimePresenceStore`
28
+ - `RealtimeSocket`
29
+ - `RealtimeEnvelope`
30
+ - `RealtimeEventHandler`
31
+ - `RealtimeAuthenticate`
32
+ - `RealtimeAuthorizeChannel`
33
+
34
+ ## Channels and presence
35
+
36
+ Connections can join/leave named channels and attach typed presence metadata. Hub broadcasts deliver only to local connections that joined the channel, while `RealtimeBroker` distributes envelopes across hubs.
37
+
38
+ `createMemoryRealtimeBroker()` and `createMemoryRealtimePresenceStore()` provide deterministic local/test implementations.
39
+
40
+ ## Authentication and authorization
41
+
42
+ `createRealtime()` accepts:
43
+
44
+ - `authenticate` for request/data-based connection identity,
45
+ - `getUserId` for presence identity,
46
+ - `authorizeChannel` for private-channel join checks.
47
+
48
+ Authentication policy remains application-owned so public and authenticated realtime endpoints can share the same runtime primitives.
49
+
50
+ ## WebSocket integration
51
+
52
+ BCP does not add a mandatory WebSocket dependency. `RealtimeSocket` defines the minimal transport surface required by `attachSocket()`.
53
+
54
+ The built-in JSON protocol supports:
55
+
56
+ - `join`
57
+ - `leave`
58
+ - `event`
59
+ - `ping`
60
+
61
+ Valid client messages refresh connection liveness. Ping receives `realtime.pong`.
62
+
63
+ ## Server-Sent Events
64
+
65
+ `RealtimeHub.sse()` and `createRealtimeSseResponse()` produce Web-standard streaming `Response` objects with event-stream headers, optional retry hints, event filtering and keep-alive comments.
66
+
67
+ ## Heartbeat lifecycle
68
+
69
+ `startHeartbeat()` periodically sends `realtime.ping` and sweeps stale connections. `sweepStale()` can also be invoked manually for deterministic infrastructure loops and tests.
70
+
71
+ ## Multi-instance contract
72
+
73
+ The memory broker and presence store are process-local.
74
+
75
+ Production multi-instance applications can implement shared `RealtimeBroker` and `RealtimePresenceStore` adapters using infrastructure such as Redis, NATS or another service without coupling BCP to one provider/client library.
76
+
77
+ ## Integration model
78
+
79
+ Realtime complements, but does not replace, durable platform primitives:
80
+
81
+ ```text
82
+ Database / Transactional Outbox
83
+ |
84
+ v
85
+ Durable Jobs / Workflow
86
+ |
87
+ v
88
+ Realtime Hub
89
+ |
90
+ WebSocket / SSE
91
+ ```
92
+
93
+ Realtime broadcasts are transient. Applications that require reconnect catch-up should refetch durable state/history after reconnect.
94
+
95
+ ## Package/runtime
96
+
97
+ - `bcp/realtime` is server-only.
98
+ - Client page/island graphs reject direct realtime imports.
99
+ - publish preparation compiles `packages/client/src/realtime.ts` to `realtime.mjs`.
100
+ - no new runtime dependency is required.
101
+
102
+ ## Validation
103
+
104
+ The release adds unit/package coverage for:
105
+
106
+ - cross-hub broker broadcasts,
107
+ - channel membership,
108
+ - presence metadata,
109
+ - authentication identity,
110
+ - channel authorization,
111
+ - socket join/event/ping protocol,
112
+ - stale heartbeat cleanup,
113
+ - SSE streaming,
114
+ - server-only boundary enforcement,
115
+ - compiled package runtime execution,
116
+ - public-entrypoint manifest parity.
117
+
118
+ ## Compatibility
119
+
120
+ Previous baseline: `0.2.12`.
121
+
122
+ There are no intentional breaking changes from `0.2.12`.