@chidchanun/bcp 0.3.0 → 0.3.2

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,3 +1,506 @@
1
+ // packages/server/src/container.ts
2
+ var ServiceNotFoundError = class extends Error {
3
+ token;
4
+ constructor(token) {
5
+ super(
6
+ `BCP Container: service "${token.description}" is not registered.`
7
+ );
8
+ this.name = "ServiceNotFoundError";
9
+ this.token = token;
10
+ }
11
+ };
12
+ var ServiceResolutionError = class extends Error {
13
+ token;
14
+ cause;
15
+ constructor(token, message, cause) {
16
+ super(
17
+ `BCP Container: could not resolve "${token.description}". ${message}`
18
+ );
19
+ this.name = "ServiceResolutionError";
20
+ this.token = token;
21
+ this.cause = cause;
22
+ }
23
+ };
24
+ var ServiceDisposalError = class extends AggregateError {
25
+ constructor(errors) {
26
+ super(
27
+ errors,
28
+ "BCP Container: one or more services failed to dispose."
29
+ );
30
+ this.name = "ServiceDisposalError";
31
+ }
32
+ };
33
+ function createServiceContainer(options = {}) {
34
+ const root = new ScopeImpl(
35
+ normalizeOptionalName(
36
+ options.name
37
+ ) ?? "root",
38
+ void 0
39
+ );
40
+ root.root = root;
41
+ const container = {
42
+ get name() {
43
+ return root.name;
44
+ },
45
+ get state() {
46
+ return root.state;
47
+ },
48
+ get parent() {
49
+ return void 0;
50
+ },
51
+ has(token) {
52
+ return root.has(token);
53
+ },
54
+ resolve(token) {
55
+ return root.resolve(token);
56
+ },
57
+ optional(token) {
58
+ return root.optional(token);
59
+ },
60
+ createScope(scopeOptions = {}) {
61
+ return root.createScope(
62
+ scopeOptions
63
+ );
64
+ },
65
+ graph() {
66
+ return root.graph();
67
+ },
68
+ dispose() {
69
+ return root.dispose();
70
+ },
71
+ register(provider, registerOptions = {}) {
72
+ root.registerProvider(
73
+ provider,
74
+ registerOptions.replace === true
75
+ );
76
+ return container;
77
+ },
78
+ registerMany(providers) {
79
+ for (const provider of providers) {
80
+ root.registerProvider(
81
+ provider,
82
+ false
83
+ );
84
+ }
85
+ return container;
86
+ },
87
+ providers() {
88
+ return Array.from(
89
+ root.providersMap.values()
90
+ );
91
+ }
92
+ };
93
+ for (const provider of options.providers ?? []) {
94
+ root.registerProvider(
95
+ provider,
96
+ false
97
+ );
98
+ }
99
+ return container;
100
+ }
101
+ var ScopeImpl = class _ScopeImpl {
102
+ constructor(name, parent) {
103
+ this.name = name;
104
+ this.parent = parent;
105
+ if (parent) {
106
+ this.root = parent.root;
107
+ parent.children.add(this);
108
+ }
109
+ }
110
+ name;
111
+ parent;
112
+ root;
113
+ providersMap = /* @__PURE__ */ new Map();
114
+ overrideProviders = /* @__PURE__ */ new Map();
115
+ singletonCache = /* @__PURE__ */ new Map();
116
+ scopedCache = /* @__PURE__ */ new Map();
117
+ disposals = [];
118
+ children = /* @__PURE__ */ new Set();
119
+ state = "active";
120
+ disposePromise;
121
+ has(token) {
122
+ assertToken(token);
123
+ return this.findProvider(token) !== void 0;
124
+ }
125
+ resolve(token) {
126
+ return this.resolveWithPath(
127
+ token,
128
+ []
129
+ );
130
+ }
131
+ async optional(token) {
132
+ if (!this.has(token)) {
133
+ return void 0;
134
+ }
135
+ return this.resolve(token);
136
+ }
137
+ createScope(options = {}) {
138
+ this.assertActive();
139
+ const scope = new _ScopeImpl(
140
+ normalizeOptionalName(
141
+ options.name
142
+ ) ?? `${this.name}:scope-${this.children.size + 1}`,
143
+ this
144
+ );
145
+ for (const provider of options.overrides ?? []) {
146
+ scope.registerOverride(
147
+ provider
148
+ );
149
+ }
150
+ return scope;
151
+ }
152
+ graph() {
153
+ const effective = /* @__PURE__ */ new Map();
154
+ let current = this;
155
+ while (current) {
156
+ for (const [id, provider] of current.overrideProviders) {
157
+ if (!effective.has(id)) {
158
+ effective.set(
159
+ id,
160
+ {
161
+ provider,
162
+ owner: current,
163
+ overridden: true
164
+ }
165
+ );
166
+ }
167
+ }
168
+ current = current.parent;
169
+ }
170
+ for (const [id, provider] of this.root.providersMap) {
171
+ if (!effective.has(id)) {
172
+ effective.set(
173
+ id,
174
+ {
175
+ provider,
176
+ owner: this.root,
177
+ overridden: false
178
+ }
179
+ );
180
+ }
181
+ }
182
+ return Array.from(
183
+ effective.values(),
184
+ (record) => ({
185
+ token: record.provider.token,
186
+ description: record.provider.token.description,
187
+ lifetime: normalizeLifetime(
188
+ record.provider.lifetime
189
+ ),
190
+ dependencies: (record.provider.dependencies ?? []).map(
191
+ (dependency) => dependency.description
192
+ ),
193
+ overridden: record.overridden
194
+ })
195
+ ).sort(
196
+ (left, right) => left.description.localeCompare(
197
+ right.description
198
+ )
199
+ );
200
+ }
201
+ dispose() {
202
+ if (this.disposePromise) {
203
+ return this.disposePromise;
204
+ }
205
+ if (this.state === "disposed") {
206
+ return Promise.resolve();
207
+ }
208
+ this.disposePromise = this.disposeInternal();
209
+ return this.disposePromise;
210
+ }
211
+ registerProvider(provider, replace) {
212
+ this.assertActive();
213
+ if (this.parent) {
214
+ throw new Error(
215
+ "BCP Container: providers can only be registered on the root container. Use scope overrides for child scopes."
216
+ );
217
+ }
218
+ validateProvider(provider);
219
+ const id = provider.token.id;
220
+ if (this.providersMap.has(id) && !replace) {
221
+ throw new Error(
222
+ `BCP Container: service "${provider.token.description}" is already registered.`
223
+ );
224
+ }
225
+ if (replace && this.hasResolved(id)) {
226
+ throw new Error(
227
+ `BCP Container: service "${provider.token.description}" cannot be replaced after it has been resolved.`
228
+ );
229
+ }
230
+ this.providersMap.set(
231
+ id,
232
+ provider
233
+ );
234
+ }
235
+ registerOverride(provider) {
236
+ validateProvider(provider);
237
+ const id = provider.token.id;
238
+ if (this.overrideProviders.has(id)) {
239
+ throw new Error(
240
+ `BCP Container: scope override for "${provider.token.description}" is already registered.`
241
+ );
242
+ }
243
+ this.overrideProviders.set(
244
+ id,
245
+ provider
246
+ );
247
+ }
248
+ async resolveWithPath(token, path) {
249
+ this.assertActive();
250
+ assertToken(token);
251
+ if (path.some(
252
+ (item) => item.id === token.id
253
+ )) {
254
+ throw new ServiceResolutionError(
255
+ token,
256
+ `Circular dependency detected: ${[
257
+ ...path,
258
+ token
259
+ ].map(
260
+ (item) => item.description
261
+ ).join(" -> ")}.`
262
+ );
263
+ }
264
+ const record = this.findProvider(token);
265
+ if (!record) {
266
+ throw new ServiceNotFoundError(
267
+ token
268
+ );
269
+ }
270
+ const lifetime = normalizeLifetime(
271
+ record.provider.lifetime
272
+ );
273
+ const cache = lifetime === "singleton" ? record.owner.singletonCache : lifetime === "scoped" ? this.scopedCache : void 0;
274
+ const existing = cache?.get(token.id);
275
+ if (existing) {
276
+ return existing;
277
+ }
278
+ const resolution = this.instantiate(
279
+ record.provider,
280
+ [
281
+ ...path,
282
+ token
283
+ ],
284
+ lifetime,
285
+ record.owner
286
+ );
287
+ cache?.set(
288
+ token.id,
289
+ resolution
290
+ );
291
+ try {
292
+ return await resolution;
293
+ } catch (error) {
294
+ cache?.delete(token.id);
295
+ if (error instanceof ServiceNotFoundError || error instanceof ServiceResolutionError) {
296
+ throw error;
297
+ }
298
+ throw new ServiceResolutionError(
299
+ token,
300
+ error instanceof Error ? error.message : String(error),
301
+ error
302
+ );
303
+ }
304
+ }
305
+ async instantiate(provider, path, lifetime, providerOwner) {
306
+ const dependencies = await Promise.all(
307
+ (provider.dependencies ?? []).map(
308
+ (dependency) => this.resolveWithPath(
309
+ dependency,
310
+ path
311
+ )
312
+ )
313
+ );
314
+ const context = {
315
+ scope: this,
316
+ resolve: (dependency) => this.resolveWithPath(
317
+ dependency,
318
+ path
319
+ ),
320
+ optional: async (dependency) => {
321
+ if (!this.has(dependency)) {
322
+ return void 0;
323
+ }
324
+ return this.resolveWithPath(
325
+ dependency,
326
+ path
327
+ );
328
+ }
329
+ };
330
+ const value = await provider.factory(
331
+ context,
332
+ dependencies
333
+ );
334
+ if (provider.dispose) {
335
+ const disposalOwner = lifetime === "singleton" ? providerOwner : this;
336
+ disposalOwner.disposals.push({
337
+ provider,
338
+ value
339
+ });
340
+ }
341
+ return value;
342
+ }
343
+ findProvider(token) {
344
+ let current = this;
345
+ while (current) {
346
+ const override = current.overrideProviders.get(
347
+ token.id
348
+ );
349
+ if (override) {
350
+ return {
351
+ provider: override,
352
+ owner: current,
353
+ overridden: true
354
+ };
355
+ }
356
+ current = current.parent;
357
+ }
358
+ const provider = this.root.providersMap.get(
359
+ token.id
360
+ );
361
+ if (!provider) {
362
+ return void 0;
363
+ }
364
+ return {
365
+ provider,
366
+ owner: this.root,
367
+ overridden: false
368
+ };
369
+ }
370
+ async disposeInternal() {
371
+ this.state = "disposing";
372
+ const errors = [];
373
+ for (const child of Array.from(
374
+ this.children
375
+ ).reverse()) {
376
+ try {
377
+ await child.dispose();
378
+ } catch (error) {
379
+ errors.push(error);
380
+ }
381
+ }
382
+ for (const record of [...this.disposals].reverse()) {
383
+ if (!record.provider.dispose) {
384
+ continue;
385
+ }
386
+ try {
387
+ await record.provider.dispose(
388
+ record.value
389
+ );
390
+ } catch (error) {
391
+ errors.push(error);
392
+ }
393
+ }
394
+ this.disposals.length = 0;
395
+ this.singletonCache.clear();
396
+ this.scopedCache.clear();
397
+ this.children.clear();
398
+ this.parent?.children.delete(this);
399
+ this.state = "disposed";
400
+ if (errors.length > 0) {
401
+ throw new ServiceDisposalError(
402
+ errors
403
+ );
404
+ }
405
+ }
406
+ hasResolved(id) {
407
+ if (this.singletonCache.has(id) || this.scopedCache.has(id)) {
408
+ return true;
409
+ }
410
+ for (const child of this.children) {
411
+ if (child.hasResolved(id)) {
412
+ return true;
413
+ }
414
+ }
415
+ return false;
416
+ }
417
+ assertActive() {
418
+ if (this.state !== "active") {
419
+ throw new Error(
420
+ `BCP Container: scope "${this.name}" is ${this.state}.`
421
+ );
422
+ }
423
+ }
424
+ };
425
+ function validateProvider(provider) {
426
+ if (!provider || typeof provider !== "object") {
427
+ throw new TypeError(
428
+ "BCP Container: provider must be an object."
429
+ );
430
+ }
431
+ assertToken(provider.token);
432
+ if (typeof provider.factory !== "function") {
433
+ throw new TypeError(
434
+ `BCP Container: provider "${provider.token.description}" must define factory().`
435
+ );
436
+ }
437
+ normalizeLifetime(
438
+ provider.lifetime
439
+ );
440
+ assertDependencies(
441
+ provider.dependencies ?? []
442
+ );
443
+ if (provider.dispose !== void 0 && typeof provider.dispose !== "function") {
444
+ throw new TypeError(
445
+ `BCP Container: provider "${provider.token.description}" dispose must be a function.`
446
+ );
447
+ }
448
+ }
449
+ function assertToken(token) {
450
+ if (!token || typeof token !== "object" || typeof token.id !== "symbol" || typeof token.description !== "string" || token.description.trim() === "") {
451
+ throw new TypeError(
452
+ "BCP Container: invalid service token. Use createServiceToken()."
453
+ );
454
+ }
455
+ }
456
+ function assertDependencies(dependencies) {
457
+ if (!Array.isArray(dependencies)) {
458
+ throw new TypeError(
459
+ "BCP Container: provider dependencies must be an array."
460
+ );
461
+ }
462
+ for (const dependency of dependencies) {
463
+ assertToken(dependency);
464
+ }
465
+ }
466
+ function normalizeLifetime(lifetime) {
467
+ const normalized = lifetime ?? "singleton";
468
+ if (normalized !== "singleton" && normalized !== "scoped" && normalized !== "transient") {
469
+ throw new TypeError(
470
+ "BCP Container: service lifetime must be singleton, scoped or transient."
471
+ );
472
+ }
473
+ return normalized;
474
+ }
475
+ function normalizeName(value, label) {
476
+ if (typeof value !== "string") {
477
+ throw new TypeError(
478
+ `BCP Container: ${label} must be a string.`
479
+ );
480
+ }
481
+ const normalized = value.trim();
482
+ if (!normalized) {
483
+ throw new TypeError(
484
+ `BCP Container: ${label} cannot be empty.`
485
+ );
486
+ }
487
+ if (normalized.length > 256 || /[\r\n]/.test(normalized)) {
488
+ throw new TypeError(
489
+ `BCP Container: ${label} must be at most 256 characters without line breaks.`
490
+ );
491
+ }
492
+ return normalized;
493
+ }
494
+ function normalizeOptionalName(value) {
495
+ if (value === void 0) {
496
+ return void 0;
497
+ }
498
+ return normalizeName(
499
+ value,
500
+ "scope name"
501
+ );
502
+ }
503
+
1
504
  // packages/server/src/deployment.ts
