@fougere/core 0.17.4-alpha.0 → 0.19.0-alpha.0

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.
Files changed (256) hide show
  1. package/README.md +1 -1
  2. package/dist/BootRefusal.d.ts +9 -0
  3. package/dist/BootRefusal.d.ts.map +1 -0
  4. package/dist/BootRefusal.js +12 -0
  5. package/dist/BootRefusal.js.map +1 -0
  6. package/dist/CallIdentity.d.ts.map +1 -1
  7. package/dist/CallIdentity.js +2 -1
  8. package/dist/CallIdentity.js.map +1 -1
  9. package/dist/Declared.d.ts +2 -5
  10. package/dist/Declared.d.ts.map +1 -1
  11. package/dist/DeclaredHandler.d.ts +3 -8
  12. package/dist/DeclaredHandler.d.ts.map +1 -1
  13. package/dist/DeclaredSubject.d.ts +2 -5
  14. package/dist/DeclaredSubject.d.ts.map +1 -1
  15. package/dist/EffectiveOperation.d.ts +7 -6
  16. package/dist/EffectiveOperation.d.ts.map +1 -1
  17. package/dist/EffectiveOperationModel.d.ts.map +1 -1
  18. package/dist/EffectiveOperationModel.js +9 -6
  19. package/dist/EffectiveOperationModel.js.map +1 -1
  20. package/dist/FrondConfig.d.ts +0 -2
  21. package/dist/FrondConfig.d.ts.map +1 -1
  22. package/dist/FrondConfig.js.map +1 -1
  23. package/dist/FrondDeclaration.d.ts +3 -8
  24. package/dist/FrondDeclaration.d.ts.map +1 -1
  25. package/dist/FrondDeclaration.js +2 -1
  26. package/dist/FrondDeclaration.js.map +1 -1
  27. package/dist/OperationOverride.d.ts +5 -3
  28. package/dist/OperationOverride.d.ts.map +1 -1
  29. package/dist/SourceView.d.ts +10 -8
  30. package/dist/SourceView.d.ts.map +1 -1
  31. package/dist/SourceView.js +10 -1
  32. package/dist/SourceView.js.map +1 -1
  33. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  34. package/dist/boot/AppLifecycle.js +2 -1
  35. package/dist/boot/AppLifecycle.js.map +1 -1
  36. package/dist/boot/Emissions.d.ts +8 -1
  37. package/dist/boot/Emissions.d.ts.map +1 -1
  38. package/dist/boot/Emissions.js +16 -11
  39. package/dist/boot/Emissions.js.map +1 -1
  40. package/dist/boot/bootstrap.d.ts.map +1 -1
  41. package/dist/boot/bootstrap.js +27 -15
  42. package/dist/boot/bootstrap.js.map +1 -1
  43. package/dist/boot/card.d.ts.map +1 -1
  44. package/dist/boot/card.js.map +1 -1
  45. package/dist/boot/install.d.ts.map +1 -1
  46. package/dist/boot/install.js +1 -0
  47. package/dist/boot/install.js.map +1 -1
  48. package/dist/boot/ownership.d.ts.map +1 -1
  49. package/dist/boot/ownership.js +26 -14
  50. package/dist/boot/ownership.js.map +1 -1
  51. package/dist/boot/unregistered.d.ts +4 -2
  52. package/dist/boot/unregistered.d.ts.map +1 -1
  53. package/dist/boot/unregistered.js +30 -30
  54. package/dist/boot/unregistered.js.map +1 -1
  55. package/dist/contract.d.ts +3 -0
  56. package/dist/contract.d.ts.map +1 -1
  57. package/dist/contract.js +3 -0
  58. package/dist/contract.js.map +1 -1
  59. package/dist/descriptor/CollectorEntry.d.ts +2 -7
  60. package/dist/descriptor/CollectorEntry.d.ts.map +1 -1
  61. package/dist/descriptor/FrondDescriptor.d.ts +5 -23
  62. package/dist/descriptor/FrondDescriptor.d.ts.map +1 -1
  63. package/dist/descriptor/FrondDescriptor.js.map +1 -1
  64. package/dist/descriptor/HandlerEntry.d.ts +2 -7
  65. package/dist/descriptor/HandlerEntry.d.ts.map +1 -1
  66. package/dist/descriptor/MiddlewareEntry.d.ts +2 -7
  67. package/dist/descriptor/MiddlewareEntry.d.ts.map +1 -1
  68. package/dist/descriptor/PresenterEntry.d.ts +2 -7
  69. package/dist/descriptor/PresenterEntry.d.ts.map +1 -1
  70. package/dist/descriptor/ProviderEntry.d.ts +2 -7
  71. package/dist/descriptor/ProviderEntry.d.ts.map +1 -1
  72. package/dist/descriptor/Subject.d.ts +10 -0
  73. package/dist/descriptor/Subject.d.ts.map +1 -0
  74. package/dist/descriptor/Subject.js +2 -0
  75. package/dist/descriptor/Subject.js.map +1 -0
  76. package/dist/descriptor/index.d.ts +1 -0
  77. package/dist/descriptor/index.d.ts.map +1 -1
  78. package/dist/descriptor/index.js +1 -0
  79. package/dist/descriptor/index.js.map +1 -1
  80. package/dist/diagnostic.d.ts +2 -1
  81. package/dist/diagnostic.d.ts.map +1 -1
  82. package/dist/diagnostic.js +2 -1
  83. package/dist/diagnostic.js.map +1 -1
  84. package/dist/dispatch/ArgumentResolver.d.ts +13 -1
  85. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -1
  86. package/dist/dispatch/ArgumentResolver.js +48 -2
  87. package/dist/dispatch/ArgumentResolver.js.map +1 -1
  88. package/dist/dispatch/EntityLookup.d.ts +4 -0
  89. package/dist/dispatch/EntityLookup.d.ts.map +1 -0
  90. package/dist/dispatch/EntityLookup.js +2 -0
  91. package/dist/dispatch/EntityLookup.js.map +1 -0
  92. package/dist/dispatch/Facade.d.ts +3 -0
  93. package/dist/dispatch/Facade.d.ts.map +1 -1
  94. package/dist/dispatch/HandlerFacade.d.ts +2 -0
  95. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  96. package/dist/dispatch/HandlerFacade.js +14 -4
  97. package/dist/dispatch/HandlerFacade.js.map +1 -1
  98. package/dist/entry/facade.d.ts +10 -1
  99. package/dist/entry/facade.d.ts.map +1 -1
  100. package/dist/entry/facade.js +26 -2
  101. package/dist/entry/facade.js.map +1 -1
  102. package/dist/index.d.ts +5 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +4 -0
  105. package/dist/index.js.map +1 -1
  106. package/dist/prefab/AggregateConstructor.d.ts +2 -2
  107. package/dist/prefab/AggregateConstructor.d.ts.map +1 -1
  108. package/dist/prefab/AggregateOf.d.ts +2 -2
  109. package/dist/prefab/AggregateOf.d.ts.map +1 -1
  110. package/dist/prefab/AggregateShape.d.ts +2 -2
  111. package/dist/prefab/AggregateShape.d.ts.map +1 -1
  112. package/dist/prefab/CrudConstructor.d.ts +2 -2
  113. package/dist/prefab/CrudConstructor.d.ts.map +1 -1
  114. package/dist/prefab/CrudConstructor.js +13 -10
  115. package/dist/prefab/CrudConstructor.js.map +1 -1
  116. package/dist/prefab/CrudOps.d.ts +2 -2
  117. package/dist/prefab/CrudOps.d.ts.map +1 -1
  118. package/dist/prefab/CrudViews.d.ts +2 -2
  119. package/dist/prefab/CrudViews.d.ts.map +1 -1
  120. package/dist/prefab/Middleware.d.ts +13 -0
  121. package/dist/prefab/Middleware.d.ts.map +1 -0
  122. package/dist/prefab/Middleware.js +10 -0
  123. package/dist/prefab/Middleware.js.map +1 -0
  124. package/dist/prefab/MirrorConstructor.d.ts +2 -2
  125. package/dist/prefab/MirrorConstructor.d.ts.map +1 -1
  126. package/dist/prefab/MirrorConstructor.js.map +1 -1
  127. package/dist/prefab/RepositoryConstructor.d.ts +4 -4
  128. package/dist/prefab/RepositoryConstructor.d.ts.map +1 -1
  129. package/dist/prefab/RepositoryConstructor.js.map +1 -1
  130. package/dist/prefab/collector.d.ts +2 -1
  131. package/dist/prefab/collector.d.ts.map +1 -1
  132. package/dist/prefab/collector.js.map +1 -1
  133. package/dist/prefab/opsOf.d.ts +4 -0
  134. package/dist/prefab/opsOf.d.ts.map +1 -0
  135. package/dist/prefab/opsOf.js +5 -0
  136. package/dist/prefab/opsOf.js.map +1 -0
  137. package/dist/prefab/prefab.d.ts +3 -3
  138. package/dist/prefab/prefab.d.ts.map +1 -1
  139. package/dist/prefab/presenter.d.ts +3 -3
  140. package/dist/prefab/presenter.d.ts.map +1 -1
  141. package/dist/prefab/presenter.js.map +1 -1
  142. package/dist/wire/Addresses.d.ts +4 -4
  143. package/dist/wire/Addresses.d.ts.map +1 -1
  144. package/dist/wire/Cardinality.d.ts +4 -0
  145. package/dist/wire/Cardinality.d.ts.map +1 -0
  146. package/dist/wire/Cardinality.js +3 -0
  147. package/dist/wire/Cardinality.js.map +1 -0
  148. package/dist/wire/Facade.d.ts +5 -7
  149. package/dist/wire/Facade.d.ts.map +1 -1
  150. package/dist/wire/Facade.js.map +1 -1
  151. package/dist/wire/Invocation.d.ts +2 -1
  152. package/dist/wire/Invocation.d.ts.map +1 -1
  153. package/dist/wire/Invocation.js +4 -1
  154. package/dist/wire/Invocation.js.map +1 -1
  155. package/dist/wire/InvocationContext.d.ts +6 -0
  156. package/dist/wire/InvocationContext.d.ts.map +1 -1
  157. package/dist/wire/OperationContract.d.ts +2 -1
  158. package/dist/wire/OperationContract.d.ts.map +1 -1
  159. package/dist/wire/OperationContract.js +2 -2
  160. package/dist/wire/OperationContract.js.map +1 -1
  161. package/dist/wire/OperationKind.d.ts +3 -1
  162. package/dist/wire/OperationKind.d.ts.map +1 -1
  163. package/dist/wire/OperationKind.js +2 -1
  164. package/dist/wire/OperationKind.js.map +1 -1
  165. package/dist/wire/Placement.d.ts +4 -0
  166. package/dist/wire/Placement.d.ts.map +1 -0
  167. package/dist/wire/Placement.js +3 -0
  168. package/dist/wire/Placement.js.map +1 -0
  169. package/dist/wire/RouteKind.d.ts +2 -1
  170. package/dist/wire/RouteKind.d.ts.map +1 -1
  171. package/dist/wire/SignedCall.d.ts +2 -0
  172. package/dist/wire/SignedCall.d.ts.map +1 -1
  173. package/dist/wire/StateShape.d.ts +1 -2
  174. package/dist/wire/StateShape.d.ts.map +1 -1
  175. package/dist/wire/StateShape.js +11 -11
  176. package/dist/wire/StateShape.js.map +1 -1
  177. package/dist/wire/TypeRef.d.ts +7 -0
  178. package/dist/wire/TypeRef.d.ts.map +1 -1
  179. package/dist/wire/binding.d.ts +11 -1
  180. package/dist/wire/binding.d.ts.map +1 -1
  181. package/dist/wire/binding.js +4 -2
  182. package/dist/wire/binding.js.map +1 -1
  183. package/dist/wire/card/CardOp.d.ts +4 -10
  184. package/dist/wire/card/CardOp.d.ts.map +1 -1
  185. package/dist/wire/refusals.d.ts +2 -1
  186. package/dist/wire/refusals.d.ts.map +1 -1
  187. package/dist/wire/refusals.js.map +1 -1
  188. package/dist/wire/topology/DeclaredFrond.d.ts +2 -1
  189. package/dist/wire/topology/DeclaredFrond.d.ts.map +1 -1
  190. package/dist/wire/topology/FrondPlacement.d.ts +2 -1
  191. package/dist/wire/topology/FrondPlacement.d.ts.map +1 -1
  192. package/package.json +3 -3
  193. package/src/BootRefusal.ts +11 -0
  194. package/src/CallIdentity.ts +2 -1
  195. package/src/Declared.ts +3 -7
  196. package/src/DeclaredHandler.ts +3 -8
  197. package/src/DeclaredSubject.ts +3 -5
  198. package/src/EffectiveOperation.ts +7 -6
  199. package/src/EffectiveOperationModel.ts +11 -7
  200. package/src/FrondConfig.ts +0 -2
  201. package/src/FrondDeclaration.ts +5 -8
  202. package/src/OperationOverride.ts +5 -3
  203. package/src/SourceView.ts +21 -3
  204. package/src/boot/AppLifecycle.ts +2 -1
  205. package/src/boot/Emissions.ts +18 -11
  206. package/src/boot/bootstrap.ts +32 -16
  207. package/src/boot/card.ts +1 -2
  208. package/src/boot/install.ts +1 -0
  209. package/src/boot/ownership.ts +35 -16
  210. package/src/boot/unregistered.ts +31 -33
  211. package/src/contract.ts +3 -0
  212. package/src/descriptor/CollectorEntry.ts +2 -7
  213. package/src/descriptor/FrondDescriptor.ts +5 -19
  214. package/src/descriptor/HandlerEntry.ts +2 -7
  215. package/src/descriptor/MiddlewareEntry.ts +2 -7
  216. package/src/descriptor/PresenterEntry.ts +2 -7
  217. package/src/descriptor/ProviderEntry.ts +2 -7
  218. package/src/descriptor/Subject.ts +10 -0
  219. package/src/descriptor/index.ts +1 -0
  220. package/src/diagnostic.ts +4 -2
  221. package/src/dispatch/ArgumentResolver.ts +54 -2
  222. package/src/dispatch/EntityLookup.ts +4 -0
  223. package/src/dispatch/Facade.ts +3 -0
  224. package/src/dispatch/HandlerFacade.ts +14 -6
  225. package/src/entry/facade.ts +38 -2
  226. package/src/index.ts +5 -1
  227. package/src/prefab/AggregateConstructor.ts +2 -2
  228. package/src/prefab/AggregateOf.ts +2 -2
  229. package/src/prefab/AggregateShape.ts +2 -2
  230. package/src/prefab/CrudConstructor.ts +18 -13
  231. package/src/prefab/CrudOps.ts +3 -3
  232. package/src/prefab/CrudViews.ts +2 -2
  233. package/src/prefab/Middleware.ts +13 -0
  234. package/src/prefab/MirrorConstructor.ts +2 -2
  235. package/src/prefab/RepositoryConstructor.ts +9 -9
  236. package/src/prefab/collector.ts +4 -3
  237. package/src/prefab/opsOf.ts +6 -0
  238. package/src/prefab/prefab.ts +5 -5
  239. package/src/prefab/presenter.ts +3 -3
  240. package/src/wire/Addresses.ts +4 -4
  241. package/src/wire/Cardinality.ts +4 -0
  242. package/src/wire/Facade.ts +6 -9
  243. package/src/wire/Invocation.ts +3 -1
  244. package/src/wire/InvocationContext.ts +6 -0
  245. package/src/wire/OperationContract.ts +4 -3
  246. package/src/wire/OperationKind.ts +4 -1
  247. package/src/wire/Placement.ts +4 -0
  248. package/src/wire/RouteKind.ts +3 -1
  249. package/src/wire/SignedCall.ts +2 -0
  250. package/src/wire/StateShape.ts +9 -11
  251. package/src/wire/TypeRef.ts +4 -0
  252. package/src/wire/binding.ts +10 -2
  253. package/src/wire/card/CardOp.ts +4 -10
  254. package/src/wire/refusals.ts +2 -1
  255. package/src/wire/topology/DeclaredFrond.ts +3 -1
  256. package/src/wire/topology/FrondPlacement.ts +3 -1
