@chidchanun/bcp 0.2.13 → 0.2.15
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 +224 -99
- package/docs/README.md +40 -35
- package/docs/api-manifest.json +26 -10
- package/docs/api-reference.md +173 -29
- package/docs/docs-web-manifest.json +8 -4
- package/docs/platform-manifest.json +33 -4
- package/docs/plugin-module-platform.md +391 -0
- package/docs/releases/0.2.14.md +241 -0
- package/docs/releases/0.2.15.md +118 -0
- package/docs/testing-platform.md +602 -0
- package/package.json +11 -1
- package/packages/bundler/src/client-boundary.ts +2 -0
- package/packages/client/src/plugins.mjs +811 -0
- package/packages/client/src/plugins.ts +26 -0
- package/packages/client/src/realtime.mjs +67 -30
- package/packages/client/src/testing.mjs +2357 -0
- package/packages/client/src/testing.ts +52 -0
- package/packages/server/src/plugins.ts +1225 -0
- package/packages/server/src/realtime.ts +95 -41
- package/packages/server/src/testing-page.ts +274 -0
- package/packages/server/src/testing.ts +1884 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# Plugin & Module Platform
|
|
2
|
+
|
|
3
|
+
BCP `0.2.15` adds the server-only `bcp/plugins` entrypoint for composing framework/application extensions with explicit dependencies and lifecycle control.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
createPluginHost,
|
|
10
|
+
defineModule,
|
|
11
|
+
definePlugin,
|
|
12
|
+
} from "bcp/plugins";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`bcp/plugins` is server-only and cannot be imported from page/client bundles.
|
|
16
|
+
|
|
17
|
+
## Define a plugin
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
export const databasePlugin =
|
|
21
|
+
definePlugin({
|
|
22
|
+
name: "database",
|
|
23
|
+
|
|
24
|
+
async setup(context) {
|
|
25
|
+
context.services.provide(
|
|
26
|
+
"database",
|
|
27
|
+
db
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async start() {
|
|
32
|
+
await db.connect();
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async stop() {
|
|
36
|
+
await db.disconnect();
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A plugin can expose four lifecycle hooks:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
setup()
|
|
45
|
+
start()
|
|
46
|
+
stop()
|
|
47
|
+
dispose()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`setup()` prepares dependency wiring and shared services. `start()` begins runtime work. `stop()` shuts active work down. `dispose()` releases setup-level resources when the host closes.
|
|
51
|
+
|
|
52
|
+
## Dependency ordering
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const jobsPlugin =
|
|
56
|
+
definePlugin({
|
|
57
|
+
name: "jobs",
|
|
58
|
+
requires: [
|
|
59
|
+
"database",
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The host resolves required dependencies before dependents:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
database
|
|
68
|
+
↓
|
|
69
|
+
jobs
|
|
70
|
+
↓
|
|
71
|
+
workflow
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Startup follows this order. Shutdown and disposal run in reverse order.
|
|
75
|
+
|
|
76
|
+
Missing required dependencies throw `PluginDependencyError` before lifecycle execution begins.
|
|
77
|
+
|
|
78
|
+
Dependency cycles are rejected:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
a -> b -> c -> a
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Optional dependencies participate in ordering only when they are registered:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
definePlugin({
|
|
88
|
+
name: "metrics-addon",
|
|
89
|
+
optional: [
|
|
90
|
+
"observability",
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Create a host
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const host =
|
|
99
|
+
createPluginHost({
|
|
100
|
+
plugins: [
|
|
101
|
+
databasePlugin,
|
|
102
|
+
jobsPlugin,
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await host.start();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For graceful shutdown:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await host.stop();
|
|
113
|
+
await host.close();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`close()` is idempotent. If the host is still started it stops the runtime before disposal.
|
|
117
|
+
|
|
118
|
+
## Modules
|
|
119
|
+
|
|
120
|
+
A module is a named bundle of plugin definitions.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
export const backendModule =
|
|
124
|
+
defineModule({
|
|
125
|
+
name: "backend",
|
|
126
|
+
plugins: [
|
|
127
|
+
databasePlugin,
|
|
128
|
+
jobsPlugin,
|
|
129
|
+
workflowPlugin,
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const host =
|
|
134
|
+
createPluginHost({
|
|
135
|
+
modules: [
|
|
136
|
+
backendModule,
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Modules do not create a second lifecycle system. They only group plugins; dependency resolution still occurs globally across the host.
|
|
142
|
+
|
|
143
|
+
You can also register before setup begins:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
host.use(databasePlugin);
|
|
147
|
+
host.use(backendModule);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Registration is locked after setup starts so runtime dependency graphs cannot change underneath active plugins.
|
|
151
|
+
|
|
152
|
+
## Typed plugin configuration
|
|
153
|
+
|
|
154
|
+
A plugin can parse its own configuration:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const httpPlugin =
|
|
158
|
+
definePlugin<{
|
|
159
|
+
port: number;
|
|
160
|
+
}>({
|
|
161
|
+
name: "http",
|
|
162
|
+
|
|
163
|
+
config: {
|
|
164
|
+
port: 3000,
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
schema: {
|
|
168
|
+
parse(value) {
|
|
169
|
+
const port =
|
|
170
|
+
Number(
|
|
171
|
+
(
|
|
172
|
+
value as {
|
|
173
|
+
port?: unknown;
|
|
174
|
+
}
|
|
175
|
+
)?.port
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!Number.isInteger(port)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"port must be an integer"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
port,
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
setup(context) {
|
|
191
|
+
console.log(
|
|
192
|
+
context.config.port
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Application-level host configuration overrides the plugin default:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const host =
|
|
202
|
+
createPluginHost({
|
|
203
|
+
plugins: [
|
|
204
|
+
httpPlugin,
|
|
205
|
+
],
|
|
206
|
+
configs: {
|
|
207
|
+
http: {
|
|
208
|
+
port: 8080,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A schema may be an object with `parse()` or a parser function.
|
|
215
|
+
|
|
216
|
+
## Shared service registry
|
|
217
|
+
|
|
218
|
+
Plugins can publish typed application services:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
setup(context) {
|
|
222
|
+
context.services.provide(
|
|
223
|
+
"mailer",
|
|
224
|
+
mailer
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
A dependent plugin can consume the service:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
setup(context) {
|
|
233
|
+
const mailer =
|
|
234
|
+
context.services.get<Mailer>(
|
|
235
|
+
"mailer"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Available operations:
|
|
241
|
+
|
|
242
|
+
```text
|
|
243
|
+
provide()
|
|
244
|
+
get()
|
|
245
|
+
optional()
|
|
246
|
+
has()
|
|
247
|
+
delete()
|
|
248
|
+
keys()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Duplicate `provide()` calls are rejected unless `{ replace: true }` is explicit.
|
|
252
|
+
|
|
253
|
+
String and Symbol service keys are supported.
|
|
254
|
+
|
|
255
|
+
## Asynchronous extension hooks
|
|
256
|
+
|
|
257
|
+
The host also exposes a small asynchronous hook bus:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const unsubscribe =
|
|
261
|
+
context.hooks.on<{
|
|
262
|
+
userId: number;
|
|
263
|
+
}>(
|
|
264
|
+
"user.created",
|
|
265
|
+
async event => {
|
|
266
|
+
await sendWelcomeEmail(
|
|
267
|
+
event.userId
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Emit from another plugin:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
await context.hooks.emit(
|
|
277
|
+
"user.created",
|
|
278
|
+
{
|
|
279
|
+
userId: 42,
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Handlers run in registration order and are awaited. The hook bus is intended for in-process extension points, not durable event delivery.
|
|
285
|
+
|
|
286
|
+
Use `bcp/events` for durable integration events that must survive process failure.
|
|
287
|
+
|
|
288
|
+
## Lifecycle failure behavior
|
|
289
|
+
|
|
290
|
+
If a plugin fails during `start()`, plugins that already started in that transition are stopped in reverse order before `PluginLifecycleError` is propagated.
|
|
291
|
+
|
|
292
|
+
```text
|
|
293
|
+
database start ✅
|
|
294
|
+
jobs start ✅
|
|
295
|
+
workflow start ❌
|
|
296
|
+
|
|
297
|
+
rollback:
|
|
298
|
+
jobs stop
|
|
299
|
+
database stop
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
This prevents partially-started application runtimes from being left active after a startup failure.
|
|
303
|
+
|
|
304
|
+
Lifecycle records can be inspected:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
host.plugin("jobs");
|
|
308
|
+
host.plugins();
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
States include:
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
registered
|
|
315
|
+
setting-up
|
|
316
|
+
ready
|
|
317
|
+
starting
|
|
318
|
+
started
|
|
319
|
+
stopping
|
|
320
|
+
stopped
|
|
321
|
+
failed
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Relationship to existing BCP platforms
|
|
325
|
+
|
|
326
|
+
`bcp/plugins` composes services; it does not replace their own durability/lifecycle contracts.
|
|
327
|
+
|
|
328
|
+
```text
|
|
329
|
+
Plugin Host
|
|
330
|
+
|
|
|
331
|
+
+-- Database plugin ------> bcp/database
|
|
332
|
+
+-- Jobs plugin ----------> bcp/jobs
|
|
333
|
+
+-- Workflow plugin ------> bcp/workflow
|
|
334
|
+
+-- Events plugin --------> bcp/events
|
|
335
|
+
+-- Realtime plugin ------> bcp/realtime
|
|
336
|
+
+-- Observability plugin -> bcp/observability
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
A plugin can wrap any existing BCP subsystem and expose the resulting instance through the service registry.
|
|
340
|
+
|
|
341
|
+
## Production guidance
|
|
342
|
+
|
|
343
|
+
Keep plugin names stable because dependency declarations reference names.
|
|
344
|
+
|
|
345
|
+
Prefer explicit required dependencies instead of relying on registration order.
|
|
346
|
+
|
|
347
|
+
Use plugin configuration parsing near the plugin boundary so invalid configuration fails before startup.
|
|
348
|
+
|
|
349
|
+
Use `dispose()` for resources created during setup, and `stop()` for active runtime processes such as workers, schedulers and network listeners.
|
|
350
|
+
|
|
351
|
+
Do not use the hook bus as a durable message broker. Use Transactional Outbox/Jobs for delivery guarantees.
|
|
352
|
+
|
|
353
|
+
## 0.2.15 scope
|
|
354
|
+
|
|
355
|
+
Included:
|
|
356
|
+
|
|
357
|
+
```text
|
|
358
|
+
bcp/plugins
|
|
359
|
+
definePlugin()
|
|
360
|
+
defineModule()
|
|
361
|
+
createPluginHost()
|
|
362
|
+
required dependencies
|
|
363
|
+
optional dependencies
|
|
364
|
+
topological ordering
|
|
365
|
+
cycle detection
|
|
366
|
+
setup/start/stop/dispose lifecycle
|
|
367
|
+
reverse shutdown
|
|
368
|
+
startup rollback
|
|
369
|
+
plugin state records
|
|
370
|
+
config parser/schema contract
|
|
371
|
+
host config overrides
|
|
372
|
+
service registry
|
|
373
|
+
async hook bus
|
|
374
|
+
server-only boundary
|
|
375
|
+
compiled plugins.mjs
|
|
376
|
+
unit tests
|
|
377
|
+
prepared package smoke
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Not included yet:
|
|
381
|
+
|
|
382
|
+
```text
|
|
383
|
+
automatic npm plugin discovery
|
|
384
|
+
remote plugin loading
|
|
385
|
+
sandboxed/untrusted plugins
|
|
386
|
+
hot plugin replacement
|
|
387
|
+
plugin marketplace
|
|
388
|
+
CLI plugin install command
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Those can be added later without changing the core host contract.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# BCP Framework 0.2.14 — Testing Platform
|
|
2
|
+
|
|
3
|
+
**Release state:** unreleased
|
|
4
|
+
|
|
5
|
+
BCP `0.2.14` adds framework-native server-side testing utilities through the new `bcp/testing` entrypoint.
|
|
6
|
+
|
|
7
|
+
The milestone is intentionally test-runner neutral: BCP does not add Jest or Vitest as runtime dependencies. The framework's own suite continues to use Node's built-in `node:test`.
|
|
8
|
+
|
|
9
|
+
## Highlights
|
|
10
|
+
|
|
11
|
+
- new server-only `bcp/testing` entrypoint,
|
|
12
|
+
- Request -> Response application test harness,
|
|
13
|
+
- persistent test cookie jar and default headers,
|
|
14
|
+
- API route module testing for GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS,
|
|
15
|
+
- page guard testing through the production guard executor,
|
|
16
|
+
- page loader testing through the production loader executor,
|
|
17
|
+
- form action testing through the production action executor,
|
|
18
|
+
- `FormData` construction helper,
|
|
19
|
+
- lightweight response assertions,
|
|
20
|
+
- real signed BCP auth session generation,
|
|
21
|
+
- optional `AuthSessionStore` registration,
|
|
22
|
+
- rollback-only database test transactions,
|
|
23
|
+
- real Middleware System v2 pipeline execution,
|
|
24
|
+
- deterministic fake clock,
|
|
25
|
+
- deterministic sequence ID factory,
|
|
26
|
+
- background job queue drain/count helpers,
|
|
27
|
+
- workflow execution/state helpers,
|
|
28
|
+
- transactional outbox dispatch/state helpers,
|
|
29
|
+
- in-memory `RealtimeSocket` test adapter,
|
|
30
|
+
- realtime connection/event harness,
|
|
31
|
+
- Server-Sent Events reader,
|
|
32
|
+
- prepared package compilation to `testing.mjs`,
|
|
33
|
+
- package smoke and client-boundary validation.
|
|
34
|
+
|
|
35
|
+
## Public API
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import {
|
|
39
|
+
createFakeClock,
|
|
40
|
+
createJobTestHarness,
|
|
41
|
+
createOutboxTestHarness,
|
|
42
|
+
createRealtimeTestHarness,
|
|
43
|
+
createRealtimeTestSocket,
|
|
44
|
+
createRouteTestHandler,
|
|
45
|
+
createSequenceIdFactory,
|
|
46
|
+
createTestApp,
|
|
47
|
+
createTestAuthSession,
|
|
48
|
+
createTestFormData,
|
|
49
|
+
createWorkflowTestHarness,
|
|
50
|
+
expectResponse,
|
|
51
|
+
readSseEvents,
|
|
52
|
+
runTestMiddleware,
|
|
53
|
+
runTestPageAction,
|
|
54
|
+
runTestPageGuards,
|
|
55
|
+
runTestPageLoader,
|
|
56
|
+
withTestTransaction,
|
|
57
|
+
} from "bcp/testing";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Request and route testing
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const app =
|
|
64
|
+
createTestApp({
|
|
65
|
+
handler:
|
|
66
|
+
createRouteTestHandler({
|
|
67
|
+
GET() {
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const response =
|
|
76
|
+
await app.get(
|
|
77
|
+
"/api/health"
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await expectResponse(response)
|
|
81
|
+
.status(200)
|
|
82
|
+
.json({
|
|
83
|
+
ok: true,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The harness maintains cookies returned by `Set-Cookie` and sends them on future requests.
|
|
88
|
+
|
|
89
|
+
## Page server runtime testing
|
|
90
|
+
|
|
91
|
+
Page loader, guard and form action helpers invoke the same execution functions used by BCP runtime:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await runTestPageGuards(
|
|
95
|
+
guard,
|
|
96
|
+
{
|
|
97
|
+
params: {
|
|
98
|
+
id: "42",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await runTestPageLoader(
|
|
104
|
+
loader,
|
|
105
|
+
{
|
|
106
|
+
url:
|
|
107
|
+
"/users/42?tab=profile",
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await runTestPageAction(
|
|
112
|
+
updateUser,
|
|
113
|
+
{
|
|
114
|
+
actionName:
|
|
115
|
+
"updateUser",
|
|
116
|
+
method: "PATCH",
|
|
117
|
+
fields: {
|
|
118
|
+
name: "BCP",
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This preserves guard-data chaining, loader/action Response short-circuits, action method validation and JSON-safe result validation.
|
|
125
|
+
|
|
126
|
+
## Authentication testing
|
|
127
|
+
|
|
128
|
+
`createTestAuthSession()` uses the same BCP session token implementation as production auth.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const session =
|
|
132
|
+
await createTestAuthSession(
|
|
133
|
+
{
|
|
134
|
+
id: 42,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
secret,
|
|
138
|
+
store:
|
|
139
|
+
authSessionStore,
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
app.setCookie(
|
|
144
|
+
session.cookieName,
|
|
145
|
+
session.token
|
|
146
|
+
);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This avoids introducing a framework-only fake user header.
|
|
150
|
+
|
|
151
|
+
## Database rollback tests
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await withTestTransaction(
|
|
155
|
+
db,
|
|
156
|
+
async tx => {
|
|
157
|
+
await tx.execute(
|
|
158
|
+
"INSERT INTO users ..."
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// assertions
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
After the callback succeeds, BCP deliberately rejects the database transaction with an internal rollback signal, then returns the callback result to the test.
|
|
167
|
+
|
|
168
|
+
## Infrastructure harnesses
|
|
169
|
+
|
|
170
|
+
`0.2.14` adds thin harnesses over the real platform contracts instead of separate mock implementations:
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
BackgroundJobQueue -> createJobTestHarness()
|
|
174
|
+
Workflow -> createWorkflowTestHarness()
|
|
175
|
+
OutboxStore + Dispatcher -> createOutboxTestHarness()
|
|
176
|
+
RealtimeHub -> createRealtimeTestHarness()
|
|
177
|
+
RealtimeSocket -> createRealtimeTestSocket()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## SSE
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const events =
|
|
184
|
+
await readSseEvents(
|
|
185
|
+
response,
|
|
186
|
+
{
|
|
187
|
+
limit: 1,
|
|
188
|
+
timeoutMs: 1_000,
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
SSE comments and retry frames are ignored and JSON `data:` payloads are decoded when possible.
|
|
194
|
+
|
|
195
|
+
## Package/runtime contract
|
|
196
|
+
|
|
197
|
+
The prepared npm package now exposes:
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
bcp/testing
|
|
201
|
+
types -> packages/client/src/testing.ts
|
|
202
|
+
browser -> packages/client/src/server-only.browser.mjs
|
|
203
|
+
default -> packages/client/src/testing.mjs
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`testing.mjs` is compiled during `package:prepare` so Node does not need a TypeScript loader to use the testing entrypoint from an installed package.
|
|
207
|
+
|
|
208
|
+
## Compatibility
|
|
209
|
+
|
|
210
|
+
`0.2.14` has no intentional breaking changes from `0.2.13`.
|
|
211
|
+
|
|
212
|
+
All existing runtime entrypoints remain supported.
|
|
213
|
+
|
|
214
|
+
## Validation
|
|
215
|
+
|
|
216
|
+
Before publishing:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
npm run typecheck
|
|
220
|
+
npm run test:unit
|
|
221
|
+
npm run test:integration
|
|
222
|
+
npm run test:e2e
|
|
223
|
+
npm run test:package
|
|
224
|
+
npm run rc:check
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The release must not be tagged or published until the final release commit passes the complete RC sequence.
|
|
228
|
+
|
|
229
|
+
## Non-goals
|
|
230
|
+
|
|
231
|
+
`0.2.14` does not add:
|
|
232
|
+
|
|
233
|
+
- a DOM/browser renderer,
|
|
234
|
+
- Playwright integration,
|
|
235
|
+
- component snapshots,
|
|
236
|
+
- a coverage runner,
|
|
237
|
+
- a Jest dependency,
|
|
238
|
+
- a Vitest dependency,
|
|
239
|
+
- automatic Docker/database test provisioning.
|
|
240
|
+
|
|
241
|
+
These can be layered on top of the base testing contracts in later milestones.
|