@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.
@@ -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.