@chidchanun/bcp 0.2.12 → 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.
- package/README.md +179 -224
- package/docs/README.md +40 -54
- package/docs/api-manifest.json +24 -71
- package/docs/api-reference.md +65 -56
- package/docs/docs-web-manifest.json +5 -3
- package/docs/platform-manifest.json +15 -4
- package/docs/realtime-platform.md +447 -0
- package/docs/releases/0.2.13.md +122 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/realtime.mjs +936 -0
- package/packages/client/src/realtime.ts +31 -0
- package/packages/server/src/realtime.ts +1464 -0
package/README.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# BCP Framework
|
|
2
2
|
|
|
3
|
-
BCP Framework is a React full-stack framework for file-based routing, SSR, SPA navigation, server data loading, API routes, authentication, authorization, SQL databases, background jobs, scheduling, workflow orchestration, transactional events, observability, uploads, storage and standalone Node.js deployment.
|
|
3
|
+
BCP Framework is a React full-stack framework for file-based routing, SSR, SPA navigation, server data loading, API routes, authentication, authorization, SQL databases, background jobs, scheduling, workflow orchestration, transactional events, realtime delivery, observability, uploads, storage and standalone Node.js deployment.
|
|
4
4
|
|
|
5
|
-
> **Development target:** `0.2.
|
|
5
|
+
> **Development target:** `0.2.13 — Realtime Platform`
|
|
6
6
|
>
|
|
7
|
-
> `0.2.
|
|
7
|
+
> `0.2.13` remains unreleased until local validation, RC checks, tagging and npm publication complete.
|
|
8
8
|
|
|
9
9
|
## Current platform
|
|
10
10
|
|
|
11
|
-
BCP `0.2.x` currently includes:
|
|
12
|
-
|
|
13
11
|
| Area | Capability |
|
|
14
12
|
| --- | --- |
|
|
15
13
|
| Application | React SSR, hydration, layouts, metadata and SPA navigation |
|
|
@@ -26,6 +24,7 @@ BCP `0.2.x` currently includes:
|
|
|
26
24
|
| Durable jobs | Redis-compatible queue/schedule adapters, visibility timeout, heartbeat, stale recovery and DLQ |
|
|
27
25
|
| Workflows | Sequential/parallel steps, retries, persisted delays, compensation and run leases |
|
|
28
26
|
| Events | Transactional outbox, SQL persistence, dispatcher leases, retries, event bus and durable queue handoff |
|
|
27
|
+
| Realtime | Channels/rooms, presence, broker delivery, WebSocket adapter contract, SSE and heartbeat |
|
|
29
28
|
| Observability | Structured logs, metrics, Prometheus output and health/readiness checks |
|
|
30
29
|
| Uploads & storage | Multipart streaming, Local/S3-compatible storage and signed URLs |
|
|
31
30
|
| Caching | Response cache and revalidation primitives |
|
|
@@ -65,86 +64,28 @@ Generated projects normally use one framework dependency:
|
|
|
65
64
|
}
|
|
66
65
|
```
|
|
67
66
|
|
|
68
|
-
##
|
|
67
|
+
## Core backend entrypoints
|
|
69
68
|
|
|
70
69
|
```ts
|
|
71
|
-
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} from "bcp/
|
|
70
|
+
import { db } from "bcp/database";
|
|
71
|
+
import { createAuth } from "bcp/auth";
|
|
72
|
+
import { createJobQueue } from "bcp/jobs";
|
|
73
|
+
import { createWorkflow } from "bcp/workflow";
|
|
74
|
+
import { createTransactionalOutbox } from "bcp/events";
|
|
75
|
+
import { createRealtime } from "bcp/realtime";
|
|
76
|
+
import { createMetricsRegistry } from "bcp/observability";
|
|
75
77
|
```
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
bcp db create create_users
|
|
81
|
-
bcp db migrate
|
|
82
|
-
bcp db status
|
|
83
|
-
bcp db rollback
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Authentication & authorization
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
import {
|
|
90
|
-
createAuth,
|
|
91
|
-
createMemoryAuthSessionStore,
|
|
92
|
-
requirePermission,
|
|
93
|
-
} from "bcp/auth";
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Request security is available from `bcp/server`:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import {
|
|
100
|
-
createCsrfToken,
|
|
101
|
-
requireCsrfRequest,
|
|
102
|
-
requireSameOriginRequest,
|
|
103
|
-
} from "bcp/server";
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Observability
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
import {
|
|
110
|
-
createHealthRegistry,
|
|
111
|
-
createMetricsRegistry,
|
|
112
|
-
createMetricsResponse,
|
|
113
|
-
} from "bcp/observability";
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Background jobs
|
|
79
|
+
## Durable jobs and scheduling
|
|
117
80
|
|
|
118
81
|
```ts
|
|
119
82
|
import {
|
|
120
83
|
createJobQueue,
|
|
84
|
+
createJobScheduler,
|
|
121
85
|
} from "bcp/jobs";
|
|
122
86
|
|
|
123
87
|
export const jobs =
|
|
124
88
|
createJobQueue();
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Register typed work:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
jobs.register<{
|
|
131
|
-
userId: number;
|
|
132
|
-
}>(
|
|
133
|
-
"email.welcome",
|
|
134
|
-
async ({ payload }) => {
|
|
135
|
-
await sendWelcomeEmail(
|
|
136
|
-
payload.userId
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Job scheduling
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
import {
|
|
146
|
-
createJobScheduler,
|
|
147
|
-
} from "bcp/jobs";
|
|
148
89
|
|
|
149
90
|
export const scheduler =
|
|
150
91
|
createJobScheduler({
|
|
@@ -152,33 +93,7 @@ export const scheduler =
|
|
|
152
93
|
});
|
|
153
94
|
```
|
|
154
95
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
await scheduler.schedule(
|
|
159
|
-
"report.weekday",
|
|
160
|
-
{},
|
|
161
|
-
{
|
|
162
|
-
cron: "30 9 * * 1-5",
|
|
163
|
-
}
|
|
164
|
-
);
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Durable Jobs Platform — 0.2.10
|
|
168
|
-
|
|
169
|
-
Workers support visibility leases, heartbeat renewal and stale-running recovery:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
const worker =
|
|
173
|
-
jobs.startWorker({
|
|
174
|
-
workerId: "email-worker",
|
|
175
|
-
concurrency: 4,
|
|
176
|
-
visibilityTimeoutMs: 30_000,
|
|
177
|
-
heartbeatIntervalMs: 10_000,
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Redis-compatible adapters are available from `bcp/jobs`. BCP does not install or own the Redis client; applications provide a command client and manage credentials/TLS/Cluster/lifecycle themselves.
|
|
96
|
+
Production adapters can provide Redis-compatible durable queue/schedule storage without BCP owning the Redis connection.
|
|
182
97
|
|
|
183
98
|
## Workflow Orchestration — 0.2.11
|
|
184
99
|
|
|
@@ -220,45 +135,13 @@ export const onboarding =
|
|
|
220
135
|
);
|
|
221
136
|
```
|
|
222
137
|
|
|
223
|
-
Workflows support retries, compensation and optional
|
|
138
|
+
Workflows support retries, compensation, persisted delays and optional durable queue execution.
|
|
224
139
|
|
|
225
140
|
## Transactional Outbox & Events — 0.2.12
|
|
226
141
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
The problem it solves:
|
|
230
|
-
|
|
231
|
-
```text
|
|
232
|
-
DB commit succeeds
|
|
233
|
-
+
|
|
234
|
-
external publish fails
|
|
235
|
-
=
|
|
236
|
-
missing integration event
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
The outbox writes the event in the **same SQL transaction** as the business data:
|
|
142
|
+
Use the same SQL transaction for business data and its outbox event:
|
|
240
143
|
|
|
241
144
|
```ts
|
|
242
|
-
import {
|
|
243
|
-
db,
|
|
244
|
-
} from "bcp/database";
|
|
245
|
-
|
|
246
|
-
import {
|
|
247
|
-
createSqlOutboxStore,
|
|
248
|
-
createTransactionalOutbox,
|
|
249
|
-
} from "bcp/events";
|
|
250
|
-
|
|
251
|
-
const outboxStore =
|
|
252
|
-
createSqlOutboxStore({
|
|
253
|
-
database: db,
|
|
254
|
-
driver: "postgresql",
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const outbox =
|
|
258
|
-
createTransactionalOutbox({
|
|
259
|
-
store: outboxStore,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
145
|
await db.transaction(
|
|
263
146
|
async tx => {
|
|
264
147
|
await tx.execute(
|
|
@@ -270,108 +153,182 @@ await db.transaction(
|
|
|
270
153
|
"order.created",
|
|
271
154
|
{
|
|
272
155
|
orderId: 42,
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
aggregateId: "42",
|
|
276
|
-
correlationId: requestId,
|
|
277
156
|
}
|
|
278
157
|
);
|
|
279
158
|
}
|
|
280
159
|
);
|
|
281
160
|
```
|
|
282
161
|
|
|
283
|
-
|
|
162
|
+
After commit, an outbox dispatcher can deliver through `bcp/jobs`, a custom publisher or the local event bus.
|
|
163
|
+
|
|
164
|
+
Read more: [Transactional Outbox & Events](docs/transactional-outbox-events.md)
|
|
165
|
+
|
|
166
|
+
## Realtime Platform — 0.2.13
|
|
167
|
+
|
|
168
|
+
`0.2.13` adds the server-only `bcp/realtime` public entrypoint.
|
|
169
|
+
|
|
170
|
+
Create a hub:
|
|
284
171
|
|
|
285
172
|
```ts
|
|
286
173
|
import {
|
|
287
|
-
|
|
288
|
-
} from "bcp/
|
|
174
|
+
createRealtime,
|
|
175
|
+
} from "bcp/realtime";
|
|
289
176
|
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
"postgresql"
|
|
293
|
-
);
|
|
177
|
+
export const realtime =
|
|
178
|
+
createRealtime();
|
|
294
179
|
```
|
|
295
180
|
|
|
296
|
-
|
|
181
|
+
### Channels / rooms
|
|
297
182
|
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
183
|
+
```ts
|
|
184
|
+
const connection =
|
|
185
|
+
await realtime.connect();
|
|
186
|
+
|
|
187
|
+
await connection.join(
|
|
188
|
+
"orders:42"
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
await realtime.broadcast(
|
|
192
|
+
"orders:42",
|
|
193
|
+
"order.updated",
|
|
194
|
+
{
|
|
195
|
+
status: "paid",
|
|
196
|
+
}
|
|
197
|
+
);
|
|
302
198
|
```
|
|
303
199
|
|
|
304
|
-
|
|
200
|
+
Rooms are represented by channel names. Connections receive broadcasts only for channels they joined.
|
|
305
201
|
|
|
306
|
-
|
|
202
|
+
### Presence
|
|
307
203
|
|
|
308
204
|
```ts
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
205
|
+
await connection.join(
|
|
206
|
+
"project:7",
|
|
207
|
+
{
|
|
208
|
+
presence: {
|
|
209
|
+
status: "online",
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
);
|
|
312
213
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
214
|
+
const members =
|
|
215
|
+
await realtime.members(
|
|
216
|
+
"project:7"
|
|
217
|
+
);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The built-in memory presence store is process-local. Multi-instance deployments should provide a shared `RealtimePresenceStore`.
|
|
221
|
+
|
|
222
|
+
### Authentication and private channels
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const realtime =
|
|
226
|
+
createRealtime({
|
|
227
|
+
authenticate:
|
|
228
|
+
async ({ request }) =>
|
|
229
|
+
request
|
|
230
|
+
? loadRealtimeUser(
|
|
231
|
+
request
|
|
232
|
+
)
|
|
233
|
+
: null,
|
|
234
|
+
|
|
235
|
+
getUserId:
|
|
236
|
+
user => user.id,
|
|
237
|
+
|
|
238
|
+
authorizeChannel:
|
|
239
|
+
({ connection, channel }) =>
|
|
240
|
+
!channel.startsWith(
|
|
241
|
+
"private:"
|
|
242
|
+
) ||
|
|
243
|
+
Boolean(
|
|
244
|
+
connection.user
|
|
245
|
+
),
|
|
321
246
|
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### WebSocket provider contract
|
|
322
250
|
|
|
323
|
-
|
|
324
|
-
|
|
251
|
+
BCP intentionally does not install `ws`, Socket.IO or another WebSocket server dependency.
|
|
252
|
+
|
|
253
|
+
Adapt the selected server/provider to `RealtimeSocket`, then attach it:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const connection =
|
|
257
|
+
await realtime.attachSocket(
|
|
258
|
+
socketAdapter,
|
|
259
|
+
{
|
|
260
|
+
request,
|
|
261
|
+
}
|
|
262
|
+
);
|
|
325
263
|
```
|
|
326
264
|
|
|
327
|
-
|
|
265
|
+
The JSON socket protocol supports `join`, `leave`, `event` and `ping` messages.
|
|
328
266
|
|
|
329
|
-
|
|
330
|
-
|
|
267
|
+
### Server-Sent Events
|
|
268
|
+
|
|
269
|
+
SSE is built in through Web `Response`:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
export function GET(
|
|
273
|
+
request: Request
|
|
274
|
+
) {
|
|
275
|
+
return realtime.sse(
|
|
276
|
+
"workflow:42",
|
|
277
|
+
{
|
|
278
|
+
signal:
|
|
279
|
+
request.signal,
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
}
|
|
331
283
|
```
|
|
332
284
|
|
|
333
|
-
|
|
285
|
+
### Cross-instance delivery
|
|
286
|
+
|
|
287
|
+
`RealtimeBroker` is the provider-neutral pub/sub boundary. The built-in memory broker supports same-process hubs; production applications can implement shared Redis/NATS/etc. brokers without changing hub APIs.
|
|
334
288
|
|
|
335
289
|
```text
|
|
336
|
-
|
|
290
|
+
Browser A -> App A ----\
|
|
291
|
+
Shared Broker
|
|
292
|
+
Browser B -> App B ----/
|
|
293
|
+
| |
|
|
294
|
+
+---- Shared Presence Store
|
|
337
295
|
```
|
|
338
296
|
|
|
339
|
-
|
|
297
|
+
### Heartbeat
|
|
340
298
|
|
|
341
299
|
```ts
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
);
|
|
300
|
+
const heartbeat =
|
|
301
|
+
realtime.startHeartbeat({
|
|
302
|
+
intervalMs: 20_000,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// shutdown
|
|
306
|
+
await heartbeat.stop();
|
|
307
|
+
await realtime.close();
|
|
350
308
|
```
|
|
351
309
|
|
|
352
|
-
|
|
310
|
+
`realtime.ping` / `realtime.pong` and `sweepStale()` provide connection liveness cleanup.
|
|
353
311
|
|
|
354
|
-
|
|
355
|
-
- lease ownership,
|
|
356
|
-
- stale lease recovery,
|
|
357
|
-
- retry/backoff,
|
|
358
|
-
- terminal failed state,
|
|
359
|
-
- queue delivery,
|
|
360
|
-
- custom publisher callbacks,
|
|
361
|
-
- local `EventBus`,
|
|
362
|
-
- retention cleanup,
|
|
363
|
-
- outbox statistics.
|
|
312
|
+
### Jobs / workflows / events integration
|
|
364
313
|
|
|
365
|
-
|
|
314
|
+
Realtime is the transient delivery edge for durable backend state:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
await realtime.broadcast(
|
|
318
|
+
`jobs:${job.id}`,
|
|
319
|
+
"job.progress",
|
|
320
|
+
{
|
|
321
|
+
progress: 60,
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
```
|
|
366
325
|
|
|
367
|
-
|
|
326
|
+
Important business facts should remain in database/outbox/jobs/workflows. Realtime delivery is transient, so reconnecting clients should refetch durable state/history when catch-up is required.
|
|
368
327
|
|
|
369
|
-
Read more: [
|
|
328
|
+
Read more: [Realtime Platform](docs/realtime-platform.md)
|
|
370
329
|
|
|
371
330
|
## Public entrypoints
|
|
372
331
|
|
|
373
|
-
The current documented platform surface is:
|
|
374
|
-
|
|
375
332
|
```text
|
|
376
333
|
bcp
|
|
377
334
|
bcp/island
|
|
@@ -384,13 +341,14 @@ bcp/auth
|
|
|
384
341
|
bcp/jobs
|
|
385
342
|
bcp/workflow
|
|
386
343
|
bcp/events
|
|
344
|
+
bcp/realtime
|
|
387
345
|
bcp/observability
|
|
388
346
|
bcp/server
|
|
389
347
|
bcp/server-only
|
|
390
348
|
bcp/middleware
|
|
391
349
|
```
|
|
392
350
|
|
|
393
|
-
Application code should use public entrypoints instead of
|
|
351
|
+
Application code should use public entrypoints instead of private `packages/*` implementation files.
|
|
394
352
|
|
|
395
353
|
## CLI
|
|
396
354
|
|
|
@@ -407,6 +365,15 @@ bcp inspect
|
|
|
407
365
|
bcp version
|
|
408
366
|
```
|
|
409
367
|
|
|
368
|
+
Database migrations:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
bcp db create create_users
|
|
372
|
+
bcp db migrate
|
|
373
|
+
bcp db status
|
|
374
|
+
bcp db rollback
|
|
375
|
+
```
|
|
376
|
+
|
|
410
377
|
Generators:
|
|
411
378
|
|
|
412
379
|
```bash
|
|
@@ -419,32 +386,21 @@ bcp generate migration create_users
|
|
|
419
386
|
## Production model
|
|
420
387
|
|
|
421
388
|
```text
|
|
422
|
-
Browser / API
|
|
423
|
-
|
|
424
|
-
security + auth
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
| |
|
|
434
|
-
| durable jobs
|
|
435
|
-
| |
|
|
436
|
-
| workers
|
|
437
|
-
|
|
|
438
|
-
SSR / response
|
|
439
|
-
|
|
440
|
-
Workflow + scheduler + logs + metrics + health
|
|
441
|
-
operate alongside the request path.
|
|
389
|
+
Browser / API / Realtime clients
|
|
390
|
+
|
|
|
391
|
+
security + auth
|
|
392
|
+
|
|
|
393
|
+
application APIs
|
|
394
|
+
/ | \
|
|
395
|
+
database workflows realtime
|
|
396
|
+
| | ^
|
|
397
|
+
outbox jobs |
|
|
398
|
+
\__________|_________/
|
|
399
|
+
durable state
|
|
442
400
|
```
|
|
443
401
|
|
|
444
402
|
## Packaging
|
|
445
403
|
|
|
446
|
-
Raw production build:
|
|
447
|
-
|
|
448
404
|
```bash
|
|
449
405
|
npm run build
|
|
450
406
|
npm start
|
|
@@ -458,8 +414,6 @@ bcp package
|
|
|
458
414
|
|
|
459
415
|
## Documentation Platform
|
|
460
416
|
|
|
461
|
-
The framework repository is the documentation source of truth.
|
|
462
|
-
|
|
463
417
|
Machine-readable contracts:
|
|
464
418
|
|
|
465
419
|
```text
|
|
@@ -470,7 +424,7 @@ docs/api-manifest.json
|
|
|
470
424
|
|
|
471
425
|
## Release validation
|
|
472
426
|
|
|
473
|
-
Before publishing `0.2.
|
|
427
|
+
Before publishing `0.2.13`:
|
|
474
428
|
|
|
475
429
|
```bash
|
|
476
430
|
npm run typecheck
|
|
@@ -481,7 +435,7 @@ npm run test:package
|
|
|
481
435
|
npm run rc:check
|
|
482
436
|
```
|
|
483
437
|
|
|
484
|
-
`0.2.
|
|
438
|
+
`0.2.13` adds unit and prepared-package smoke coverage for broker broadcasts, channel membership, presence, authentication/channel authorization, socket protocol, heartbeat cleanup, SSE streaming, public runtime compilation and browser boundary enforcement.
|
|
485
439
|
|
|
486
440
|
Do not tag or publish until the exact final release commit passes the full RC sequence.
|
|
487
441
|
|
|
@@ -508,12 +462,13 @@ Do not tag or publish until the exact final release commit passes the full RC se
|
|
|
508
462
|
| `0.2.10` | Durable Jobs Platform |
|
|
509
463
|
| `0.2.11` | Workflow Orchestration |
|
|
510
464
|
| `0.2.12` | Transactional Outbox & Events |
|
|
465
|
+
| `0.2.13` | Realtime Platform |
|
|
511
466
|
|
|
512
467
|
## Roadmap
|
|
513
468
|
|
|
514
|
-
`0.2.
|
|
469
|
+
`0.2.13` establishes provider-neutral live delivery on top of the durable database/outbox/jobs/workflow stack.
|
|
515
470
|
|
|
516
|
-
The next logical milestone is **`0.2.
|
|
471
|
+
The next logical milestone is **`0.2.14 — Testing Platform`**, focused on framework-native request/route/auth/database/jobs/workflow/realtime testing utilities and application test harnesses.
|
|
517
472
|
|
|
518
473
|
Native desktop/mobile compilation remains later roadmap work.
|
|
519
474
|
|