@chidchanun/bcp 0.2.11 → 0.2.13

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "versionTarget": "0.2.11",
4
+ "versionTarget": "0.2.13",
5
5
  "releaseState": "unreleased",
6
6
  "sections": [
7
7
  {
@@ -48,16 +48,17 @@
48
48
  {
49
49
  "id": "database",
50
50
  "title": "Database",
51
- "description": "Provider-neutral MySQL, PostgreSQL and SQLite primitives, lifecycle and migrations.",
51
+ "description": "Provider-neutral MySQL, PostgreSQL and SQLite primitives, lifecycle, migrations and transactional outbox integration.",
52
52
  "pages": [
53
53
  { "route": "/docs/database", "source": "database.md", "title": "Database" },
54
- { "route": "/docs/database-migrations", "source": "database-migrations.md", "title": "Database Migrations" }
54
+ { "route": "/docs/database-migrations", "source": "database-migrations.md", "title": "Database Migrations" },
55
+ { "route": "/docs/transactional-outbox-events", "source": "transactional-outbox-events.md", "title": "Transactional Outbox & Events" }
55
56
  ]
56
57
  },
57
58
  {
58
59
  "id": "runtime",
59
60
  "title": "Runtime & Infrastructure",
60
- "description": "Middleware, durable background jobs, scheduling, workflow orchestration, observability, logging, caching, security and production hardening.",
61
+ "description": "Middleware, jobs, scheduling, workflows, event delivery, realtime channels, observability, caching, security and production hardening.",
61
62
  "pages": [
62
63
  { "route": "/docs/middleware", "source": "middleware.md", "title": "Middleware" },
63
64
  { "route": "/docs/hydration", "source": "hydration.md", "title": "Hydration" },
@@ -67,6 +68,7 @@
67
68
  { "route": "/docs/job-scheduling", "source": "job-scheduling.md", "title": "Job Scheduling Platform" },
68
69
  { "route": "/docs/durable-jobs", "source": "durable-jobs.md", "title": "Durable Jobs Platform" },
69
70
  { "route": "/docs/workflow-orchestration", "source": "workflow-orchestration.md", "title": "Workflow Orchestration" },
71
+ { "route": "/docs/realtime-platform", "source": "realtime-platform.md", "title": "Realtime Platform" },
70
72
  { "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
71
73
  { "route": "/docs/security", "source": "security.md", "title": "Security" },
72
74
  { "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
@@ -112,7 +114,9 @@
112
114
  }
113
115
  ],
114
116
  "releases": [
115
- { "route": "/releases/0.2.11", "source": "releases/0.2.11.md", "version": "0.2.11", "state": "unreleased" },
117
+ { "route": "/releases/0.2.13", "source": "releases/0.2.13.md", "version": "0.2.13", "state": "unreleased" },
118
+ { "route": "/releases/0.2.12", "source": "releases/0.2.12.md", "version": "0.2.12" },
119
+ { "route": "/releases/0.2.11", "source": "releases/0.2.11.md", "version": "0.2.11" },
116
120
  { "route": "/releases/0.2.10", "source": "releases/0.2.10.md", "version": "0.2.10" },
117
121
  { "route": "/releases/0.2.9", "source": "releases/0.2.9.md", "version": "0.2.9" },
118
122
  { "route": "/releases/0.2.8", "source": "releases/0.2.8.md", "version": "0.2.8" },
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "version": "0.2.11",
4
+ "version": "0.2.13",
5
5
  "releaseState": "unreleased",
6
- "baseline": "workflow-orchestration",
6
+ "baseline": "realtime-platform",
7
7
  "runtime": {
8
8
  "node": ">=24.11.0",
9
9
  "react": "19",
@@ -21,6 +21,8 @@
21
21
  "bcp/auth",
22
22
  "bcp/jobs",
23
23
  "bcp/workflow",
24
+ "bcp/events",
25
+ "bcp/realtime",
24
26
  "bcp/observability",
25
27
  "bcp/server",
26
28
  "bcp/server-only",
@@ -100,6 +102,25 @@
100
102
  "workflowDelays": true,
101
103
  "workflowCompensation": true,
102
104
  "workflowQueueExecution": true,
105
+ "transactionalOutboxEvents": true,
106
+ "outboxStoreContract": true,
107
+ "sqlOutboxStore": true,
108
+ "outboxDispatcher": true,
109
+ "outboxDispatcherLeases": true,
110
+ "outboxRetryRecovery": true,
111
+ "outboxRetentionCleanup": true,
112
+ "outboxStatistics": true,
113
+ "eventBus": true,
114
+ "outboxJobDelivery": true,
115
+ "realtimePlatform": true,
116
+ "realtimeBrokerContract": true,
117
+ "realtimePresenceStore": true,
118
+ "realtimeChannels": true,
119
+ "realtimeChannelAuthorization": true,
120
+ "realtimeSocketAdapter": true,
121
+ "realtimeServerSentEvents": true,
122
+ "realtimeHeartbeats": true,
123
+ "realtimeCrossHubBroadcast": true,
103
124
  "databaseMigrations": true,
104
125
  "databaseAdapterContract": true,
105
126
  "databasePostgresql": true,
@@ -139,7 +160,7 @@
139
160
  "s3-compatible"
140
161
  ],
141
162
  "compatibility": {
142
- "previousBaseline": "0.2.10",
163
+ "previousBaseline": "0.2.12",
143
164
  "intentionalBreakingChangesFromPreviousBaseline": false,
144
165
  "migrationGuide": "migration-0.2.md"
145
166
  },
@@ -160,7 +181,9 @@
160
181
  "jobScheduling": "job-scheduling.md",
161
182
  "durableJobs": "durable-jobs.md",
162
183
  "workflowOrchestration": "workflow-orchestration.md",
184
+ "transactionalOutboxEvents": "transactional-outbox-events.md",
185
+ "realtimePlatform": "realtime-platform.md",
163
186
  "migrationGuide": "migration-0.2.md",
164
- "releaseNotes": "releases/0.2.11.md"
187
+ "releaseNotes": "releases/0.2.13.md"
165
188
  }
166
189
  }
@@ -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,147 @@
1
+ # BCP Framework 0.2.12 — Transactional Outbox & Events
2
+
3
+ Release state: unreleased development target.
4
+
5
+ `0.2.12` adds a server-only transactional outbox and event-delivery platform that bridges BCP Database transactions with durable jobs or application-owned publishers without performing external side effects inside the business transaction.
6
+
7
+ ## New public entrypoint
8
+
9
+ ```ts
10
+ import {
11
+ createEventBus,
12
+ createMemoryOutboxStore,
13
+ createOutboxDispatcher,
14
+ createOutboxMigrationSql,
15
+ createSqlOutboxStore,
16
+ createTransactionalOutbox,
17
+ } from "bcp/events";
18
+ ```
19
+
20
+ `bcp/events` is server-only and receives the same browser/client boundary protection as `bcp/database`, `bcp/jobs` and `bcp/workflow`.
21
+
22
+ ## Transaction-bound outbox writes
23
+
24
+ Application code can insert business rows and outbox rows through the same `TransactionDatabase`:
25
+
26
+ ```ts
27
+ await db.transaction(async tx => {
28
+ await tx.execute(
29
+ "INSERT INTO orders ..."
30
+ );
31
+
32
+ await outbox.publish(
33
+ tx,
34
+ "order.created",
35
+ {
36
+ orderId: 42,
37
+ }
38
+ );
39
+ });
40
+ ```
41
+
42
+ `createSqlOutboxStore()` writes through the caller transaction rather than the root database connection.
43
+
44
+ ## SQL providers
45
+
46
+ Provider-specific migration SQL is available for:
47
+
48
+ ```text
49
+ mysql
50
+ postgresql
51
+ sqlite
52
+ ```
53
+
54
+ The SQL store persists:
55
+
56
+ - event ID/type/payload,
57
+ - metadata,
58
+ - correlation/causation/aggregate IDs,
59
+ - state,
60
+ - attempts/max attempts,
61
+ - created/available/processing/published/failed timestamps,
62
+ - delivery errors,
63
+ - lease owner/expiry.
64
+
65
+ ## Dispatcher
66
+
67
+ `createOutboxDispatcher()` adds:
68
+
69
+ - batched claiming,
70
+ - dispatcher leases,
71
+ - stale-lease recovery,
72
+ - retry/backoff,
73
+ - terminal failed state,
74
+ - durable job-queue handoff,
75
+ - custom publisher delivery,
76
+ - in-process EventBus delivery,
77
+ - polling runner lifecycle,
78
+ - cleanup/statistics through the store.
79
+
80
+ ## Durable job handoff
81
+
82
+ With `queue: jobs`, event type:
83
+
84
+ ```text
85
+ order.created
86
+ ```
87
+
88
+ is handed off as:
89
+
90
+ ```text
91
+ event.order.created
92
+ ```
93
+
94
+ The queued payload contains the event envelope and uses stable job ID:
95
+
96
+ ```text
97
+ outbox:<event-id>
98
+ ```
99
+
100
+ A successful enqueue marks the outbox event `published`. Downstream job completion remains the responsibility of the job runtime.
101
+
102
+ ## Event bus
103
+
104
+ `createEventBus()` provides local server-side handler registration and sequential event delivery for tests, local composition and single-process handlers.
105
+
106
+ It is not a distributed durable broker.
107
+
108
+ ## States
109
+
110
+ ```text
111
+ pending
112
+ processing
113
+ published
114
+ failed
115
+ ```
116
+
117
+ Expired `processing` leases can return to `pending` for another dispatcher attempt.
118
+
119
+ ## Delivery semantics
120
+
121
+ The platform closes the database-commit / external-publish gap but intentionally uses at-least-once delivery semantics.
122
+
123
+ Consumers should use stable event IDs and application-level idempotency when duplicate external side effects are unsafe.
124
+
125
+ ## Backward compatibility
126
+
127
+ `0.2.12` is additive relative to `0.2.11`:
128
+
129
+ - existing `bcp/jobs` APIs remain unchanged,
130
+ - existing `bcp/workflow` APIs remain unchanged,
131
+ - no database adapter contract method was removed,
132
+ - existing applications do not need an outbox unless they opt into `bcp/events`.
133
+
134
+ ## Validation
135
+
136
+ The release candidate must pass:
137
+
138
+ ```bash
139
+ npm run typecheck
140
+ npm run test:unit
141
+ npm run test:integration
142
+ npm run test:e2e
143
+ npm run test:package
144
+ npm run rc:check
145
+ ```
146
+
147
+ Package validation executes the compiled `events.mjs` runtime and verifies the `bcp/events` export, transaction/outbox API, dispatcher delivery and migration helper.