@@ -172,8 +172,16 @@ export class Emissions {
172
172
  return (this.subscribers.get(fact) ?? []).map(({ facade }) => facade);
173
173
  }
174
174
 
175
+ /**
176
+ * What a listener is handed a fact through: the calls a door makes, never the facade code
177
+ * holds. A fact arrives as the invocation's input and the listener's binding plan places it,
178
+ * wherever it sits in the signature.
179
+ */
180
+ private calls: (facade: string) => Record<string, Function> | undefined = () => undefined;
181
+
175
182
  /** Register one emission value per fact — announced here, or merely listened to. */
176
- register(): void {
183
+ register(calls: (facade: string) => Record<string, Function> | undefined): void {
184
+ this.calls = calls;
177
185
  this.orderPipes();
178
186
  for (const fact of new Set([...this.announced, ...this.subscribers.keys()])) {
179
187
  this.container.registerValue(emitKeyOf(fact), (raw: unknown) => this.announce(fact, raw));
@@ -295,8 +303,7 @@ export class Emissions {
295
303
  private async finished(fact: string, payload: unknown): Promise<unknown> {
296
304
  let carried = payload;
297
305
  for (const link of this.pipes.get(fact) ?? []) {
298
- const facade = this.container.resolve<Record<string, Function>>(link.facade);
299
- const answered = await facade[link.op]({ ...Invocation.empty, input: carried });
306
+ const answered = await this.reach(link.facade, fact)[link.op]({ ...Invocation.empty, input: carried });
300
307
 
301
308
  // A link that answers nothing SUPPRESSES the fact — every subscriber was then handed
302
309
  // `null` and crashed reading it, one message each. It is refused rather than named,
@@ -377,18 +384,18 @@ export class Emissions {
377
384
  facade,
378
385
  op,
379
386
  done: ambient.enterChain(fact, async () => {
380
- let handler: Record<string, Function>;
381
- try {
382
- handler = this.container.resolve<Record<string, Function>>(facade);
383
- } catch (cause) {
384
- throw new Error(`${fact} → ${facade} could not be reached`, { cause });
385
- }
386
-
387
- return handler[op]({ ...Invocation.empty, input: payload });
387
+ return this.reach(facade, fact)[op]({ ...Invocation.empty, input: payload });
388
388
  }),
389
389
  }));
390
390
  }
391
391
 
392
+ private reach(facade: string, fact: string): Record<string, Function> {
393
+ const calls = this.calls(facade);
394
+ if (!calls) throw new Error(`${fact} → ${facade} could not be reached`);
395
+
396
+ return calls;
397
+ }
398
+
392
399
  /** A subscriber refusing the SHAPE, said in one line instead of dumped as an error. */
393
400
  private describeRefusal(fact: string, cause: unknown): string | undefined {
394
401
  const refusals = validationErrorsOf(cause);
@@ -57,7 +57,8 @@ import { LocalRoutePolicy } from '../dispatch/LocalRoutePolicy.js';
57
57
  import { OperationRoute } from '../dispatch/OperationRoute.js';
58
58
  import { remoteRoutes } from '../dispatch/remoteRoutes.js';
59
59
  import { RouteRegistry } from '../dispatch/RouteRegistry.js';
60
- import { facadeOperations } from '../entry/facade.js';
60
+ import { callOperations, facadeOperations } from '../entry/facade.js';
61
+ import { decoded } from '../dispatch/decoded.js';
61
62
 
62
63
  const LOG_LINE = lowerFirst(LogLine.name);
63
64
 
@@ -403,6 +404,7 @@ interface Serving {
403
404
  container: Container;
404
405
  fronds: Fronds;
405
406
  remoteRouter: RemoteRouter | undefined;
407
+ dispatcher: Dispatcher;
406
408
  localDispatcher: Dispatcher;
407
409
  routeRegistry: RouteRegistry;
408
410
  effectiveByKey: Map<string, EffectiveOperationsMap>;
@@ -415,7 +417,7 @@ interface Serving {
415
417
  * `facadeFor` is THE rule, stated once: every projection reads it and nothing else.
416
418
  */
417
419
  function readings(
418
- { container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log }: Serving,
420
+ { container, fronds, remoteRouter, dispatcher, localDispatcher, routeRegistry, effectiveByKey, log }: Serving,
419
421
  ): Pick<App, 'resolve' | 'schemaFor' | 'facadeFor' | 'operationsFor' | 'presenterFor'> {
420
422
  const resolve = <T>(name: string): T => {
421
423
  try {
@@ -454,6 +456,19 @@ function readings(
454
456
  }
455
457
  };
456
458
 
459
+ /**
460
+ * What a door is handed for an address it may serve: the calls, not the facade code holds.
461
+ * The names are the routes this process registered; an address served elsewhere has none
462
+ * before it is reached, so its calls are named when they are made.
463
+ */
464
+ const callsAt = (address: string, surface: string | undefined, through: Dispatcher): Record<string, Function> => {
465
+ const names = routeRegistry.operationNames(address, surface);
466
+ if (names.length === 0) return callOperations(through, address, undefined, surface);
467
+ const effective = effectiveByKey.get(facadeKeyOf(address, surface));
468
+
469
+ return callOperations(through, address, names, surface, (operation, answer) => decoded(effective?.get(operation)?.output, answer));
470
+ };
471
+
457
472
  /** Said once per pair, so a facade that registers in a loop says it once. */
458
473
  const saidAbsent = new Set<string>();
459
474
 
@@ -474,9 +489,9 @@ function readings(
474
489
 
475
490
  /** THE membership rule, stated once — every projection reads this and nothing else. */
476
491
  const facadeFor = (address: string, surface?: string): Record<string, Function> | undefined => {
477
- if (!surface) return facadeAt(facadeKeyOf(address), true);
492
+ if (!surface) return facadeAt(facadeKeyOf(address), true) && callsAt(address, undefined, dispatcher);
478
493
 
479
- const own = facadeAt(facadeKeyOf(address, surface), false);
494
+ const own = facadeAt(facadeKeyOf(address, surface), false) && callsAt(address, surface, localDispatcher);
480
495
  if (!fronds.owner(address)) {
481
496
  sayNoSurfaceAcross(address, surface);
482
497
 
@@ -490,7 +505,7 @@ function readings(
490
505
  const fallback = facadeAt(facadeKeyOf(address), false);
491
506
 
492
507
  return fallback
493
- ? facadeOperations(
508
+ ? callOperations(
494
509
  localDispatcher,
495
510
  address,
496
511
  routeRegistry.operationNames(address, surface),
@@ -574,7 +589,7 @@ function answerRemotes(
574
589
  { container, routeRegistry, dispatcher, getMiddlewares, options, state }: Pick<Assembly, 'container' | 'routeRegistry' | 'dispatcher' | 'getMiddlewares' | 'options' | 'state'>,
575
590
  remoteRouter: RemoteRouter,
576
591
  fronds: Fronds,
577
- ): void {
592
+ ): (name: string) => boolean {
578
593
  // Only for what another process may serve: an entity a frond hosts HERE and gives no handler
579
594
  // is served by nobody, and a stand-in for it had GraphQL project an entity with no operation.
580
595
  const servedElsewhere = (address: string): boolean => {
@@ -582,9 +597,8 @@ function answerRemotes(
582
597
 
583
598
  return owner === undefined || owner.name in (options.remotes ?? {});
584
599
  };
585
- container.setFallback((name) => (isFacadeKey(name) && servedElsewhere(addressOf(name))
586
- ? facadeOperations(dispatcher, addressOf(name))
587
- : undefined));
600
+ const answeredElsewhere = (name: string): boolean => isFacadeKey(name) && servedElsewhere(addressOf(name));
601
+ container.setFallback((name) => (answeredElsewhere(name) ? facadeOperations(dispatcher, addressOf(name)) : undefined));
588
602
 
589
603
  const remoteFacades = new Map<string, Record<string, Function>>();
590
604
  routeRegistry.addResolver(remoteRoutes((address) => {
@@ -595,6 +609,8 @@ function answerRemotes(
595
609
 
596
610
  return facade;
597
611
  }));
612
+
613
+ return answeredElsewhere;
598
614
  }
599
615
 
600
616
  /** Refused rather than replaced: two declarations of one name would make the answer depend on wiring order. */
@@ -695,16 +711,16 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
695
711
  refuseWhatDoesNotHold(refused);
696
712
  warnAboutUnboundPorts(options, boundPorts, log);
697
713
 
714
+ const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
715
+ container, fronds, remoteRouter, dispatcher, localDispatcher, routeRegistry, effectiveByKey, log,
716
+ });
717
+
698
718
  // A pipe order and an `Emit<T, A>` are read here, once every facade exists.
699
- emissions.register();
719
+ emissions.register((facade) => facadeFor(addressOf(facade)));
700
720
  refuseWhatDoesNotHold(refused);
701
721
  markLineCarriers(emissions);
702
722
 
703
- if (remoteRouter) answerRemotes(assembly, remoteRouter, fronds);
704
-
705
- const { resolve, schemaFor, facadeFor, operationsFor, presenterFor } = readings({
706
- container, fronds, remoteRouter, localDispatcher, routeRegistry, effectiveByKey, log,
707
- });
723
+ const answeredElsewhere = remoteRouter ? answerRemotes(assembly, remoteRouter, fronds) : () => false;
708
724
 
709
725
  const app: App = {
710
726
  container,
@@ -741,7 +757,7 @@ export async function createApp(options: CreateAppOptions): Promise<App> {
741
757
 
742
758
  built = app;
743
759
  await appLifecycle.up(app);
744
- refuseWhatDoesNotHold(unregistered(container, fronds));
760
+ refuseWhatDoesNotHold(unregistered(container, fronds, answeredElsewhere));
745
761
  stopAnnouncing = announceLines(emissions, container, carry);
746
762
 
747
763
  return app;
package/src/boot/card.ts CHANGED
@@ -2,12 +2,11 @@
2
2
  import { Card } from '@fougere/schema';
3
3
  import type { App } from './App.js';
4
4
  import { factsAnnouncedBy } from '../wire/Emit.js';
5
- import type { InvocationContext } from '../wire/InvocationContext.js';
6
5
  import { type CardOp } from '../wire/card/CardOp.js';
7
6
  import { type IdentityCard } from '../wire/card/IdentityCard.js';
8
7
  import { facadeKeyOf } from '../wire/Facade.js';
9
8
 
10
- type AnyFacade = Record<string, (invocation?: InvocationContext) => Promise<unknown>>;
9
+ type AnyFacade = Record<string, (...args: unknown[]) => Promise<unknown>>;
11
10
 
12
11
  /** Serialize what the app hosts, for one audience. */
13
12
  export function identityCardOf(app: App, surface?: string): IdentityCard {
@@ -434,6 +434,7 @@ function buildFacadeInto(
434
434
  frond: frond.name,
435
435
  handlers: frond.handlers,
436
436
  operations: operationModel.forHandler(handler),
437
+ entities: (name) => assembly.entityByName.get(lowerFirst(name)),
437
438
  collectors: collectorTypeNames,
438
439
  presenter: presenterMap.get(handler.address),
439
440
  presenterScope: scope,
@@ -4,7 +4,7 @@ import { type ProviderEntry } from '../descriptor/ProviderEntry.js';
4
4
  import { inheritsCrud } from '../prefab/CrudConstructor.js';
5
5
  import { targetOf } from '../prefab/prefab.js';
6
6
  import { ownedBy, repositoryKeyOf } from '../prefab/RepositoryConstructor.js';
7
- import { entityOfStorageKey } from '../storage/Storage.js';
7
+ import { entityOfStorageKey, membersOfTogetherKey } from '../storage/Storage.js';
8
8
  import type { Diagnostic } from '../diagnostic.js';
9
9
 
10
10
  /** One name, one file: two providers under one key and the second silently replaces the first. */
@@ -129,23 +129,16 @@ function refuseStorageReached(
129
129
  const allowed = decl.kind === 'provider' ? builtOn(decl.ctor) : [];
130
130
 
131
131
  for (const dep of decl.deps) {
132
- const entity = entityOfStorageKey(dep, known);
133
- if (!entity) continue;
134
-
135
- const owner = owners.get(entity);
136
- if (owner && owner !== decl.ctor.name) {
137
- refused.push({
138
- severity: 'blocking',
139
- code: 'aggregate-storage-reached',
140
- filePath: decl.filePath,
141
- frond,
142
- subject: `${decl.ctor.name}(${dep})`,
143
- message: `${decl.ctor.name} asks for ${dep}, and ${owner} owns ${entity}. Nothing else `
144
- + `reaches an owned entity's storage — name the operation on ${owner} and ask for it: `
145
- + `constructor(private ${entity}: ${owner}) {}`,
146
- });
132
+ const framed = membersOfTogetherKey(dep);
133
+ if (framed) {
134
+ const members = framed.entities.map(lowerFirst).filter(known);
135
+ for (const entity of members) reachesOwned(decl, dep, entity, frond, owners, refused);
147
136
  continue;
148
137
  }
138
+
139
+ const entity = entityOfStorageKey(dep, known);
140
+ if (!entity) continue;
141
+ if (reachesOwned(decl, dep, entity, frond, owners, refused)) continue;
149
142
  if (allowed.includes(entity)) continue;
150
143
 
151
144
  refused.push({
@@ -162,6 +155,32 @@ function refuseStorageReached(
162
155
  }
163
156
  }
164
157
 
158
+ /** Whether `dep` reaches an entity an aggregate other than `decl` owns — a frame as much as a storage. */
159
+ function reachesOwned(
160
+ decl: { ctor: { name: string }; filePath: string },
161
+ dep: string,
162
+ entity: string,
163
+ frond: string,
164
+ owners: Map<string, string>,
165
+ refused: Diagnostic[],
166
+ ): boolean {
167
+ const owner = owners.get(entity);
168
+ if (!owner || owner === decl.ctor.name) return false;
169
+
170
+ refused.push({
171
+ severity: 'blocking',
172
+ code: 'aggregate-storage-reached',
173
+ filePath: decl.filePath,
174
+ frond,
175
+ subject: `${decl.ctor.name}(${dep})`,
176
+ message: `${decl.ctor.name} asks for ${dep}, and ${owner} owns ${entity}. Nothing else `
177
+ + `reaches an owned entity's storage — name the operation on ${owner} and ask for it: `
178
+ + `constructor(private ${entity}: ${owner}) {}`,
179
+ });
180
+
181
+ return true;
182
+ }
183
+
165
184
  export function storageInUserCode(
166
185
  frond: FrondDescriptor,
167
186
  owners: Map<string, string>,
@@ -1,42 +1,40 @@
1
1
  import type { Container } from '@fougere/container';
2
2
  import type { Diagnostic } from '../diagnostic.js';
3
3
  import type { FrondDescriptor } from '../descriptor/FrondDescriptor.js';
4
-
5
- type Declared = { frond: string; filePath: string };
6
-
7
- /** Every class a frond hands the container, by identity — the one key a bundler cannot rename. */
8
- function declaredBy(fronds: readonly FrondDescriptor[]): Map<Function, Declared> {
9
- const declared = new Map<Function, Declared>();
10
- for (const frond of fronds) {
11
- const entries = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors, ...frond.middlewares];
12
- for (const { ctor, filePath } of entries) declared.set(ctor, { frond: frond.name, filePath });
13
- }
14
-
15
- return declared;
16
- }
4
+ import { SEAMS } from './ports.js';
17
5
 
18
6
  /**
19
7
  * What a constructor asks for and nothing where it is built answers — refused at boot rather than
20
- * at its first call, once the extensions have risen, since `up` registers too.
8
+ * at its first call, once the extensions have risen, since `up` registers too. Read off what each
9
+ * class DECLARES, in the scope of the frond that declares it. A seam is answered by the link that
10
+ * wraps it, and a facade another process serves by the stand-in `answeredElsewhere` names.
21
11
  */
22
- export function unregistered(container: Container, fronds: readonly FrondDescriptor[]): Diagnostic[] {
23
- const declared = declaredBy(fronds);
24
- const seen = new Set<string>();
25
-
26
- return container.unresolved().flatMap(({ ctor, missing }) => {
27
- const subject = ctor.name;
28
- if (seen.has(`${subject}→${missing}`)) return [];
29
- seen.add(`${subject}→${missing}`);
30
- const where = declared.get(ctor);
12
+ export function unregistered(
13
+ container: Container,
14
+ fronds: readonly FrondDescriptor[],
15
+ answeredElsewhere: (name: string) => boolean,
16
+ ): Diagnostic[] {
17
+ const refused = new Map<string, Diagnostic>();
18
+ for (const frond of fronds) {
19
+ if (!container.has(`frond:${frond.name}`)) continue;
20
+ const scope = container.resolve<Container>(`frond:${frond.name}`);
21
+ const declared = [...frond.handlers, ...frond.providers, ...frond.presenters, ...frond.collectors, ...frond.middlewares];
22
+ for (const { ctor, deps, filePath } of declared) {
23
+ for (const missing of deps) {
24
+ if (scope.has(missing) || SEAMS.has(missing) || answeredElsewhere(missing)) continue;
25
+ if (refused.has(`${ctor.name}→${missing}`)) continue;
26
+ refused.set(`${ctor.name}→${missing}`, {
27
+ severity: 'blocking',
28
+ code: 'dependency-unregistered',
29
+ frond: frond.name,
30
+ subject: ctor.name,
31
+ filePath,
32
+ message: `${ctor.name} asks for ${missing}, and nothing in frond '${frond.name}' or above it answers that name. `
33
+ + `A class of that name in a convention directory, or one extending it, would.`,
34
+ });
35
+ }
36
+ }
37
+ }
31
38
 
32
- return [{
33
- severity: 'blocking' as const,
34
- code: 'dependency-unregistered',
35
- ...(where ? { frond: where.frond } : {}),
36
- subject,
37
- filePath: where?.filePath ?? '',
38
- message: `${subject} asks for ${missing}, and nothing ${where ? `in frond '${where.frond}' or above it ` : ''}answers that name. `
39
- + `A class of that name in a convention directory, or one extending it, would.`,
40
- }];
41
- });
39
+ return [...refused.values()];
42
40
  }
package/src/contract.ts CHANGED
@@ -53,6 +53,9 @@ export type { DeclaredEdge } from './wire/topology/DeclaredEdge.js';
53
53
  export type { DeclaredFrond } from './wire/topology/DeclaredFrond.js';
54
54
  export type { DeclaredTopology } from './wire/topology/DeclaredTopology.js';
55
55
  export type { Edge } from './wire/topology/Edge.js';
56
+ export { OPERATION_KINDS, type OperationKind } from './wire/OperationKind.js';
57
+ export { CARDINALITIES, type Cardinality } from './wire/Cardinality.js';
58
+ export { PLACEMENTS, type Placement } from './wire/Placement.js';
56
59
  export type { FrondPlacement } from './wire/topology/FrondPlacement.js';
57
60
  export type { TopologyReport } from './wire/topology/TopologyReport.js';
58
61
  export { assertIdentityCard } from './wire/card/IdentityCard.js';
@@ -1,11 +1,6 @@
1
+ import type { Subject } from './Subject.js';
1
2
  /** A discovered collector (resolves handler input params from invocation context). */
2
- export interface CollectorEntry {
3
+ export interface CollectorEntry extends Subject {
3
4
  /** Registration key of the TYPE this collector resolves — 'user', 'ability'. */
4
5
  typeName: string;
5
- /** The collector class. */
6
- ctor: new (...args: never[]) => unknown;
7
- /** Constructor dependency type names (from AST scan). */
8
- deps: string[];
9
- /** Absolute file path (for debugging). */
10
- filePath: string;
11
6
  }
@@ -1,4 +1,6 @@
1
1
  import type { OperationContract } from '../wire/OperationContract.js';
2
+ import type { OperationOverride } from '../OperationOverride.js';
3
+ import type { FrondConfig } from '../FrondConfig.js';
2
4
  import type { ProviderEntry } from './ProviderEntry.js';
3
5
  import type { EntityEntry } from './EntityEntry.js';
4
6
  import type { HandlerEntry } from './HandlerEntry.js';
@@ -15,7 +17,7 @@ import type { ExtensionEntry } from './ExtensionEntry.js';
15
17
  */
16
18
  export const nameOf = (provider: ProviderEntry): string => provider.name ?? provider.ctor.name;
17
19
 
18
- export interface FrondDescriptor {
20
+ export interface FrondDescriptor extends Pick<FrondConfig, 'pipes' | 'surfaces' | 'reads'> {
19
21
  name: string;
20
22
  source: FrondSource;
21
23
  providers: ProviderEntry[];
@@ -41,26 +43,10 @@ export interface FrondDescriptor {
41
43
  * flat, and a directory says nothing about who shares its code.
42
44
  */
43
45
  extends?: string;
44
- /** The ops that finish a fact, in order — see `FrondConfig.pipes`. */
45
- pipes?: Record<string, string[]>;
46
- /**
47
- * Per-surface entity lists from frond.config.ts (e.g. { graphql: ['Post'], rest: ['Post',
48
- * 'Author'] }).
49
- */
50
- surfaces?: Record<string, string[]>;
51
- /** Entities this frond may read across sources — see `FrondConfig.reads`. */
52
- reads?: string[];
53
46
  /** Per-operation overrides from frond.config.ts. */
54
- operationsOverrides?: Record<string, OperationContract & {
55
- kind?: 'query' | 'command';
56
- /** Class name to resolve from DI (overrides default `{Entity}Handler` lookup). */
47
+ operationsOverrides?: Record<string, OperationContract & Omit<OperationOverride, 'handler'> & {
48
+ /** The handler CLASS of `frond.config.ts`, by the name DI resolves it under. */
57
49
  handlerName?: string;
58
- /** Method name on the resolved handler (defaults to op name). */
59
- method?: string;
60
- /** The GraphQL root field this op answers to — read by `adapter/graphql`. */
61
- graphql?: string;
62
- /** Where it answers over REST — read by `adapter/rest`, the dual of `graphql`. */
63
- rest?: { method?: string; path?: string; status?: number };
64
50
  }>;
65
51
  }
66
52
 
@@ -1,8 +1,9 @@
1
+ import type { Subject } from './Subject.js';
1
2
  import type { SchemaView } from '@fougere/schema';
2
3
  import type { OperationsMap } from '../wire/OperationsMap.js';
3
4
 
4
5
  /** A discovered handler (controls what a service exposes). */
5
- export interface HandlerEntry {
6
+ export interface HandlerEntry extends Subject {
6
7
  /** Registration key (e.g. 'postHandler'). */
7
8
  name: string;
8
9
  /**
@@ -10,14 +11,8 @@ export interface HandlerEntry {
10
11
  * `post`).
11
12
  */
12
13
  address: string;
13
- /** The handler class. */
14
- ctor: new (...args: never[]) => unknown;
15
14
  /** All operations with full signatures for binding. */
16
15
  operations: OperationsMap;
17
- /** Constructor dependency type names (from AST scan). */
18
- deps: string[];
19
- /** Absolute file path (for debugging). */
20
- filePath: string;
21
16
  /** Whether this handler is part of the frond's public contract. */
22
17
  exposed?: boolean;
23
18
  /** Output schema override — when Crud(Entity, Output), restricts storage output. */
@@ -1,3 +1,4 @@
1
+ import type { Subject } from './Subject.js';
1
2
  /**
2
3
  * A discovered middleware — a class declaring `around(ctx, next)`, which runs before and after
3
4
  * every operation of its frond, and of the fronds that inherit from it.
@@ -5,13 +6,7 @@
5
6
  * How far it reaches is not written here: it is where the frond sits in `FougereConfig.fronds`,
6
7
  * and stating it twice would put one decision in two places.
7
8
  */
8
- export interface MiddlewareEntry {
9
+ export interface MiddlewareEntry extends Subject {
9
10
  /** Class name — the key it registers under in its frond's scope. */
10
11
  name: string;
11
- /** The middleware class. */
12
- ctor: new (...args: never[]) => unknown;
13
- /** Constructor dependency type names (from AST scan). */
14
- deps: string[];
15
- /** Absolute file path (for debugging). */
16
- filePath: string;
17
12
  }
@@ -1,12 +1,11 @@
1
+ import type { Subject } from './Subject.js';
1
2
  import type { PresenterViews } from '../prefab/presenter.js';
2
3
  import type { PresenterFieldMeta } from './PresenterFieldMeta.js';
3
4
 
4
5
  /** A discovered presenter (computed fields for an entity's output). */
5
- export interface PresenterEntry {
6
+ export interface PresenterEntry extends Subject {
6
7
  /** Entity name this presenter enriches (e.g. 'post' from PostPresenter). */
7
8
  entityName: string;
8
- /** The presenter class. */
9
- ctor: new (...args: never[]) => unknown;
10
9
  /** Computed field names (method names on prototype). */
11
10
  fields: string[];
12
11
  /** Per-field type metadata (inferred from source via parser). */
@@ -16,8 +15,4 @@ export interface PresenterEntry {
16
15
  * items: [OrderItemView] })`).
17
16
  */
18
17
  views?: PresenterViews;
19
- /** Constructor dependency type names (from AST scan). */
20
- deps: string[];
21
- /** Absolute file path (for debugging). */
22
- filePath: string;
23
18
  }
@@ -1,5 +1,6 @@
1
+ import type { Subject } from './Subject.js';
1
2
  /** A discovered provider — a class under `services/` or `repositories/`, injected by type. */
2
- export interface ProviderEntry {
3
+ export interface ProviderEntry extends Subject {
3
4
  /**
4
5
  * The container key — the class name as the SOURCE spells it.
5
6
  *
@@ -9,12 +10,6 @@ export interface ProviderEntry {
9
10
  * A name a tool may rewrite is written down, for the same reason `deps` is.
10
11
  */
11
12
  name?: string;
12
- /** The class constructor (default export of the file). */
13
- ctor: new (...args: never[]) => unknown;
14
- /** Constructor dependency type names (from AST scan). */
15
- deps: string[];
16
- /** Absolute file path (for debugging). */
17
- filePath: string;
18
13
  /**
19
14
  * What the source said and the runtime erased: a port, which a class below answers for and
20
15
  * nothing may instantiate — never a candidate for the ports above it.
@@ -0,0 +1,10 @@
1
+ import type { Ctor } from '../Ctor.js';
2
+
3
+ /** A class the boot builds — what its constructor asks for, and where the scan found it. */
4
+ export interface Subject {
5
+ ctor: Ctor;
6
+ /** Constructor dependency type names (from AST scan). */
7
+ deps: string[];
8
+ /** Absolute file path (for debugging). */
9
+ filePath: string;
10
+ }
@@ -21,6 +21,7 @@ export { type OperationsMap } from '../wire/OperationsMap.js';
21
21
  export { type Param } from '../wire/Param.js';
22
22
  export { type TypeRef } from '../wire/TypeRef.js';
23
23
  export { computeBindingPlan, type BindingPlan } from '../wire/binding.js';
24
+ export { opsOf } from '../prefab/opsOf.js';
24
25
  export type { Signature } from '../wire/Signature.js';
25
26
  export { awaitKeyOf, emitKeyOf } from '../wire/Emit.js';
26
27
  export { addressOf } from '../wire/Facade.js';
package/src/diagnostic.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { BootRefusal } from './BootRefusal.js';
2
+
1
3
  /**
2
4
  * One thing that does not hold, in the terms of whoever has to fix it — raised by the scan,
3
5
  * by a boot rule, or by `fougere check`, which is why it belongs to no phase.
@@ -30,12 +32,12 @@ export interface Diagnostic {
30
32
  * Every blocking diagnostic in ONE refusal, so a boot names all of them rather than the
31
33
  * first — the shape `fougere check` renders, read back by whoever has to fix it.
32
34
  */
33
- export function refusalOf(diagnostics: readonly Diagnostic[], what: string): Error | undefined {
35
+ export function refusalOf(diagnostics: readonly Diagnostic[], what: string): BootRefusal | undefined {
34
36
  const blocking = diagnostics.filter((one) => one.severity === 'blocking');
35
37
  if (blocking.length === 0) return undefined;
36
38
 
37
39
  const lines = blocking.map((one) =>
38
40
  ` [${one.code}]${one.subject ? ` ${one.subject}` : ''}\n ${one.message}\n ${one.filePath}`);
39
41
 
40
- return new Error(`Fougere boot refused: ${blocking.length} ${what}:\n${lines.join('\n')}`);
42
+ return new BootRefusal(`Fougere boot refused: ${blocking.length} ${what}:\n${lines.join('\n')}`);
41
43
  }
@@ -1,15 +1,21 @@
1
+ import { FieldValueValidator, type SchemaView } from '@fougere/schema';
1
2
  import type { InvocationContext } from '../wire/InvocationContext.js';
2
3
  import type { BindingPlan } from '../wire/binding.js';
3
4
  import { ErrorCode } from '../wire/ErrorCode.js';
4
5
  import { FougereError } from '../wire/FougereError.js';
5
6
  import type { CollectorLookup } from './CollectorLookup.js';
7
+ import type { EntityLookup } from './EntityLookup.js';
6
8
  import { collectedAs } from '../prefab/collector.js';
9
+ import { validateInput } from './validateInput.js';
7
10
 
8
11
  type ParamSource = Extract<BindingPlan[number]['source'], { kind: 'param' }>;
9
12
 
10
13
  /** Resolves an operation's declared binding plan against one invocation. */
11
14
  export class ArgumentResolver {
12
- constructor(private readonly collectors?: CollectorLookup) {}
15
+ constructor(
16
+ private readonly collectors?: CollectorLookup,
17
+ private readonly entities?: EntityLookup,
18
+ ) {}
13
19
 
14
20
  async resolve(plan: BindingPlan, ctx: InvocationContext): Promise<unknown[]> {
15
21
  const args: unknown[] = [];
@@ -26,7 +32,7 @@ export class ArgumentResolver {
26
32
  break;
27
33
  }
28
34
  case 'param': {
29
- args.push(ArgumentResolver.param(binding.source, binding.optional, ctx));
35
+ args.push(this.judged(binding.source, ArgumentResolver.param(binding.source, binding.optional, ctx)));
30
36
  break;
31
37
  }
32
38
  case 'fact':
@@ -56,6 +62,39 @@ export class ArgumentResolver {
56
62
  return args;
57
63
  }
58
64
 
65
+ /**
66
+ * The arguments code wrote, judged where a door would have judged them: the input against
67
+ * the op's view, a value a collector would have produced through that collector's entity —
68
+ * which also puts back a `Date` that crossed a wire as text. Nothing is collected: what the
69
+ * caller left out stays out.
70
+ */
71
+ given(plan: BindingPlan, input: SchemaView | undefined, ctx: InvocationContext): unknown[] {
72
+ const args = ctx.args ?? [];
73
+
74
+ return plan.map((binding, index) => {
75
+ const value = args[index];
76
+ switch (binding.source.kind) {
77
+ case 'context':
78
+ return ctx;
79
+ case 'input':
80
+ case 'fact':
81
+ case 'pipe':
82
+ return validateInput(input, { ...ctx, input: value }).input;
83
+ case 'collector': {
84
+ const collector = this.collectors?.(binding.source.typeName);
85
+
86
+ return collector ? collectedAs(collector, value) : value;
87
+ }
88
+ case 'param':
89
+ if (value === undefined && !binding.optional) throw refused(binding.source.name, 'Required');
90
+
91
+ return this.judged(binding.source, value);
92
+ case 'query':
93
+ return value;
94
+ }
95
+ });
96
+ }
97
+
59
98
  /**
60
99
  * A value the caller typed in a path or a query string. `null` is a value, not a miss: only
61
100
  * `undefined` means absent, so `T | null` stays apart from `T | undefined`.
@@ -77,6 +116,19 @@ export class ArgumentResolver {
77
116
  return coerced;
78
117
  }
79
118
 
119
+ /** `age: Sample['age']` obeys what `Sample.age` declares, after the query string was read. */
120
+ private judged(source: ParamSource, value: unknown): unknown {
121
+ if (value === undefined || !source.field) return value;
122
+
123
+ const field = this.entities?.(source.field.entity)?.getFields()[source.field.name];
124
+ if (!field) return value;
125
+
126
+ const verdict = FieldValueValidator.of(field).parse(value);
127
+ if ('message' in verdict) throw refused(source.name, verdict.message);
128
+
129
+ return verdict.value;
130
+ }
131
+
80
132
  private static number(value: unknown): number | undefined {
81
133
  if (typeof value === 'number') return Number.isNaN(value) ? undefined : value;
82
134
  if (typeof value !== 'string' || value.trim() === '') return undefined;
@@ -0,0 +1,4 @@
1
+ import type { SchemaView } from '@fougere/schema';
2
+
3
+ /** An entity this process knows, by the name a type wrote — `Post` in `Post['id']`. */
4
+ export type EntityLookup = (name: string) => SchemaView | undefined;