2
505
  import {
3
506
  randomUUID
@@ -73,7 +576,7 @@ function createDeploymentRuntime(options) {
73
576
  "runtime start timestamp"
74
577
  );
75
578
  const metadata = {
76
- serviceName: normalizeName(
579
+ serviceName: normalizeName2(
77
580
  options.serviceName,
78
581
  "serviceName"
79
582
  ),
@@ -84,7 +587,7 @@ function createDeploymentRuntime(options) {
84
587
  options.version
85
588
  )
86
589
  } : {},
87
- deploymentId: normalizeName(
590
+ deploymentId: normalizeName2(
88
591
  options.deploymentId ?? environment.BCP_DEPLOYMENT_ID ?? idFactory(),
89
592
  "deploymentId"
90
593
  ),
@@ -132,7 +635,7 @@ function createDeploymentRuntime(options) {
132
635
  "BCP Deployment: resources can only be registered before runtime start."
133
636
  );
134
637
  }
135
- const name = normalizeName(
638
+ const name = normalizeName2(
136
639
  resource.name,
137
640
  "resource name"
138
641
  );
@@ -496,7 +999,7 @@ function normalizeSignals(signals) {
496
999
  new Set(signals)
497
1000
  );
498
1001
  }
499
- function normalizeName(value, field) {
1002
+ function normalizeName2(value, field) {
500
1003
  const normalized = String(value ?? "").trim();
501
1004
  if (!normalized) {
502
1005
  throw new TypeError(
@@ -579,6 +1082,382 @@ function withTimeout(promise, timeoutMs, label) {
579
1082
  );
580
1083
  }
581
1084
 
1085
+ // packages/server/src/modules.ts
1086
+ var MODULE_V2_KIND = "bcp-module-v2";
1087
+ var ModuleDependencyError = class extends Error {
1088
+ constructor(message) {
1089
+ super(message);
1090
+ this.name = "ModuleDependencyError";
1091
+ }
1092
+ };
1093
+ var ModuleLifecycleError = class extends Error {
1094
+ module;
1095
+ phase;
1096
+ cause;
1097
+ constructor(moduleName, phase, cause) {
1098
+ super(
1099
+ `BCP Modules: ${phase} failed for module "${moduleName}": ${formatError(cause)}`
1100
+ );
1101
+ this.name = "ModuleLifecycleError";
1102
+ this.module = moduleName;
1103
+ this.phase = phase;
1104
+ this.cause = cause;
1105
+ }
1106
+ };
1107
+ function isModuleDefinition(value) {
1108
+ return Boolean(
1109
+ value && typeof value === "object" && value.kind === MODULE_V2_KIND
1110
+ );
1111
+ }
1112
+ function composeModules(roots = []) {
1113
+ const ordered = resolveModuleOrder(roots);
1114
+ const providers = [];
1115
+ const plugins = [];
1116
+ const services = [];
1117
+ const resources = [];
1118
+ const serviceKeys = /* @__PURE__ */ new Set();
1119
+ for (const module of ordered) {
1120
+ providers.push(
1121
+ ...module.providers ?? []
1122
+ );
1123
+ plugins.push(
1124
+ ...module.plugins ?? []
1125
+ );
1126
+ resources.push(
1127
+ ...module.resources ?? []
1128
+ );
1129
+ for (const entry of module.services ?? []) {
1130
+ const [key] = entry;
1131
+ if (serviceKeys.has(key)) {
1132
+ throw new ModuleDependencyError(
1133
+ `BCP Modules: duplicate shared service ${formatServiceKey(key)} while composing module "${module.name}".`
1134
+ );
1135
+ }
1136
+ serviceKeys.add(key);
1137
+ services.push(entry);
1138
+ }
1139
+ validateModuleExports(
1140
+ module
1141
+ );
1142
+ }
1143
+ return {
1144
+ modules: ordered,
1145
+ providers,
1146
+ plugins,
1147
+ services,
1148
+ resources,
1149
+ records() {
1150
+ return ordered.map(
1151
+ (module) => ({
1152
+ name: module.name,
1153
+ ...module.version ? {
1154
+ version: module.version
1155
+ } : {},
1156
+ imports: (module.imports ?? []).map(
1157
+ (dependency) => dependency.name
1158
+ ),
1159
+ providers: (module.providers ?? []).map(
1160
+ (provider) => provider.token.description
1161
+ ),
1162
+ exports: (module.exports ?? []).map(
1163
+ (token) => token.description
1164
+ ),
1165
+ plugins: (module.plugins ?? []).map(
1166
+ (plugin) => plugin.name
1167
+ ),
1168
+ resources: (module.resources ?? []).map(
1169
+ (resource) => resource.name
1170
+ )
1171
+ })
1172
+ );
1173
+ },
1174
+ exportedTokens(moduleName) {
1175
+ const normalized = normalizeName3(
1176
+ moduleName,
1177
+ "module name"
1178
+ );
1179
+ const module = ordered.find(
1180
+ (entry) => entry.name === normalized
1181
+ );
1182
+ if (!module) {
1183
+ throw new ModuleDependencyError(
1184
+ `BCP Modules: module "${normalized}" is not part of this composition.`
1185
+ );
1186
+ }
1187
+ return [
1188
+ ...module.exports ?? []
1189
+ ];
1190
+ },
1191
+ createLifecycleResources(context) {
1192
+ return ordered.flatMap(
1193
+ (module) => [
1194
+ ...module.resources ?? [],
1195
+ createModuleLifecycleResource(
1196
+ module,
1197
+ context
1198
+ )
1199
+ ]
1200
+ );
1201
+ }
1202
+ };
1203
+ }
1204
+ function resolveModuleOrder(roots) {
1205
+ const byName = /* @__PURE__ */ new Map();
1206
+ const visiting = /* @__PURE__ */ new Set();
1207
+ const visited = /* @__PURE__ */ new Set();
1208
+ const order = [];
1209
+ const visit = (module, path) => {
1210
+ validateModuleDefinition(module);
1211
+ const name = module.name;
1212
+ const existing = byName.get(name);
1213
+ if (existing && existing !== module) {
1214
+ throw new ModuleDependencyError(
1215
+ `BCP Modules: duplicate module name "${name}" refers to different definitions.`
1216
+ );
1217
+ }
1218
+ byName.set(name, module);
1219
+ if (visited.has(name)) {
1220
+ return;
1221
+ }
1222
+ if (visiting.has(name)) {
1223
+ throw new ModuleDependencyError(
1224
+ `BCP Modules: circular module dependency detected: ${[
1225
+ ...path,
1226
+ name
1227
+ ].join(" -> ")}.`
1228
+ );
1229
+ }
1230
+ visiting.add(name);
1231
+ for (const dependency of module.imports ?? []) {
1232
+ visit(
1233
+ dependency,
1234
+ [
1235
+ ...path,
1236
+ name
1237
+ ]
1238
+ );
1239
+ }
1240
+ visiting.delete(name);
1241
+ visited.add(name);
1242
+ order.push(module);
1243
+ };
1244
+ for (const root of roots) {
1245
+ visit(root, []);
1246
+ }
1247
+ return order;
1248
+ }
1249
+ function createModuleLifecycleResource(module, shared) {
1250
+ const config = parseModuleConfig(
1251
+ module.schema,
1252
+ module.config
1253
+ );
1254
+ const context = {
1255
+ name: module.name,
1256
+ config,
1257
+ container: shared.container,
1258
+ services: shared.services,
1259
+ hooks: shared.hooks
1260
+ };
1261
+ let setupComplete = false;
1262
+ let disposed = false;
1263
+ return {
1264
+ name: `bcp:module:${module.name}`,
1265
+ async start() {
1266
+ try {
1267
+ if (!setupComplete) {
1268
+ await runModuleHook(
1269
+ module,
1270
+ "setup",
1271
+ module.setup,
1272
+ context
1273
+ );
1274
+ setupComplete = true;
1275
+ }
1276
+ await runModuleHook(
1277
+ module,
1278
+ "start",
1279
+ module.start,
1280
+ context
1281
+ );
1282
+ } catch (error) {
1283
+ if (!disposed) {
1284
+ disposed = true;
1285
+ try {
1286
+ await runModuleHook(
1287
+ module,
1288
+ "dispose",
1289
+ module.dispose,
1290
+ context
1291
+ );
1292
+ } catch (disposeError) {
1293
+ throw new AggregateError(
1294
+ [
1295
+ error,
1296
+ disposeError
1297
+ ],
1298
+ `BCP Modules: startup and cleanup failed for module "${module.name}".`
1299
+ );
1300
+ }
1301
+ }
1302
+ throw error;
1303
+ }
1304
+ },
1305
+ ready() {
1306
+ return true;
1307
+ },
1308
+ async stop() {
1309
+ const errors = [];
1310
+ try {
1311
+ await runModuleHook(
1312
+ module,
1313
+ "stop",
1314
+ module.stop,
1315
+ context
1316
+ );
1317
+ } catch (error) {
1318
+ errors.push(error);
1319
+ }
1320
+ if (!disposed) {
1321
+ disposed = true;
1322
+ try {
1323
+ await runModuleHook(
1324
+ module,
1325
+ "dispose",
1326
+ module.dispose,
1327
+ context
1328
+ );
1329
+ } catch (error) {
1330
+ errors.push(error);
1331
+ }
1332
+ }
1333
+ if (errors.length > 0) {
1334
+ throw new AggregateError(
1335
+ errors,
1336
+ `BCP Modules: shutdown failed for module "${module.name}".`
1337
+ );
1338
+ }
1339
+ },
1340
+ diagnostics() {
1341
+ return {
1342
+ name: module.name,
1343
+ ...module.version ? {
1344
+ version: module.version
1345
+ } : {},
1346
+ imports: (module.imports ?? []).map(
1347
+ (dependency) => dependency.name
1348
+ ),
1349
+ exports: (module.exports ?? []).map(
1350
+ (token) => token.description
1351
+ )
1352
+ };
1353
+ }
1354
+ };
1355
+ }
1356
+ async function runModuleHook(module, phase, hook, context) {
1357
+ if (!hook) {
1358
+ return;
1359
+ }
1360
+ try {
1361
+ await hook(context);
1362
+ } catch (error) {
1363
+ throw new ModuleLifecycleError(
1364
+ module.name,
1365
+ phase,
1366
+ error
1367
+ );
1368
+ }
1369
+ }
1370
+ function validateModuleDefinition(module) {
1371
+ if (!module || typeof module !== "object") {
1372
+ throw new TypeError(
1373
+ "BCP Modules: module must be an object."
1374
+ );
1375
+ }
1376
+ if (module.kind !== MODULE_V2_KIND) {
1377
+ throw new TypeError(
1378
+ `BCP Modules: module kind must be "${MODULE_V2_KIND}". Use defineModule().`
1379
+ );
1380
+ }
1381
+ normalizeName3(
1382
+ module.name,
1383
+ "module name"
1384
+ );
1385
+ for (const [label, value] of [
1386
+ ["imports", module.imports],
1387
+ ["providers", module.providers],
1388
+ ["exports", module.exports],
1389
+ ["plugins", module.plugins],
1390
+ ["resources", module.resources]
1391
+ ]) {
1392
+ if (value !== void 0 && !Array.isArray(value)) {
1393
+ throw new TypeError(
1394
+ `BCP Modules: ${label} must be an array.`
1395
+ );
1396
+ }
1397
+ }
1398
+ for (const [phase, hook] of [
1399
+ ["setup", module.setup],
1400
+ ["start", module.start],
1401
+ ["stop", module.stop],
1402
+ ["dispose", module.dispose]
1403
+ ]) {
1404
+ if (hook !== void 0 && typeof hook !== "function") {
1405
+ throw new TypeError(
1406
+ `BCP Modules: ${phase} must be a function.`
1407
+ );
1408
+ }
1409
+ }
1410
+ }
1411
+ function validateModuleExports(module) {
1412
+ const available = /* @__PURE__ */ new Set();
1413
+ for (const provider of module.providers ?? []) {
1414
+ available.add(
1415
+ provider.token.id
1416
+ );
1417
+ }
1418
+ for (const dependency of module.imports ?? []) {
1419
+ for (const token of dependency.exports ?? []) {
1420
+ available.add(token.id);
1421
+ }
1422
+ }
1423
+ for (const token of module.exports ?? []) {
1424
+ if (!available.has(token.id)) {
1425
+ throw new ModuleDependencyError(
1426
+ `BCP Modules: module "${module.name}" exports "${token.description}" but does not provide or import it.`
1427
+ );
1428
+ }
1429
+ }
1430
+ }
1431
+ function parseModuleConfig(parser, value) {
1432
+ if (!parser) {
1433
+ return value;
1434
+ }
1435
+ if (typeof parser === "function") {
1436
+ return parser(value);
1437
+ }
1438
+ return parser.parse(value);
1439
+ }
1440
+ function normalizeName3(value, label) {
1441
+ if (typeof value !== "string") {
1442
+ throw new TypeError(
1443
+ `BCP Modules: ${label} must be a string.`
1444
+ );
1445
+ }
1446
+ const normalized = value.trim();
1447
+ if (!normalized) {
1448
+ throw new TypeError(
1449
+ `BCP Modules: ${label} cannot be empty.`
1450
+ );
1451
+ }
1452
+ return normalized;
1453
+ }
1454
+ function formatServiceKey(key) {
1455
+ return typeof key === "symbol" ? key.description ? `Symbol(${key.description})` : key.toString() : key;
1456
+ }
1457
+ function formatError(value) {
1458
+ return value instanceof Error ? value.message : String(value);
1459
+ }
1460
+
582
1461
  // packages/server/src/plugins.ts
583
1462
  var PluginDependencyError = class extends Error {
584
1463
  constructor(message) {
@@ -592,7 +1471,7 @@ var PluginLifecycleError = class extends Error {
592
1471
  cause;
593
1472
  constructor(plugin, phase, cause) {
594
1473
  super(
595
- `BCP Plugins: ${phase} failed for plugin "${plugin}": ${formatError(cause)}`
1474
+ `BCP Plugins: ${phase} failed for plugin "${plugin}": ${formatError2(cause)}`
596
1475
  );
597
1476
  this.name = "PluginLifecycleError";
598
1477
  this.plugin = plugin;
@@ -606,7 +1485,7 @@ function defineModule(module) {
606
1485
  "BCP Plugins: module must be an object."
607
1486
  );
608
1487
  }
609
- normalizeName2(
1488
+ normalizeName4(
610
1489
  module.name,
611
1490
  "module name"
612
1491
  );
@@ -627,7 +1506,7 @@ function createPluginServiceRegistry(initial) {
627
1506
  assertServiceKey(key);
628
1507
  if (values.has(key)) {
629
1508
  throw new Error(
630
- `BCP Plugins: duplicate initial service ${formatServiceKey(key)}.`
1509
+ `BCP Plugins: duplicate initial service ${formatServiceKey2(key)}.`
631
1510
  );
632
1511
  }
633
1512
  values.set(key, value);
@@ -638,7 +1517,7 @@ function createPluginServiceRegistry(initial) {
638
1517
  assertServiceKey(key);
639
1518
  if (values.has(key) && !options.replace) {
640
1519
  throw new Error(
641
- `BCP Plugins: service ${formatServiceKey(key)} is already registered.`
1520
+ `BCP Plugins: service ${formatServiceKey2(key)} is already registered.`
642
1521
  );
643
1522
  }
644
1523
  values.set(key, value);
@@ -647,7 +1526,7 @@ function createPluginServiceRegistry(initial) {
647
1526
  assertServiceKey(key);
648
1527
  if (!values.has(key)) {
649
1528
  throw new Error(
650
- `BCP Plugins: service ${formatServiceKey(key)} is not registered.`
1529
+ `BCP Plugins: service ${formatServiceKey2(key)} is not registered.`
651
1530
  );
652
1531
  }
653
1532
  return values.get(key);
@@ -676,7 +1555,7 @@ function createPluginHookBus() {
676
1555
  const hooks = /* @__PURE__ */ new Map();
677
1556
  const bus = {
678
1557
  on(name, handler) {
679
- const normalized = normalizeName2(
1558
+ const normalized = normalizeName4(
680
1559
  name,
681
1560
  "hook name"
682
1561
  );
@@ -706,7 +1585,7 @@ function createPluginHookBus() {
706
1585
  };
707
1586
  },
708
1587
  async emit(name, payload) {
709
- const normalized = normalizeName2(
1588
+ const normalized = normalizeName4(
710
1589
  name,
711
1590
  "hook name"
712
1591
  );
@@ -722,7 +1601,7 @@ function createPluginHookBus() {
722
1601
  },
723
1602
  listenerCount(name) {
724
1603
  return hooks.get(
725
- normalizeName2(
1604
+ normalizeName4(
726
1605
  name,
727
1606
  "hook name"
728
1607
  )
@@ -734,7 +1613,7 @@ function createPluginHookBus() {
734
1613
  return;
735
1614
  }
736
1615
  hooks.delete(
737
- normalizeName2(
1616
+ normalizeName4(
738
1617
  name,
739
1618
  "hook name"
740
1619
  )
@@ -967,7 +1846,7 @@ function createPluginHost(options = {}) {
967
1846
  },
968
1847
  plugin(name) {
969
1848
  const entry = entries.get(
970
- normalizeName2(
1849
+ normalizeName4(
971
1850
  name,
972
1851
  "plugin name"
973
1852
  )
@@ -1001,7 +1880,7 @@ function createPluginHost(options = {}) {
1001
1880
  validatePluginDefinition(
1002
1881
  definition
1003
1882
  );
1004
- const name = normalizeName2(
1883
+ const name = normalizeName4(
1005
1884
  definition.name,
1006
1885
  "plugin name"
1007
1886
  );
@@ -1218,7 +2097,7 @@ function validatePluginDefinition(definition) {
1218
2097
  "BCP Plugins: plugin definition must be an object."
1219
2098
  );
1220
2099
  }
1221
- const name = normalizeName2(
2100
+ const name = normalizeName4(
1222
2101
  definition.name,
1223
2102
  "plugin name"
1224
2103
  );
@@ -1261,7 +2140,7 @@ function normalizeDependencyList(value, plugin, field) {
1261
2140
  );
1262
2141
  }
1263
2142
  const normalized = value.map(
1264
- (item) => normalizeName2(
2143
+ (item) => normalizeName4(
1265
2144
  item,
1266
2145
  `${field} dependency`
1267
2146
  )
@@ -1302,7 +2181,7 @@ function requireContext(entry) {
1302
2181
  }
1303
2182
  function markFailed(entry, error) {
1304
2183
  entry.record.state = "failed";
1305
- entry.record.error = formatError(error);
2184
+ entry.record.error = formatError2(error);
1306
2185
  }
1307
2186
  function cloneRecord(record) {
1308
2187
  return {
@@ -1322,7 +2201,7 @@ function isPluginModule(value) {
1322
2201
  )
1323
2202
  );
1324
2203
  }
1325
- function normalizeName2(value, field) {
2204
+ function normalizeName4(value, field) {
1326
2205
  const text = String(value ?? "").trim();
1327
2206
  if (!text) {
1328
2207
  throw new TypeError(
@@ -1340,14 +2219,14 @@ function normalizeOptionalVersion(value) {
1340
2219
  if (value === void 0) {
1341
2220
  return void 0;
1342
2221
  }
1343
- return normalizeName2(
2222
+ return normalizeName4(
1344
2223
  value,
1345
2224
  "plugin version"
1346
2225
  );
1347
2226
  }
1348
2227
  function assertServiceKey(key) {
1349
2228
  if (typeof key === "string") {
1350
- normalizeName2(
2229
+ normalizeName4(
1351
2230
  key,
1352
2231
  "service key"
1353
2232
  );
@@ -1359,10 +2238,10 @@ function assertServiceKey(key) {
1359
2238
  );
1360
2239
  }
1361
2240
  }
1362
- function formatServiceKey(key) {
2241
+ function formatServiceKey2(key) {
1363
2242
  return typeof key === "symbol" ? String(key) : `"${key}"`;
1364
2243
  }
1365
- function formatError(error) {
2244
+ function formatError2(error) {
1366
2245
  if (error instanceof Error) {
1367
2246
  return error.message || error.name;
1368
2247
  }
@@ -1386,7 +2265,7 @@ var ApplicationLifecycleError = class extends Error {
1386
2265
  cause;
1387
2266
  constructor(phase, cause) {
1388
2267
  super(
1389
- `BCP Application: ${phase} failed: ${formatError2(cause)}`
2268
+ `BCP Application: ${phase} failed: ${formatError3(cause)}`
1390
2269
  );
1391
2270
  this.name = "ApplicationLifecycleError";
1392
2271
  this.phase = phase;
@@ -1399,7 +2278,7 @@ function defineApp(definition) {
1399
2278
  }
1400
2279
  function createApp(definition) {
1401
2280
  defineApp(definition);
1402
- const name = normalizeName3(
2281
+ const name = normalizeName5(
1403
2282
  definition.name,
1404
2283
  "application name"
1405
2284
  );
@@ -1410,11 +2289,33 @@ function createApp(definition) {
1410
2289
  definition.schema,
1411
2290
  definition.config
1412
2291
  );
2292
+ const moduleInputs = definition.modules ?? [];
2293
+ const modules = composeModules(
2294
+ moduleInputs.filter(
2295
+ isModuleDefinition
2296
+ )
2297
+ );
2298
+ const legacyModules = moduleInputs.filter(
2299
+ (module) => !isModuleDefinition(module)
2300
+ );
2301
+ const container = createServiceContainer({
2302
+ name: `${name}:container`,
2303
+ providers: [
2304
+ ...modules.providers,
2305
+ ...definition.providers ?? []
2306
+ ]
2307
+ });
1413
2308
  const plugins = createPluginHost({
1414
- plugins: definition.plugins,
1415
- modules: definition.modules,
2309
+ plugins: [
2310
+ ...modules.plugins,
2311
+ ...definition.plugins ?? []
2312
+ ],
2313
+ modules: legacyModules,
1416
2314
  configs: definition.pluginConfigs,
1417
- services: definition.services
2315
+ services: [
2316
+ ...modules.services,
2317
+ ...definition.services ?? []
2318
+ ]
1418
2319
  });
1419
2320
  const deployment = createDeploymentRuntime({
1420
2321
  ...definition.deployment ?? {},
@@ -1436,6 +2337,8 @@ function createApp(definition) {
1436
2337
  version
1437
2338
  } : {},
1438
2339
  config,
2340
+ container,
2341
+ modules,
1439
2342
  services: plugins.services,
1440
2343
  hooks: plugins.hooks,
1441
2344
  plugins,
@@ -1447,6 +2350,29 @@ function createApp(definition) {
1447
2350
  return state;
1448
2351
  }
1449
2352
  };
2353
+ deployment.addResource({
2354
+ name: "bcp:container",
2355
+ ready() {
2356
+ return {
2357
+ ok: container.state === "active",
2358
+ detail: `Service container is ${container.state}.`
2359
+ };
2360
+ },
2361
+ async stop() {
2362
+ await container.dispose();
2363
+ },
2364
+ diagnostics() {
2365
+ return {
2366
+ state: container.state,
2367
+ providers: container.graph().map(
2368
+ (node) => ({
2369
+ token: node.description,
2370
+ lifetime: node.lifetime
2371
+ })
2372
+ )
2373
+ };
2374
+ }
2375
+ });
1450
2376
  deployment.addResource({
1451
2377
  name: "bcp:plugins",
1452
2378
  async start() {
@@ -1468,6 +2394,13 @@ function createApp(definition) {
1468
2394
  };
1469
2395
  }
1470
2396
  });
2397
+ for (const resource of modules.createLifecycleResources({
2398
+ container,
2399
+ services: plugins.services,
2400
+ hooks: plugins.hooks
2401
+ })) {
2402
+ deployment.addResource(resource);
2403
+ }
1471
2404
  for (const resource of definition.resources ?? []) {
1472
2405
  deployment.addResource(resource);
1473
2406
  }
@@ -1478,6 +2411,8 @@ function createApp(definition) {
1478
2411
  } : {},
1479
2412
  config,
1480
2413
  context,
2414
+ container,
2415
+ modules,
1481
2416
  services: plugins.services,
1482
2417
  hooks: plugins.hooks,
1483
2418
  plugins,
@@ -1499,6 +2434,19 @@ function createApp(definition) {
1499
2434
  );
1500
2435
  return app;
1501
2436
  },
2437
+ register(provider, options = {}) {
2438
+ assertMutable();
2439
+ container.register(
2440
+ provider,
2441
+ options
2442
+ );
2443
+ return app;
2444
+ },
2445
+ createScope(options = {}) {
2446
+ return container.createScope(
2447
+ options
2448
+ );
2449
+ },
1502
2450
  addResource(resource) {
1503
2451
  assertMutable();
1504
2452
  deployment.addResource(
@@ -1675,6 +2623,13 @@ function createApp(definition) {
1675
2623
  errors.push(error);
1676
2624
  }
1677
2625
  }
2626
+ if (container.state !== "disposed") {
2627
+ try {
2628
+ await container.dispose();
2629
+ } catch (error) {
2630
+ errors.push(error);
2631
+ }
2632
+ }
1678
2633
  try {
1679
2634
  await disposeApplication();
1680
2635
  } catch (error) {
@@ -1720,8 +2675,12 @@ function createApp(definition) {
1720
2675
  } : {},
1721
2676
  state,
1722
2677
  services: plugins.services.keys().map(
1723
- formatServiceKey2
2678
+ formatServiceKey3
1724
2679
  ),
2680
+ containerProviders: container.graph().map(
2681
+ (node) => node.description
2682
+ ),
2683
+ modules: modules.records(),
1725
2684
  pluginCount: plugins.plugins().length
1726
2685
  };
1727
2686
  }
@@ -1741,6 +2700,12 @@ function createApp(definition) {
1741
2700
  await plugins.close();
1742
2701
  } catch {
1743
2702
  }
2703
+ if (container.state !== "disposed") {
2704
+ try {
2705
+ await container.dispose();
2706
+ } catch {
2707
+ }
2708
+ }
1744
2709
  try {
1745
2710
  await disposeApplication();
1746
2711
  } catch {
@@ -1772,7 +2737,7 @@ function createApp(definition) {
1772
2737
  function assertMutable() {
1773
2738
  if (state !== "created") {
1774
2739
  throw new Error(
1775
- "BCP Application: plugins, services and resources must be registered before start()."
2740
+ "BCP Application: plugins, services, providers and resources must be registered before start()."
1776
2741
  );
1777
2742
  }
1778
2743
  }
@@ -1797,13 +2762,18 @@ function validateDefinition(definition) {
1797
2762
  "BCP Application: definition must be an object."
1798
2763
  );
1799
2764
  }
1800
- normalizeName3(
2765
+ normalizeName5(
1801
2766
  definition.name,
1802
2767
  "application name"
1803
2768
  );
1804
2769
  normalizeOptionalText2(
1805
2770
  definition.version
1806
2771
  );
2772
+ if (definition.providers !== void 0 && !Array.isArray(definition.providers)) {
2773
+ throw new TypeError(
2774
+ "BCP Application: providers must be an array."
2775
+ );
2776
+ }
1807
2777
  if (definition.plugins !== void 0 && !Array.isArray(definition.plugins)) {
1808
2778
  throw new TypeError(
1809
2779
  "BCP Application: plugins must be an array."
@@ -1832,7 +2802,7 @@ function validateDefinition(definition) {
1832
2802
  }
1833
2803
  }
1834
2804
  }
1835
- function normalizeName3(value, label) {
2805
+ function normalizeName5(value, label) {
1836
2806
  if (typeof value !== "string") {
1837
2807
  throw new TypeError(
1838
2808
  `BCP Application: ${label} must be a string.`
@@ -1857,10 +2827,10 @@ function normalizeOptionalText2(value) {
1857
2827
  }
1858
2828
  return value.trim() || void 0;
1859
2829
  }
1860
- function formatServiceKey2(key) {
2830
+ function formatServiceKey3(key) {
1861
2831
  return typeof key === "symbol" ? key.description ? `Symbol(${key.description})` : key.toString() : key;
1862
2832
  }
1863
- function formatError2(value) {
2833
+ function formatError3(value) {
1864
2834
  if (value instanceof Error) {
1865
2835
  return value.message;
1866
2836
  }